@almatar/smart-booking-dashboard 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/esm2022/almatar-smart-booking-dashboard.mjs +5 -0
- package/esm2022/lib/components/action-card/action-card.component.mjs +19 -0
- package/esm2022/lib/components/action-dialog/action-dialog.component.mjs +177 -0
- package/esm2022/lib/components/action-grid/action-grid.component.mjs +18 -0
- package/esm2022/lib/components/add-note-dialog/add-note-dialog.component.mjs +62 -0
- package/esm2022/lib/components/booking-input/booking-input.component.mjs +43 -0
- package/esm2022/lib/components/cancel-dialog/cancel-dialog.component.mjs +1441 -0
- package/esm2022/lib/components/data-search/data-search.component.mjs +84 -0
- package/esm2022/lib/components/flight-itinerary-card/flight-itinerary-card.component.mjs +73 -0
- package/esm2022/lib/components/result-panel/result-panel.component.mjs +46 -0
- package/esm2022/lib/components/workflow-actions/workflow-actions.component.mjs +19 -0
- package/esm2022/lib/config/dashboard.config.mjs +16 -0
- package/esm2022/lib/config/flight-actions.config.mjs +906 -0
- package/esm2022/lib/config/hotel-actions.config.mjs +53 -0
- package/esm2022/lib/config/labels.ar.mjs +116 -0
- package/esm2022/lib/models/action.types.mjs +2 -0
- package/esm2022/lib/models/booking.types.mjs +2 -0
- package/esm2022/lib/pages/smart-booking-dashboard/smart-booking-dashboard.component.mjs +145 -0
- package/esm2022/lib/services/booking.service.mjs +65 -0
- package/esm2022/lib/services/toast.service.mjs +19 -0
- package/esm2022/lib/smart-booking-dashboard.module.mjs +17 -0
- package/esm2022/lib/smart-booking-dashboard.routes.mjs +7 -0
- package/esm2022/public-api.mjs +9 -0
- package/fesm2022/almatar-smart-booking-dashboard.mjs +3259 -0
- package/fesm2022/almatar-smart-booking-dashboard.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/components/action-card/action-card.component.d.ts +10 -0
- package/lib/components/action-dialog/action-dialog.component.d.ts +26 -0
- package/lib/components/action-grid/action-grid.component.d.ts +10 -0
- package/lib/components/add-note-dialog/add-note-dialog.component.d.ts +25 -0
- package/lib/components/booking-input/booking-input.component.d.ts +23 -0
- package/lib/components/cancel-dialog/cancel-dialog.component.d.ts +395 -0
- package/lib/components/data-search/data-search.component.d.ts +21 -0
- package/lib/components/flight-itinerary-card/flight-itinerary-card.component.d.ts +17 -0
- package/lib/components/result-panel/result-panel.component.d.ts +14 -0
- package/lib/components/workflow-actions/workflow-actions.component.d.ts +7 -0
- package/lib/config/dashboard.config.d.ts +15 -0
- package/lib/config/flight-actions.config.d.ts +2 -0
- package/lib/config/hotel-actions.config.d.ts +2 -0
- package/lib/config/labels.ar.d.ts +2 -0
- package/lib/models/action.types.d.ts +34 -0
- package/lib/models/booking.types.d.ts +6 -0
- package/lib/pages/smart-booking-dashboard/smart-booking-dashboard.component.d.ts +43 -0
- package/lib/services/booking.service.d.ts +22 -0
- package/lib/services/toast.service.d.ts +10 -0
- package/lib/smart-booking-dashboard.module.d.ts +7 -0
- package/lib/smart-booking-dashboard.routes.d.ts +2 -0
- package/package.json +50 -0
- package/public-api.d.ts +5 -0
|
@@ -0,0 +1,3259 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, input, model, output, Component, computed, signal, effect, HostListener, NgModule } from '@angular/core';
|
|
3
|
+
import { NgTemplateOutlet, NgClass, LowerCasePipe, UpperCasePipe } from '@angular/common';
|
|
4
|
+
import * as i3$1 from 'primeng/button';
|
|
5
|
+
import { ButtonModule } from 'primeng/button';
|
|
6
|
+
import { HttpClient } from '@angular/common/http';
|
|
7
|
+
import { map, catchError } from 'rxjs/operators';
|
|
8
|
+
import { ToastrService } from 'ngx-toastr';
|
|
9
|
+
import * as i1 from '@angular/forms';
|
|
10
|
+
import { FormsModule } from '@angular/forms';
|
|
11
|
+
import * as i2 from 'primeng/selectbutton';
|
|
12
|
+
import { SelectButtonModule } from 'primeng/selectbutton';
|
|
13
|
+
import * as i3 from 'primeng/inputtext';
|
|
14
|
+
import { InputTextModule } from 'primeng/inputtext';
|
|
15
|
+
import * as i5 from 'primeng/tooltip';
|
|
16
|
+
import { TooltipModule } from 'primeng/tooltip';
|
|
17
|
+
import * as i1$1 from 'primeng/dialog';
|
|
18
|
+
import { DialogModule } from 'primeng/dialog';
|
|
19
|
+
import * as i1$2 from 'primeng/skeleton';
|
|
20
|
+
import { SkeletonModule } from 'primeng/skeleton';
|
|
21
|
+
import { TagModule } from 'primeng/tag';
|
|
22
|
+
import * as i5$1 from '@ng-select/ng-select';
|
|
23
|
+
import { NgSelectModule } from '@ng-select/ng-select';
|
|
24
|
+
import { of } from 'rxjs';
|
|
25
|
+
import { trigger, transition, style, animate } from '@angular/animations';
|
|
26
|
+
import * as i2$1 from 'primeng/api';
|
|
27
|
+
import * as i1$3 from '@angular/router';
|
|
28
|
+
import { RouterModule } from '@angular/router';
|
|
29
|
+
|
|
30
|
+
const SBD_DEFAULT_CONFIG = {
|
|
31
|
+
apiBase: '/api',
|
|
32
|
+
platform: 'BO',
|
|
33
|
+
};
|
|
34
|
+
const SBD_CONFIG = new InjectionToken('SBD_CONFIG', {
|
|
35
|
+
providedIn: 'root',
|
|
36
|
+
factory: () => SBD_DEFAULT_CONFIG,
|
|
37
|
+
});
|
|
38
|
+
/** Optional: call in the host app's providers to override the defaults. */
|
|
39
|
+
function provideSmartBookingDashboard(config = {}) {
|
|
40
|
+
return makeEnvironmentProviders([
|
|
41
|
+
{ provide: SBD_CONFIG, useValue: { ...SBD_DEFAULT_CONFIG, ...config } },
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class SbdBookingService {
|
|
46
|
+
constructor() {
|
|
47
|
+
this.http = inject(HttpClient);
|
|
48
|
+
this.config = inject(SBD_CONFIG);
|
|
49
|
+
this.base = this.config.apiBase;
|
|
50
|
+
}
|
|
51
|
+
getBooking(id, product, fields = 'all', language = 'en') {
|
|
52
|
+
const start = performance.now();
|
|
53
|
+
const fieldsParam = Array.isArray(fields) ? fields.join(',') : fields;
|
|
54
|
+
const productParam = product === 'flight' ? 'flights' : 'hotels';
|
|
55
|
+
const headers = { platform: this.config.platform, 'Accept-Language': language };
|
|
56
|
+
return this.http
|
|
57
|
+
.get(`${this.base}/v1/fcr/booking/information/${id}`, {
|
|
58
|
+
params: { product: productParam, fields: fieldsParam, addAirlineStamp: 'true' },
|
|
59
|
+
headers,
|
|
60
|
+
})
|
|
61
|
+
.pipe(map(data => ({ data, elapsedMs: performance.now() - start })));
|
|
62
|
+
}
|
|
63
|
+
resendTicket(id) {
|
|
64
|
+
return this.http.post(`${this.base}/v1/fcr/booking/${id}/resend`, {});
|
|
65
|
+
}
|
|
66
|
+
getRefundDetails(id, product = 'flight', language = 'en') {
|
|
67
|
+
const productParam = product === 'flight' ? 'flights' : 'hotels';
|
|
68
|
+
const headers = { platform: this.config.platform };
|
|
69
|
+
if (language === 'ar')
|
|
70
|
+
headers['Accept-Language'] = 'ar';
|
|
71
|
+
return this.http.post(`${this.base}/v1/fcr/refund/details`, {
|
|
72
|
+
bookingId: id,
|
|
73
|
+
product: productParam,
|
|
74
|
+
}, { headers });
|
|
75
|
+
}
|
|
76
|
+
addNote(id, product = 'flight', message, action = 'GENERAL') {
|
|
77
|
+
const productParam = product === 'flight' ? 'flight' : 'hotel';
|
|
78
|
+
return this.http.put(`${this.base}/v1/back-office/booking/${productParam}/addNote/${id}`, { notes: [{ message, action }] });
|
|
79
|
+
}
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
confirmCancel(id, product = 'flight', legInfo, selectedSource, preferredRefundMethod, splitAmounts) {
|
|
82
|
+
const productParam = product === 'flight' ? 'flights' : 'hotels';
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
84
|
+
const body = { bookingId: String(id), product: productParam };
|
|
85
|
+
if (legInfo && legInfo.length > 0)
|
|
86
|
+
body.legInfo = legInfo;
|
|
87
|
+
if (selectedSource)
|
|
88
|
+
body.selectedSource = selectedSource;
|
|
89
|
+
if (preferredRefundMethod)
|
|
90
|
+
body.preferredRefundMethod = preferredRefundMethod;
|
|
91
|
+
if (preferredRefundMethod === 'split' && splitAmounts) {
|
|
92
|
+
body.walletRefundAmount = splitAmounts.walletRefundAmount;
|
|
93
|
+
body.totalCardRefundAmount = splitAmounts.totalCardRefundAmount;
|
|
94
|
+
}
|
|
95
|
+
return this.http.post(`${this.base}/v1/fcr/refund`, body, { headers: { platform: this.config.platform } });
|
|
96
|
+
}
|
|
97
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdBookingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
98
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdBookingService, providedIn: 'root' }); }
|
|
99
|
+
}
|
|
100
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdBookingService, decorators: [{
|
|
101
|
+
type: Injectable,
|
|
102
|
+
args: [{ providedIn: 'root' }]
|
|
103
|
+
}] });
|
|
104
|
+
|
|
105
|
+
class SbdToastService {
|
|
106
|
+
constructor() {
|
|
107
|
+
this.toastr = inject(ToastrService);
|
|
108
|
+
}
|
|
109
|
+
success(msg) { this.toastr.success(msg); }
|
|
110
|
+
error(msg) { this.toastr.error(msg); }
|
|
111
|
+
info(msg) { this.toastr.info(msg); }
|
|
112
|
+
warning(msg) { this.toastr.warning(msg); }
|
|
113
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
114
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdToastService, providedIn: 'root' }); }
|
|
115
|
+
}
|
|
116
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdToastService, decorators: [{
|
|
117
|
+
type: Injectable,
|
|
118
|
+
args: [{ providedIn: 'root' }]
|
|
119
|
+
}] });
|
|
120
|
+
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
+
const na$1 = (v) => v != null && v !== '' ? String(v) : 'Not available';
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
124
|
+
const bi = (data) => data?.data?.booking_info;
|
|
125
|
+
const fmtMins = (mins) => mins != null ? `${Math.floor(mins / 60)}h ${mins % 60}m` : 'Not available';
|
|
126
|
+
const fmtDate = (dt) => {
|
|
127
|
+
if (!dt)
|
|
128
|
+
return 'Not available';
|
|
129
|
+
try {
|
|
130
|
+
return new Date(dt).toLocaleString('en-GB', {
|
|
131
|
+
day: '2-digit', month: 'short', year: 'numeric',
|
|
132
|
+
hour: '2-digit', minute: '2-digit',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return dt;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const dirIcon = (dir) => (dir === 'Return' || dir === 'return') ? '←' : '→';
|
|
140
|
+
/**
|
|
141
|
+
* Format a baggage allowance object (CabinBaggage / CheckedBaggage / UnderSeatBaggage).
|
|
142
|
+
* Negative values (-1) indicate the allowance is not applicable.
|
|
143
|
+
*/
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
145
|
+
const fmtBag = (bag) => {
|
|
146
|
+
if (!bag)
|
|
147
|
+
return 'Not included';
|
|
148
|
+
const { Pieces, Weight, Unit, Dimension, Remark } = bag;
|
|
149
|
+
// Negative Pieces = not applicable (e.g. Pieces: -1, Weight: -1)
|
|
150
|
+
if (Pieces != null && Pieces < 0 && (Weight == null || Weight < 0))
|
|
151
|
+
return 'Not included';
|
|
152
|
+
// Zero across the board = not included
|
|
153
|
+
if (Pieces === 0 && (Weight === 0 || Weight == null))
|
|
154
|
+
return 'Not included';
|
|
155
|
+
const parts = [];
|
|
156
|
+
if (Pieces != null && Pieces > 0)
|
|
157
|
+
parts.push(`${Pieces} pc${Pieces > 1 ? 's' : ''}`);
|
|
158
|
+
if (Weight != null && Weight > 0)
|
|
159
|
+
parts.push(`${Weight} ${Unit ?? 'KG'}`);
|
|
160
|
+
if (Dimension)
|
|
161
|
+
parts.push(Dimension);
|
|
162
|
+
if (Remark)
|
|
163
|
+
parts.push(Remark);
|
|
164
|
+
return parts.length > 0 ? parts.join(' · ') : 'Not available';
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Resolve pnr_info to a flat array regardless of shape.
|
|
168
|
+
* New BE format: [{pnr, LegDirectionality}, ...]
|
|
169
|
+
* Legacy format: {pnr: '...'} or null
|
|
170
|
+
*/
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
|
+
const resolvePnrList = (pnrRaw) => {
|
|
173
|
+
if (Array.isArray(pnrRaw))
|
|
174
|
+
return pnrRaw;
|
|
175
|
+
if (pnrRaw?.pnr)
|
|
176
|
+
return [{ pnr: pnrRaw.pnr, LegDirectionality: 'Departure' }];
|
|
177
|
+
return [];
|
|
178
|
+
};
|
|
179
|
+
const FLIGHT_ACTIONS = [
|
|
180
|
+
// ── Booking Overview ──────────────────────────────────────────────────────
|
|
181
|
+
{
|
|
182
|
+
key: 'booking_info',
|
|
183
|
+
type: 'flight',
|
|
184
|
+
label: 'Booking Overview',
|
|
185
|
+
subtitle: 'Route · Class · Fare',
|
|
186
|
+
icon: 'pi pi-file',
|
|
187
|
+
apiField: 'booking_info',
|
|
188
|
+
formatter: (data) => {
|
|
189
|
+
const d = bi(data);
|
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
191
|
+
const legs = d?.leg_info ?? [];
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
193
|
+
const airlines = d?.airline_info ?? [];
|
|
194
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
195
|
+
const passengers = d?.passenger_info?.passengers ?? [];
|
|
196
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
197
|
+
const baggage = d?.baggage;
|
|
198
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
199
|
+
const dur = d?.duration;
|
|
200
|
+
const ps = d?.payment_status;
|
|
201
|
+
const cp = d?.cancellation_policy;
|
|
202
|
+
const pnrList = resolvePnrList(d?.pnr_info);
|
|
203
|
+
const pnrDep = pnrList.find((p) => p.LegDirectionality === 'Departure')?.pnr ?? pnrList[0]?.pnr;
|
|
204
|
+
const pnrRet = pnrList.find((p) => p.LegDirectionality === 'Return')?.pnr;
|
|
205
|
+
const origin = legs[0]?.LegDepartureAirportCode;
|
|
206
|
+
const dest = legs[legs.length - 1]?.LegArrivalAirportCode;
|
|
207
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
208
|
+
const sections = [];
|
|
209
|
+
// ── Booking Reference ────────────────────────────────────────────────
|
|
210
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
211
|
+
const refRows = [];
|
|
212
|
+
if (pnrList.length > 1) {
|
|
213
|
+
pnrList.forEach((p) => refRows.push({ label: `PNR (${p.LegDirectionality})`, value: na$1(p.pnr) }));
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
refRows.push({ label: 'PNR', value: na$1(pnrDep) });
|
|
217
|
+
}
|
|
218
|
+
refRows.push({ label: 'Booking ID', value: na$1(cp?.bookingId) }, { label: 'Supplier ref', value: na$1(cp?.supplierBookingId) }, { label: 'Supplier', value: na$1(cp?.supplierName ?? legs[0]?.supplierName) });
|
|
219
|
+
sections.push({ title: 'Booking Reference', rows: refRows });
|
|
220
|
+
// ── Per-leg sections ─────────────────────────────────────────────────
|
|
221
|
+
legs.forEach((l, i) => {
|
|
222
|
+
const dir = l.LegDirectionality ?? '';
|
|
223
|
+
const from = l.LegDepartureAirportCode;
|
|
224
|
+
const to = l.LegArrivalAirportCode;
|
|
225
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
226
|
+
const segs = airlines[i]?.Segments ?? [];
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
228
|
+
const durSegs = dur?.legs?.[i]?.Segments ?? [];
|
|
229
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
230
|
+
const stops = l.Stops ?? [];
|
|
231
|
+
const legDurMins = durSegs.reduce((sum, s) => sum + (s.SegmentElapsedTimeMinutes ?? 0), 0);
|
|
232
|
+
const flightNos = segs.map((s) => `${s.AirlineCode}${s.FlightNumber}`).filter(Boolean).join(', ');
|
|
233
|
+
sections.push({
|
|
234
|
+
title: `${dirIcon(dir)} ${dir || `Leg ${i + 1}`} · ${from} → ${to}`,
|
|
235
|
+
rows: [
|
|
236
|
+
{ label: 'Departure', value: fmtDate(l.LegDepartureDateTime) },
|
|
237
|
+
{ label: 'Arrival', value: fmtDate(l.LegArrivalDateTime) },
|
|
238
|
+
{ label: 'Duration', value: legDurMins > 0 ? fmtMins(legDurMins) : na$1(null) },
|
|
239
|
+
{ label: 'Airline(s)', value: na$1(l.LegAirlinesNames?.join(', ')) },
|
|
240
|
+
{ label: 'Flight no.', value: na$1(flightNos) },
|
|
241
|
+
{ label: 'Cabin', value: na$1(l.LegCabins?.[0]?.Name) },
|
|
242
|
+
{ label: 'Fare brand', value: na$1(l.fareBrand?.name) },
|
|
243
|
+
{
|
|
244
|
+
label: 'Stops',
|
|
245
|
+
value: stops.length === 0
|
|
246
|
+
? 'Non-stop'
|
|
247
|
+
: stops.map((s) => `${s.StopAirportCity ?? s.StopAirport} (${fmtMins(s.LayoverDuration)})`).join(', '),
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
// ── Passengers ───────────────────────────────────────────────────────
|
|
253
|
+
const paxTypeLabel = { ADT: 'Adult', CNN: 'Child', INF: 'Infant' };
|
|
254
|
+
passengers.forEach((p, i) => {
|
|
255
|
+
const fullName = [p?.name?.prefix, p?.name?.givenName, p?.name?.surName].filter(Boolean).join(' ');
|
|
256
|
+
const phone = p?.phone ?? (p?.contactNumber?.phoneNumber
|
|
257
|
+
? `${p.contactNumber.countryCode ?? ''}${p.contactNumber.phoneNumber}` : null);
|
|
258
|
+
sections.push({
|
|
259
|
+
title: `Passenger ${i + 1} · ${fullName || na$1(null)}`,
|
|
260
|
+
rows: [
|
|
261
|
+
{ label: 'Type', value: na$1(paxTypeLabel[p?.type] ?? p?.type) },
|
|
262
|
+
{ label: 'Gender', value: na$1(p?.gender) },
|
|
263
|
+
{ label: 'Date of birth', value: na$1(p?.dateOfBirth) },
|
|
264
|
+
{ label: 'Passport', value: na$1(p?.identity?.number) },
|
|
265
|
+
{ label: 'Nationality', value: na$1(p?.identity?.nationality) },
|
|
266
|
+
{ label: 'Passport exp.', value: na$1(p?.identity?.expirationDate) },
|
|
267
|
+
{ label: 'Issue country', value: na$1(p?.identity?.issueCountry) },
|
|
268
|
+
{ label: 'Doc type', value: na$1(p?.identity?.type) },
|
|
269
|
+
{ label: 'Email', value: na$1(p?.email) },
|
|
270
|
+
{ label: 'Phone', value: na$1(phone) },
|
|
271
|
+
],
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
// ── Payment ──────────────────────────────────────────────────────────
|
|
275
|
+
if (ps) {
|
|
276
|
+
const statusName = ps?.status?.name ?? ps?.status?.key;
|
|
277
|
+
const paid = ps?.amountInPaidCurrency != null && ps?.paidCurrency
|
|
278
|
+
? `${ps.amountInPaidCurrency} ${ps.paidCurrency}` : null;
|
|
279
|
+
sections.push({
|
|
280
|
+
title: 'Payment',
|
|
281
|
+
rows: [
|
|
282
|
+
{ label: 'Status', value: na$1(statusName) },
|
|
283
|
+
{ label: 'Amount', value: ps?.amount != null ? `${ps.amount} ${na$1(ps.currency)}` : 'Not available' },
|
|
284
|
+
{ label: 'Paid amount', value: na$1(paid) },
|
|
285
|
+
{ label: 'Payment method', value: na$1(ps?.method) },
|
|
286
|
+
{ label: 'Date', value: fmtDate(ps?.date) },
|
|
287
|
+
],
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
// ── Baggage per route (full detail) ──────────────────────────────────
|
|
291
|
+
const baggageInfo = baggage?.baggage_info ?? [];
|
|
292
|
+
baggageInfo.forEach((entry) => {
|
|
293
|
+
const route = Object.keys(entry)[0];
|
|
294
|
+
if (!route)
|
|
295
|
+
return;
|
|
296
|
+
const paxTypes = entry[route] ?? {};
|
|
297
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
298
|
+
const rows = [];
|
|
299
|
+
Object.entries(paxTypes).forEach(([paxType, pd]) => {
|
|
300
|
+
const label = paxType === 'ADT' ? 'Adult' : paxType === 'CNN' ? 'Child' : paxType === 'INF' ? 'Infant' : paxType;
|
|
301
|
+
const cabin = pd.CabinBaggage;
|
|
302
|
+
const checked = pd.CheckedBaggage;
|
|
303
|
+
const underSeat = pd.UnderSeatBaggage;
|
|
304
|
+
rows.push({ label: `${label} · Cabin`, value: fmtBag(cabin) }, { label: `${label} · Checked`, value: fmtBag(checked) });
|
|
305
|
+
if (underSeat)
|
|
306
|
+
rows.push({ label: `${label} · Under Seat`, value: fmtBag(underSeat) });
|
|
307
|
+
});
|
|
308
|
+
sections.push({ title: `Baggage · ${route}`, rows });
|
|
309
|
+
});
|
|
310
|
+
// ── Terminal Info summary ────────────────────────────────────────────
|
|
311
|
+
const terminalData = d?.terminal ?? [];
|
|
312
|
+
terminalData.forEach((tl, li) => {
|
|
313
|
+
const legMeta = legs[li];
|
|
314
|
+
const legLabel = legMeta
|
|
315
|
+
? `Terminal · ${legMeta.LegDepartureAirportCode} → ${legMeta.LegArrivalAirportCode}`
|
|
316
|
+
: `Terminal · Leg ${li + 1}`;
|
|
317
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
318
|
+
const termRows = [];
|
|
319
|
+
(tl.Segments ?? []).forEach((s, si) => {
|
|
320
|
+
const airlineSeg = airlines[li]?.Segments?.[si];
|
|
321
|
+
const flightNo = airlineSeg ? `${airlineSeg.AirlineCode}${airlineSeg.FlightNumber}` : `Seg ${si + 1}`;
|
|
322
|
+
const depTerm = s.DepartureAirportTerminalID ? `T${s.DepartureAirportTerminalID}` : '—';
|
|
323
|
+
const arrTerm = s.ArrivalAirportTerminalID ? `T${s.ArrivalAirportTerminalID}` : '—';
|
|
324
|
+
termRows.push({
|
|
325
|
+
label: flightNo,
|
|
326
|
+
value: `${na$1(s.DepartureAirportCode)} ${depTerm} → ${na$1(s.ArrivalAirportCode)} ${arrTerm}`,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
if (termRows.length > 0)
|
|
330
|
+
sections.push({ title: legLabel, rows: termRows });
|
|
331
|
+
});
|
|
332
|
+
// ── Cancellation Policy summary ──────────────────────────────────────
|
|
333
|
+
const cpLegs = cp?.legs ?? [];
|
|
334
|
+
cpLegs.forEach((leg) => {
|
|
335
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
336
|
+
const cpRows = [];
|
|
337
|
+
(leg.passengerPenalties ?? []).forEach((pp) => {
|
|
338
|
+
const typeLabel = pp.type === 'ADT' ? 'Adult' : pp.type === 'CNN' ? 'Child' : 'Infant';
|
|
339
|
+
const bDep = pp.penalties?.beforeDeparture;
|
|
340
|
+
const refund = bDep?.refund;
|
|
341
|
+
const exchange = bDep?.exchange;
|
|
342
|
+
const refundStr = refund?.isActionable ? `Refund: ${refund.fee} ${refund.currency}` : 'No refund';
|
|
343
|
+
const exchangeStr = exchange?.isActionable ? `Exchange: ${exchange.fee} ${exchange.currency}` : 'No exchange';
|
|
344
|
+
cpRows.push({ label: typeLabel, value: `${refundStr} · ${exchangeStr}` });
|
|
345
|
+
});
|
|
346
|
+
if (cpRows.length > 0) {
|
|
347
|
+
sections.push({
|
|
348
|
+
title: `Cancellation and Change Policy · ${leg.direction} (${leg.from} → ${leg.to})`,
|
|
349
|
+
rows: cpRows,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
return {
|
|
354
|
+
title: origin ? `${origin} → ${dest}` : 'Flight Booking',
|
|
355
|
+
subtitle: pnrDep
|
|
356
|
+
? `PNR: ${pnrDep}${pnrRet ? ' / ' + pnrRet : ''} · ${legs.length} leg${legs.length !== 1 ? 's' : ''} · ${passengers.length} pax`
|
|
357
|
+
: `${legs.length} leg${legs.length !== 1 ? 's' : ''} · ${passengers.length} pax`,
|
|
358
|
+
sections,
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
// ── Terminal Info ─────────────────────────────────────────────────────────
|
|
363
|
+
{
|
|
364
|
+
key: 'terminal',
|
|
365
|
+
type: 'flight',
|
|
366
|
+
label: 'Terminal Info',
|
|
367
|
+
subtitle: 'Gate · Check-in',
|
|
368
|
+
icon: 'pi pi-send',
|
|
369
|
+
apiField: 'booking_info',
|
|
370
|
+
formatter: (data) => {
|
|
371
|
+
const d = bi(data);
|
|
372
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
373
|
+
const terminalLegs = d?.terminal ?? [];
|
|
374
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
375
|
+
const legInfos = d?.leg_info ?? [];
|
|
376
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
377
|
+
const airlines = d?.airline_info ?? [];
|
|
378
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
379
|
+
const sections = [];
|
|
380
|
+
terminalLegs.forEach((tl, li) => {
|
|
381
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
382
|
+
const segs = tl.Segments ?? [];
|
|
383
|
+
const legMeta = legInfos[li];
|
|
384
|
+
const legLabel = legMeta
|
|
385
|
+
? `${dirIcon(legMeta.LegDirectionality)} ${legMeta.LegDepartureAirportCode} → ${legMeta.LegArrivalAirportCode}`
|
|
386
|
+
: `Leg ${li + 1}`;
|
|
387
|
+
// Leg-level times as fallback when segment-level times are absent
|
|
388
|
+
const legDepTime = legMeta?.LegDepartureDateTime;
|
|
389
|
+
const legArrTime = legMeta?.LegArrivalDateTime;
|
|
390
|
+
segs.forEach((s, si) => {
|
|
391
|
+
const airlineSeg = airlines[li]?.Segments?.[si];
|
|
392
|
+
// Resolve flight number: terminal seg → airline_info seg
|
|
393
|
+
const flightNo = s.FlightNumber
|
|
394
|
+
? `${s.AirlineCode ?? ''}${s.FlightNumber}`
|
|
395
|
+
: airlineSeg ? `${airlineSeg.AirlineCode}${airlineSeg.FlightNumber}` : null;
|
|
396
|
+
// Resolve times: terminal seg → airline_info seg → leg-level fallback
|
|
397
|
+
const depTime = s.DepartureDateTime
|
|
398
|
+
?? airlineSeg?.DepartureDateTime
|
|
399
|
+
?? (si === 0 ? legDepTime : null);
|
|
400
|
+
const arrTime = s.ArrivalDateTime
|
|
401
|
+
?? airlineSeg?.ArrivalDateTime
|
|
402
|
+
?? (si === segs.length - 1 ? legArrTime : null);
|
|
403
|
+
sections.push({
|
|
404
|
+
title: `${legLabel} · ${flightNo ?? `Seg ${si + 1}`}`,
|
|
405
|
+
rows: [
|
|
406
|
+
{ label: 'Departure airport', value: s.DepartureAirportName ? `${s.DepartureAirportName} (${s.DepartureAirportCode})` : na$1(s.DepartureAirportCode) },
|
|
407
|
+
{ label: 'Dep. terminal', value: na$1(s.DepartureAirportTerminalID) },
|
|
408
|
+
{ label: 'Arrival airport', value: s.ArrivalAirportName ? `${s.ArrivalAirportName} (${s.ArrivalAirportCode})` : na$1(s.ArrivalAirportCode) },
|
|
409
|
+
{ label: 'Arr. terminal', value: na$1(s.ArrivalAirportTerminalID) },
|
|
410
|
+
{ label: 'Flight', value: na$1(flightNo) },
|
|
411
|
+
{ label: 'Departure time', value: fmtDate(depTime) },
|
|
412
|
+
{ label: 'Arrival time', value: fmtDate(arrTime) },
|
|
413
|
+
],
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
const totalSegs = sections.length;
|
|
418
|
+
return {
|
|
419
|
+
title: 'Terminal Details',
|
|
420
|
+
subtitle: `${totalSegs} segment${totalSegs !== 1 ? 's' : ''}`,
|
|
421
|
+
sections: sections.length
|
|
422
|
+
? sections
|
|
423
|
+
: [{ title: 'Info', rows: [{ label: 'Status', value: 'No terminal data available' }] }],
|
|
424
|
+
};
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
// ── Payment Status ────────────────────────────────────────────────────────
|
|
428
|
+
{
|
|
429
|
+
key: 'payment_status',
|
|
430
|
+
type: 'flight',
|
|
431
|
+
hidden: true,
|
|
432
|
+
label: 'Payment Status',
|
|
433
|
+
subtitle: 'Charges · Receipt',
|
|
434
|
+
icon: 'pi pi-credit-card',
|
|
435
|
+
apiField: 'booking_info',
|
|
436
|
+
formatter: (data) => {
|
|
437
|
+
const ps = bi(data)?.payment_status;
|
|
438
|
+
const statusName = ps?.status?.name ?? ps?.status?.key;
|
|
439
|
+
const paid = ps?.amountInPaidCurrency != null && ps?.paidCurrency
|
|
440
|
+
? `${ps.amountInPaidCurrency} ${ps.paidCurrency}` : null;
|
|
441
|
+
const isSuccess = ps?.status?.key === 'SUCCESS';
|
|
442
|
+
return {
|
|
443
|
+
title: 'Payment Details',
|
|
444
|
+
subtitle: na$1(statusName),
|
|
445
|
+
sections: [
|
|
446
|
+
{
|
|
447
|
+
title: 'Transaction',
|
|
448
|
+
rows: [
|
|
449
|
+
{ label: 'Status', value: na$1(statusName), valueClass: isSuccess ? 'positive' : 'neutral' },
|
|
450
|
+
{ label: 'Amount', value: ps?.amount != null ? `${ps.amount} ${na$1(ps.currency)}` : 'Not available' },
|
|
451
|
+
{ label: 'Paid amount', value: na$1(paid) },
|
|
452
|
+
{ label: 'Payment method', value: na$1(ps?.method) },
|
|
453
|
+
{ label: 'Transaction ref', value: na$1(ps?.transactionRef ?? ps?.reference) },
|
|
454
|
+
{ label: 'Date', value: fmtDate(ps?.date) },
|
|
455
|
+
{ label: 'Currency', value: na$1(ps?.currency) },
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
};
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
// ── Passenger Info ────────────────────────────────────────────────────────
|
|
463
|
+
{
|
|
464
|
+
key: 'passenger_info',
|
|
465
|
+
type: 'flight',
|
|
466
|
+
hidden: true,
|
|
467
|
+
label: 'Passenger Info',
|
|
468
|
+
subtitle: 'Names · Docs',
|
|
469
|
+
icon: 'pi pi-users',
|
|
470
|
+
apiField: 'booking_info',
|
|
471
|
+
formatter: (data) => {
|
|
472
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
473
|
+
const passengers = bi(data)?.passenger_info?.passengers ?? [];
|
|
474
|
+
const typeLabel = { ADT: 'Adult', CNN: 'Child', INF: 'Infant' };
|
|
475
|
+
return {
|
|
476
|
+
title: 'Passenger Details',
|
|
477
|
+
subtitle: `${passengers.length} traveller${passengers.length !== 1 ? 's' : ''}`,
|
|
478
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
479
|
+
sections: passengers.map((p, i) => {
|
|
480
|
+
const fullName = [p?.name?.prefix, p?.name?.givenName, p?.name?.surName].filter(Boolean).join(' ');
|
|
481
|
+
const phone = p?.phone ?? (p?.contactNumber?.phoneNumber
|
|
482
|
+
? `${p.contactNumber.countryCode ?? ''}${p.contactNumber.phoneNumber}` : null);
|
|
483
|
+
return {
|
|
484
|
+
title: `Passenger ${i + 1} · ${fullName || na$1(null)}`,
|
|
485
|
+
rows: [
|
|
486
|
+
{ label: 'Type', value: na$1(typeLabel[p?.type] ?? p?.type) },
|
|
487
|
+
{ label: 'Gender', value: na$1(p?.gender) },
|
|
488
|
+
{ label: 'Date of birth', value: na$1(p?.dateOfBirth) },
|
|
489
|
+
{ label: 'Passport', value: na$1(p?.identity?.number) },
|
|
490
|
+
{ label: 'Nationality', value: na$1(p?.identity?.nationality) },
|
|
491
|
+
{ label: 'Passport exp.', value: na$1(p?.identity?.expirationDate) },
|
|
492
|
+
{ label: 'Issue country', value: na$1(p?.identity?.issueCountry) },
|
|
493
|
+
{ label: 'Doc type', value: na$1(p?.identity?.type) },
|
|
494
|
+
{ label: 'Email', value: na$1(p?.email) },
|
|
495
|
+
{ label: 'Phone', value: na$1(phone) },
|
|
496
|
+
],
|
|
497
|
+
};
|
|
498
|
+
}),
|
|
499
|
+
};
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
// ── Passenger Count ───────────────────────────────────────────────────────
|
|
503
|
+
{
|
|
504
|
+
key: 'passenger_count',
|
|
505
|
+
type: 'flight',
|
|
506
|
+
hidden: true,
|
|
507
|
+
label: 'Passenger Count',
|
|
508
|
+
subtitle: 'Adults · Children · Infants',
|
|
509
|
+
icon: 'pi pi-users',
|
|
510
|
+
apiField: 'booking_info',
|
|
511
|
+
formatter: (data) => {
|
|
512
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
513
|
+
const passengers = bi(data)?.passenger_info?.passengers ?? [];
|
|
514
|
+
const adults = passengers.filter(p => p?.type === 'ADT');
|
|
515
|
+
const children = passengers.filter(p => p?.type === 'CNN');
|
|
516
|
+
const infants = passengers.filter(p => p?.type === 'INF');
|
|
517
|
+
return {
|
|
518
|
+
title: 'Traveller Breakdown',
|
|
519
|
+
subtitle: `${passengers.length} total`,
|
|
520
|
+
sections: [
|
|
521
|
+
{
|
|
522
|
+
title: 'Summary',
|
|
523
|
+
rows: [
|
|
524
|
+
{ label: 'Total', value: String(passengers.length) },
|
|
525
|
+
{ label: 'Adults (ADT)', value: String(adults.length) },
|
|
526
|
+
{ label: 'Children (CNN)', value: String(children.length) },
|
|
527
|
+
{ label: 'Infants (INF)', value: String(infants.length) },
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
...adults.map((p, i) => ({
|
|
531
|
+
title: `Adult ${i + 1} · ${[p?.name?.prefix, p?.name?.givenName, p?.name?.surName].filter(Boolean).join(' ') || 'N/A'}`,
|
|
532
|
+
rows: [
|
|
533
|
+
{ label: 'Passport', value: na$1(p?.identity?.number) },
|
|
534
|
+
{ label: 'Email', value: na$1(p?.email) },
|
|
535
|
+
],
|
|
536
|
+
})),
|
|
537
|
+
...children.map((p, i) => ({
|
|
538
|
+
title: `Child ${i + 1} · ${[p?.name?.prefix, p?.name?.givenName, p?.name?.surName].filter(Boolean).join(' ') || 'N/A'}`,
|
|
539
|
+
rows: [
|
|
540
|
+
{ label: 'Passport', value: na$1(p?.identity?.number) },
|
|
541
|
+
{ label: 'DOB', value: na$1(p?.dateOfBirth) },
|
|
542
|
+
],
|
|
543
|
+
})),
|
|
544
|
+
...infants.map((p, i) => ({
|
|
545
|
+
title: `Infant ${i + 1} · ${[p?.name?.prefix, p?.name?.givenName, p?.name?.surName].filter(Boolean).join(' ') || 'N/A'}`,
|
|
546
|
+
rows: [
|
|
547
|
+
{ label: 'DOB', value: na$1(p?.dateOfBirth) },
|
|
548
|
+
],
|
|
549
|
+
})),
|
|
550
|
+
],
|
|
551
|
+
};
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
// ── Airline Info ──────────────────────────────────────────────────────────
|
|
555
|
+
{
|
|
556
|
+
key: 'airline_info',
|
|
557
|
+
type: 'flight',
|
|
558
|
+
hidden: true,
|
|
559
|
+
label: 'Airline Info',
|
|
560
|
+
subtitle: 'Carrier · Flight no.',
|
|
561
|
+
icon: 'pi pi-send',
|
|
562
|
+
apiField: 'booking_info',
|
|
563
|
+
formatter: (data) => {
|
|
564
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
565
|
+
const airlines = bi(data)?.airline_info ?? [];
|
|
566
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
567
|
+
const legInfos = bi(data)?.leg_info ?? [];
|
|
568
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
569
|
+
const durLegs = bi(data)?.duration?.legs ?? [];
|
|
570
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
571
|
+
const sections = [];
|
|
572
|
+
airlines.forEach((a, li) => {
|
|
573
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
574
|
+
const segs = a.Segments ?? [];
|
|
575
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
576
|
+
const durSegs = durLegs[li]?.Segments ?? [];
|
|
577
|
+
const legMeta = legInfos[li];
|
|
578
|
+
const legLabel = legMeta
|
|
579
|
+
? `${dirIcon(legMeta.LegDirectionality)} ${legMeta.LegDepartureAirportCode} → ${legMeta.LegArrivalAirportCode}`
|
|
580
|
+
: `Leg ${li + 1}`;
|
|
581
|
+
// Leg-level times as fallback for first/last segment
|
|
582
|
+
const legDepTime = legMeta?.LegDepartureDateTime;
|
|
583
|
+
const legArrTime = legMeta?.LegArrivalDateTime;
|
|
584
|
+
segs.forEach((s, si) => {
|
|
585
|
+
// Use segment-level times if available, fall back to leg times
|
|
586
|
+
const depTime = s.DepartureDateTime ?? (si === 0 ? legDepTime : null);
|
|
587
|
+
const arrTime = s.ArrivalDateTime ?? (si === segs.length - 1 ? legArrTime : null);
|
|
588
|
+
sections.push({
|
|
589
|
+
title: `${legLabel} · ${s.AirlineCode}${s.FlightNumber} (Seg ${si + 1})`,
|
|
590
|
+
rows: [
|
|
591
|
+
{ label: 'Airline', value: na$1(s.AirlineName) },
|
|
592
|
+
{ label: 'IATA code', value: na$1(s.AirlineCode) },
|
|
593
|
+
{ label: 'Flight no.', value: s.FlightNumber ? `${s.AirlineCode}${s.FlightNumber}` : 'Not available' },
|
|
594
|
+
{ label: 'Route', value: `${na$1(s.DepartureAirportCode)} → ${na$1(s.ArrivalAirportCode)}` },
|
|
595
|
+
{ label: 'Departure', value: fmtDate(depTime) },
|
|
596
|
+
{ label: 'Arrival', value: fmtDate(arrTime) },
|
|
597
|
+
{ label: 'Duration', value: durSegs[si]?.SegmentElapsedTimeMinutes ? fmtMins(durSegs[si].SegmentElapsedTimeMinutes) : 'Not available' },
|
|
598
|
+
{ label: 'Aircraft', value: na$1(s.AircraftType ?? s.Equipment) },
|
|
599
|
+
{ label: 'Operating code', value: na$1(s.OperatingAirlineCode) },
|
|
600
|
+
],
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
const totalSegs = sections.length;
|
|
605
|
+
return {
|
|
606
|
+
title: airlines[0]?.Segments?.[0]?.AirlineName ?? 'Airline Info',
|
|
607
|
+
subtitle: `${totalSegs} segment${totalSegs !== 1 ? 's' : ''}`,
|
|
608
|
+
sections,
|
|
609
|
+
};
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
// ── Leg Info ──────────────────────────────────────────────────────────────
|
|
613
|
+
{
|
|
614
|
+
key: 'leg_info',
|
|
615
|
+
type: 'flight',
|
|
616
|
+
hidden: true,
|
|
617
|
+
label: 'Leg Info',
|
|
618
|
+
subtitle: 'Route · Stops · Cabin',
|
|
619
|
+
icon: 'pi pi-map-marker',
|
|
620
|
+
apiField: 'booking_info',
|
|
621
|
+
formatter: (data) => {
|
|
622
|
+
const d = bi(data);
|
|
623
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
624
|
+
const legs = d?.leg_info ?? [];
|
|
625
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
626
|
+
const airlines = d?.airline_info ?? [];
|
|
627
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
628
|
+
const durLegs = d?.duration?.legs ?? [];
|
|
629
|
+
return {
|
|
630
|
+
title: 'Flight Legs',
|
|
631
|
+
subtitle: `${legs.length} leg${legs.length !== 1 ? 's' : ''}`,
|
|
632
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
633
|
+
sections: legs.map((l, i) => {
|
|
634
|
+
const dir = l.LegDirectionality ?? '';
|
|
635
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
636
|
+
const stops = l.Stops ?? [];
|
|
637
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
638
|
+
const segs = airlines[i]?.Segments ?? [];
|
|
639
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
640
|
+
const durSegs = durLegs[i]?.Segments ?? [];
|
|
641
|
+
const legDurMins = durSegs.reduce((sum, s) => sum + (s.SegmentElapsedTimeMinutes ?? 0), 0);
|
|
642
|
+
const flightNos = segs.map((s) => `${s.AirlineCode}${s.FlightNumber}`).filter(Boolean).join(', ');
|
|
643
|
+
return {
|
|
644
|
+
title: `${dirIcon(dir)} ${dir || `Leg ${i + 1}`} · ${l.LegDepartureAirportCode} → ${l.LegArrivalAirportCode}`,
|
|
645
|
+
rows: [
|
|
646
|
+
{ label: 'Direction', value: na$1(dir) },
|
|
647
|
+
{ label: 'Departure', value: fmtDate(l.LegDepartureDateTime) },
|
|
648
|
+
{ label: 'Arrival', value: fmtDate(l.LegArrivalDateTime) },
|
|
649
|
+
{ label: 'Duration', value: legDurMins > 0 ? fmtMins(legDurMins) : na$1(null) },
|
|
650
|
+
{ label: 'Airline(s)', value: na$1(l.LegAirlinesNames?.join(', ')) },
|
|
651
|
+
{ label: 'Flight no.', value: na$1(flightNos) },
|
|
652
|
+
{ label: 'Cabin', value: na$1(l.LegCabins?.[0]?.Name) },
|
|
653
|
+
{ label: 'Fare brand', value: na$1(l.fareBrand?.name) },
|
|
654
|
+
{ label: 'Supplier', value: na$1(l.supplierName) },
|
|
655
|
+
{
|
|
656
|
+
label: 'Stops',
|
|
657
|
+
value: stops.length === 0
|
|
658
|
+
? 'Non-stop'
|
|
659
|
+
: stops.map((s) => `${s.StopAirportCity ?? s.StopAirport} (${na$1(s.StopAirport)}) – ${fmtMins(s.LayoverDuration)} layover`).join(' · '),
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
};
|
|
663
|
+
}),
|
|
664
|
+
};
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
// ── PNR Info ──────────────────────────────────────────────────────────────
|
|
668
|
+
{
|
|
669
|
+
key: 'pnr_info',
|
|
670
|
+
type: 'flight',
|
|
671
|
+
label: 'PNR Info',
|
|
672
|
+
subtitle: 'PNR · Booking ref',
|
|
673
|
+
icon: 'pi pi-tag',
|
|
674
|
+
apiField: 'booking_info',
|
|
675
|
+
formatter: (data) => {
|
|
676
|
+
const d = bi(data);
|
|
677
|
+
const pnrList = resolvePnrList(d?.pnr_info);
|
|
678
|
+
const pnrDep = pnrList.find((p) => p.LegDirectionality === 'Departure')?.pnr ?? pnrList[0]?.pnr;
|
|
679
|
+
const pnrRet = pnrList.find((p) => p.LegDirectionality === 'Return')?.pnr;
|
|
680
|
+
const cp = d?.cancellation_policy;
|
|
681
|
+
const isCrossSeller = pnrList.length > 1
|
|
682
|
+
|| cp?.supplierName?.toLowerCase().includes('cross')
|
|
683
|
+
|| false;
|
|
684
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
685
|
+
const pnrRows = [];
|
|
686
|
+
if (pnrList.length > 1) {
|
|
687
|
+
pnrList.forEach((p) => {
|
|
688
|
+
pnrRows.push({ label: `PNR (${p.LegDirectionality})`, value: na$1(p.pnr) });
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
pnrRows.push({ label: 'PNR', value: na$1(pnrDep) });
|
|
693
|
+
}
|
|
694
|
+
pnrRows.push({ label: 'Booking ID', value: na$1(cp?.bookingId) }, { label: 'Supplier ref', value: na$1(cp?.supplierBookingId) }, { label: 'Supplier', value: na$1(cp?.supplierName) }, { label: 'Cross-seller', value: isCrossSeller ? 'Yes' : 'No' });
|
|
695
|
+
return {
|
|
696
|
+
title: pnrDep ? `PNR: ${pnrDep}${pnrRet ? ' / ' + pnrRet : ''}` : 'PNR Info',
|
|
697
|
+
subtitle: isCrossSeller ? 'Cross-seller booking — two PNRs' : 'Global Distribution System reference',
|
|
698
|
+
sections: [{ title: 'References', rows: pnrRows }],
|
|
699
|
+
};
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
// ── Baggage Policy ────────────────────────────────────────────────────────
|
|
703
|
+
{
|
|
704
|
+
key: 'baggage',
|
|
705
|
+
type: 'flight',
|
|
706
|
+
label: 'Baggage Policy',
|
|
707
|
+
subtitle: 'Cabin · Checked · Add-ons',
|
|
708
|
+
icon: 'pi pi-briefcase',
|
|
709
|
+
apiField: 'booking_info',
|
|
710
|
+
formatter: (data) => {
|
|
711
|
+
const baggage = bi(data)?.baggage;
|
|
712
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
713
|
+
const infoArray = baggage?.baggage_info ?? [];
|
|
714
|
+
const addOns = baggage?.add_on_baggage ?? [];
|
|
715
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
716
|
+
const sections = [];
|
|
717
|
+
const routeNames = [];
|
|
718
|
+
infoArray.forEach((entry) => {
|
|
719
|
+
const route = Object.keys(entry)[0];
|
|
720
|
+
if (!route)
|
|
721
|
+
return;
|
|
722
|
+
routeNames.push(route);
|
|
723
|
+
const paxTypes = entry[route] ?? {};
|
|
724
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
725
|
+
const rows = [];
|
|
726
|
+
Object.entries(paxTypes).forEach(([paxType, pd]) => {
|
|
727
|
+
const label = paxType === 'ADT' ? 'Adult' : paxType === 'CNN' ? 'Child' : paxType === 'INF' ? 'Infant' : paxType;
|
|
728
|
+
const cabin = pd.CabinBaggage;
|
|
729
|
+
const checked = pd.CheckedBaggage;
|
|
730
|
+
const underSeat = pd.UnderSeatBaggage;
|
|
731
|
+
rows.push({ label: `${label} · Cabin`, value: fmtBag(cabin) }, { label: `${label} · Checked`, value: fmtBag(checked) });
|
|
732
|
+
if (underSeat) {
|
|
733
|
+
rows.push({ label: `${label} · Under Seat`, value: fmtBag(underSeat) });
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
sections.push({ title: `Baggage · ${route}`, rows });
|
|
737
|
+
});
|
|
738
|
+
if (addOns.length > 0) {
|
|
739
|
+
sections.push({
|
|
740
|
+
title: 'Add-on Baggage',
|
|
741
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
742
|
+
rows: addOns.map((a, i) => ({
|
|
743
|
+
label: `Add-on ${i + 1}`,
|
|
744
|
+
value: [a.weight, a.unit, a.route].filter(Boolean).join(' · ') || 'See booking details',
|
|
745
|
+
})),
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
title: 'Baggage Allowance',
|
|
750
|
+
subtitle: routeNames.length > 0 ? routeNames.join(' · ') : 'All baggage types',
|
|
751
|
+
sections: sections.length > 0
|
|
752
|
+
? sections
|
|
753
|
+
: [{ title: 'Info', rows: [{ label: 'Status', value: 'No baggage data available' }] }],
|
|
754
|
+
};
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
// ── Flight Timings ────────────────────────────────────────────────────────
|
|
758
|
+
{
|
|
759
|
+
key: 'flight_timings',
|
|
760
|
+
type: 'flight',
|
|
761
|
+
hidden: true,
|
|
762
|
+
label: 'Flight Timings',
|
|
763
|
+
subtitle: 'Departure · Arrival',
|
|
764
|
+
icon: 'pi pi-clock',
|
|
765
|
+
apiField: 'booking_info',
|
|
766
|
+
formatter: (data) => {
|
|
767
|
+
const d = bi(data);
|
|
768
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
769
|
+
const legs = d?.leg_info ?? [];
|
|
770
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
771
|
+
const airlines = d?.airline_info ?? [];
|
|
772
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
773
|
+
const durLegs = d?.duration?.legs ?? [];
|
|
774
|
+
return {
|
|
775
|
+
title: 'Flight Timings',
|
|
776
|
+
subtitle: legs.length > 0
|
|
777
|
+
? `${legs[0].LegDepartureAirportCode} → ${legs[legs.length - 1].LegArrivalAirportCode}`
|
|
778
|
+
: 'All legs',
|
|
779
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
780
|
+
sections: legs.map((l, li) => {
|
|
781
|
+
const dir = l.LegDirectionality ?? '';
|
|
782
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
783
|
+
const segs = airlines[li]?.Segments ?? [];
|
|
784
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
785
|
+
const durSegs = durLegs[li]?.Segments ?? [];
|
|
786
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
787
|
+
const stops = l.Stops ?? [];
|
|
788
|
+
const legDurMins = durSegs.reduce((sum, s) => sum + (s.SegmentElapsedTimeMinutes ?? 0), 0);
|
|
789
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
790
|
+
const rows = [
|
|
791
|
+
{ label: 'Departure', value: fmtDate(l.LegDepartureDateTime) },
|
|
792
|
+
{ label: 'Arrival', value: fmtDate(l.LegArrivalDateTime) },
|
|
793
|
+
{ label: 'Duration', value: legDurMins > 0 ? fmtMins(legDurMins) : na$1(null) },
|
|
794
|
+
];
|
|
795
|
+
// Interleave segment rows with layover rows
|
|
796
|
+
segs.forEach((s, si) => {
|
|
797
|
+
const segDur = durSegs[si]?.SegmentElapsedTimeMinutes;
|
|
798
|
+
rows.push({
|
|
799
|
+
label: `Seg ${si + 1} · ${s.AirlineCode}${s.FlightNumber}`,
|
|
800
|
+
value: `${na$1(s.DepartureAirportCode)} → ${na$1(s.ArrivalAirportCode)}${segDur ? ' · ' + fmtMins(segDur) : ''}`,
|
|
801
|
+
});
|
|
802
|
+
// Layover after this segment (if there is a stop at arrival airport)
|
|
803
|
+
if (si < segs.length - 1) {
|
|
804
|
+
const stop = stops.find((st) => st.StopAirport === s.ArrivalAirportCode);
|
|
805
|
+
if (stop) {
|
|
806
|
+
rows.push({ label: `Layover · ${stop.StopAirport}`, value: fmtMins(stop.LayoverDuration) });
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
return {
|
|
811
|
+
title: `${dirIcon(dir)} ${dir || `Leg ${li + 1}`} · ${l.LegDepartureAirportCode} → ${l.LegArrivalAirportCode}`,
|
|
812
|
+
rows,
|
|
813
|
+
};
|
|
814
|
+
}),
|
|
815
|
+
};
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
// ── Duration ──────────────────────────────────────────────────────────────
|
|
819
|
+
{
|
|
820
|
+
key: 'duration',
|
|
821
|
+
type: 'flight',
|
|
822
|
+
hidden: true,
|
|
823
|
+
label: 'Duration',
|
|
824
|
+
subtitle: 'Total flight time',
|
|
825
|
+
icon: 'pi pi-refresh',
|
|
826
|
+
apiField: 'booking_info',
|
|
827
|
+
formatter: (data) => {
|
|
828
|
+
const d = bi(data);
|
|
829
|
+
const dur = d?.duration;
|
|
830
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
831
|
+
const legs = d?.leg_info ?? [];
|
|
832
|
+
const total = fmtMins(dur?.totalMinutes);
|
|
833
|
+
return {
|
|
834
|
+
title: total,
|
|
835
|
+
subtitle: 'Total journey duration including layovers',
|
|
836
|
+
sections: [
|
|
837
|
+
{
|
|
838
|
+
title: 'Total',
|
|
839
|
+
rows: [{ label: 'Journey duration', value: total }],
|
|
840
|
+
},
|
|
841
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
842
|
+
...(dur?.legs ?? []).map((leg, li) => {
|
|
843
|
+
const legMeta = legs[li];
|
|
844
|
+
const dir = legMeta?.LegDirectionality ?? '';
|
|
845
|
+
const from = legMeta?.LegDepartureAirportCode;
|
|
846
|
+
const to = legMeta?.LegArrivalAirportCode;
|
|
847
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
848
|
+
const rows = [];
|
|
849
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
850
|
+
(leg.Segments ?? []).forEach((s) => {
|
|
851
|
+
rows.push({ label: `${s.DepartureAirportCode} → ${s.ArrivalAirportCode}`, value: fmtMins(s.SegmentElapsedTimeMinutes) });
|
|
852
|
+
});
|
|
853
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
854
|
+
(leg.Stops ?? []).forEach((s) => {
|
|
855
|
+
rows.push({ label: `Layover · ${s.StopAirport}`, value: fmtMins(s.LayoverDuration) });
|
|
856
|
+
});
|
|
857
|
+
return {
|
|
858
|
+
title: `${dirIcon(dir)} ${dir || `Leg ${li + 1}`}${from && to ? ` · ${from} → ${to}` : ''}`,
|
|
859
|
+
rows,
|
|
860
|
+
};
|
|
861
|
+
}),
|
|
862
|
+
],
|
|
863
|
+
};
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
// ── Cancellation Policy ───────────────────────────────────────────────────
|
|
867
|
+
{
|
|
868
|
+
key: 'cancellation_policy',
|
|
869
|
+
type: 'flight',
|
|
870
|
+
label: 'Cancellation Policy',
|
|
871
|
+
subtitle: 'Refund · Fees · Legs',
|
|
872
|
+
icon: 'pi pi-ban',
|
|
873
|
+
apiField: 'booking_info',
|
|
874
|
+
formatter: (data) => {
|
|
875
|
+
const d = bi(data);
|
|
876
|
+
const cp = d?.cancellation_policy;
|
|
877
|
+
if (!cp) {
|
|
878
|
+
return {
|
|
879
|
+
title: 'Cancellation Policy',
|
|
880
|
+
subtitle: 'No data available',
|
|
881
|
+
sections: [{ title: 'Info', rows: [{ label: 'Status', value: 'No cancellation policy data available' }] }],
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
885
|
+
const sections = [];
|
|
886
|
+
const pnrList = resolvePnrList(d?.pnr_info);
|
|
887
|
+
const isCrossSeller = pnrList.length > 1 || cp?.supplierName?.toLowerCase().includes('cross');
|
|
888
|
+
const legs = cp?.legs ?? [];
|
|
889
|
+
sections.push({
|
|
890
|
+
title: 'Booking Reference',
|
|
891
|
+
rows: [
|
|
892
|
+
{ label: 'Booking ID', value: na$1(cp.bookingId) },
|
|
893
|
+
{ label: 'Supplier ref', value: na$1(cp.supplierBookingId) },
|
|
894
|
+
{ label: 'Supplier', value: na$1(cp.supplierName) },
|
|
895
|
+
{ label: 'Cross-seller', value: isCrossSeller ? 'Yes' : 'No' },
|
|
896
|
+
],
|
|
897
|
+
});
|
|
898
|
+
const fmtFee = (fee, currency) => fee != null ? `${fee} ${currency ?? 'SAR'}` : '0';
|
|
899
|
+
legs.forEach((leg) => {
|
|
900
|
+
const title = `Cancellation and Change Policy · ${leg.direction} (${leg.from} → ${leg.to})`;
|
|
901
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
902
|
+
const rows = [];
|
|
903
|
+
(leg.passengerPenalties ?? []).forEach((pp) => {
|
|
904
|
+
const typeLabel = pp.type === 'ADT' ? 'Adult' : pp.type === 'CNN' ? 'Child' : 'Infant';
|
|
905
|
+
const bDep = pp.penalties?.beforeDeparture;
|
|
906
|
+
const aDep = pp.penalties?.afterDeparture;
|
|
907
|
+
const bRefund = bDep?.refund;
|
|
908
|
+
const bExchange = bDep?.exchange;
|
|
909
|
+
const aRefund = aDep?.refund;
|
|
910
|
+
const aExchange = aDep?.exchange;
|
|
911
|
+
rows.push({
|
|
912
|
+
label: `${typeLabel} · Before dep.`,
|
|
913
|
+
value: [
|
|
914
|
+
bRefund?.isActionable ? `Refund: ${fmtFee(bRefund.fee, bRefund.currency)}` : 'No refund',
|
|
915
|
+
bExchange?.isActionable ? `Exchange: ${fmtFee(bExchange.fee, bExchange.currency)}` : 'No exchange',
|
|
916
|
+
].join(' · '),
|
|
917
|
+
valueClass: bRefund?.isActionable || bExchange?.isActionable ? 'neutral' : 'negative',
|
|
918
|
+
}, {
|
|
919
|
+
label: `${typeLabel} · After dep.`,
|
|
920
|
+
value: [
|
|
921
|
+
aRefund?.isActionable ? `Refund: ${fmtFee(aRefund.fee, aRefund.currency)}` : 'No refund',
|
|
922
|
+
aExchange?.isActionable ? `Exchange: ${fmtFee(aExchange.fee, aExchange.currency)}` : 'No exchange',
|
|
923
|
+
].join(' · '),
|
|
924
|
+
valueClass: aRefund?.isActionable || aExchange?.isActionable ? 'neutral' : 'negative',
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
sections.push({ title, rows });
|
|
928
|
+
});
|
|
929
|
+
return {
|
|
930
|
+
title: `${cp.supplierName ?? 'Cancellation'} Policy`,
|
|
931
|
+
subtitle: isCrossSeller ? 'Cross-seller booking · contact supplier for refund' : `Booking ${cp.bookingId ?? ''}`,
|
|
932
|
+
sections,
|
|
933
|
+
callout: isCrossSeller
|
|
934
|
+
? { type: 'info', message: 'Cross-seller booking — refund is processed by the partner and may take additional time.' }
|
|
935
|
+
: undefined,
|
|
936
|
+
};
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
// ── Flight Status ─────────────────────────────────────────────────────────
|
|
940
|
+
{
|
|
941
|
+
key: 'flight_status',
|
|
942
|
+
type: 'flight',
|
|
943
|
+
hidden: true,
|
|
944
|
+
label: 'Flight Status',
|
|
945
|
+
subtitle: 'Live · Delays',
|
|
946
|
+
icon: 'pi pi-chart-bar',
|
|
947
|
+
apiField: 'booking_info',
|
|
948
|
+
formatter: (data) => {
|
|
949
|
+
const d = bi(data);
|
|
950
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
951
|
+
const legs = d?.leg_info ?? [];
|
|
952
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
953
|
+
const airlines = d?.airline_info ?? [];
|
|
954
|
+
const origin = legs[0]?.LegDepartureAirportCode;
|
|
955
|
+
const dest = legs[legs.length - 1]?.LegArrivalAirportCode;
|
|
956
|
+
return {
|
|
957
|
+
title: origin ? `${origin} → ${dest}` : 'Flight Status',
|
|
958
|
+
subtitle: `${legs.length} leg${legs.length !== 1 ? 's' : ''} · Scheduled times from booking`,
|
|
959
|
+
sections: legs.map((l, li) => {
|
|
960
|
+
const dir = l.LegDirectionality ?? '';
|
|
961
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
962
|
+
const segs = airlines[li]?.Segments ?? [];
|
|
963
|
+
return {
|
|
964
|
+
title: `${dirIcon(dir)} ${dir || `Leg ${li + 1}`} · ${l.LegDepartureAirportCode} → ${l.LegArrivalAirportCode}`,
|
|
965
|
+
rows: [
|
|
966
|
+
{ label: 'Departure', value: fmtDate(l.LegDepartureDateTime) },
|
|
967
|
+
{ label: 'Arrival', value: fmtDate(l.LegArrivalDateTime) },
|
|
968
|
+
{ label: 'Airline(s)', value: na$1(l.LegAirlinesNames?.join(', ')) },
|
|
969
|
+
{ label: 'Cabin', value: na$1(l.LegCabins?.[0]?.Name) },
|
|
970
|
+
{ label: 'Fare brand', value: na$1(l.fareBrand?.name) },
|
|
971
|
+
...segs.map((s, si) => ({
|
|
972
|
+
label: `Flight ${si + 1}`,
|
|
973
|
+
value: `${s.AirlineCode}${s.FlightNumber} · ${s.DepartureAirportCode} → ${s.ArrivalAirportCode}`,
|
|
974
|
+
})),
|
|
975
|
+
],
|
|
976
|
+
};
|
|
977
|
+
}),
|
|
978
|
+
callout: { type: 'info', message: 'Live status not available — check the airline app or airport boards for real-time updates.' },
|
|
979
|
+
};
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
// ── Wi-Fi & Extras ────────────────────────────────────────────────────────
|
|
983
|
+
{
|
|
984
|
+
key: 'wifi',
|
|
985
|
+
type: 'flight',
|
|
986
|
+
hidden: true,
|
|
987
|
+
label: 'Wi-Fi & Extras',
|
|
988
|
+
subtitle: 'Onboard services',
|
|
989
|
+
icon: 'pi pi-info-circle',
|
|
990
|
+
apiField: 'booking_info',
|
|
991
|
+
formatter: (data) => {
|
|
992
|
+
const d = bi(data);
|
|
993
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
994
|
+
const legs = d?.leg_info ?? [];
|
|
995
|
+
const addOns = d?.baggage?.add_on_baggage ?? [];
|
|
996
|
+
return {
|
|
997
|
+
title: 'Onboard Extras',
|
|
998
|
+
subtitle: `${legs.length} leg${legs.length !== 1 ? 's' : ''}`,
|
|
999
|
+
sections: [
|
|
1000
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1001
|
+
...legs.map((l, li) => {
|
|
1002
|
+
const dir = l.LegDirectionality ?? '';
|
|
1003
|
+
return {
|
|
1004
|
+
title: `${dirIcon(dir)} ${dir || `Leg ${li + 1}`} · ${l.LegDepartureAirportCode} → ${l.LegArrivalAirportCode}`,
|
|
1005
|
+
rows: [
|
|
1006
|
+
{ label: 'Cabin', value: na$1(l.LegCabins?.[0]?.Name) },
|
|
1007
|
+
{ label: 'Fare brand', value: na$1(l.fareBrand?.name) },
|
|
1008
|
+
{ label: 'Airline(s)', value: na$1(l.LegAirlinesNames?.join(', ')) },
|
|
1009
|
+
],
|
|
1010
|
+
};
|
|
1011
|
+
}),
|
|
1012
|
+
...(addOns.length > 0 ? [{
|
|
1013
|
+
title: 'Add-on Baggage',
|
|
1014
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1015
|
+
rows: addOns.map((a, i) => ({
|
|
1016
|
+
label: `Add-on ${i + 1}`,
|
|
1017
|
+
value: [a.weight, a.unit, a.route].filter(Boolean).join(' · ') || 'See booking details',
|
|
1018
|
+
})),
|
|
1019
|
+
}] : []),
|
|
1020
|
+
],
|
|
1021
|
+
callout: { type: 'info', message: 'Detailed onboard amenities depend on the operating airline and fare class.' },
|
|
1022
|
+
};
|
|
1023
|
+
},
|
|
1024
|
+
},
|
|
1025
|
+
];
|
|
1026
|
+
|
|
1027
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1028
|
+
const na = (v) => v ?? 'Not available';
|
|
1029
|
+
const HOTEL_ACTIONS = [
|
|
1030
|
+
{ key: 'booking_info', type: 'hotel', label: 'Booking Overview', subtitle: 'Summary · Status', icon: 'pi pi-file',
|
|
1031
|
+
apiField: 'booking_info',
|
|
1032
|
+
formatter: (data) => ({ title: `Booking ${na(data?.booking_info?.reference)}`, subtitle: `Created ${na(data?.booking_info?.created_at)}`,
|
|
1033
|
+
points: [{ label: 'Status', value: na(data?.booking_info?.status) }, { label: 'Total paid', value: na(data?.booking_info?.total) }, { label: 'Source', value: na(data?.booking_info?.source) }, { label: 'Currency', value: na(data?.booking_info?.currency) }] }) },
|
|
1034
|
+
{ key: 'room_information', type: 'hotel', label: 'Room Information', subtitle: 'Type · Beds · View', icon: 'pi pi-home',
|
|
1035
|
+
apiField: 'room_information',
|
|
1036
|
+
formatter: (data) => ({ title: na(data?.room_information?.type), subtitle: na(data?.room_information?.description),
|
|
1037
|
+
points: [{ label: 'Room type', value: na(data?.room_information?.type) }, { label: 'Beds', value: na(data?.room_information?.beds) }, { label: 'View', value: na(data?.room_information?.view) }, { label: 'Floor', value: na(data?.room_information?.floor) }, { label: 'Size', value: na(data?.room_information?.size) }] }) },
|
|
1038
|
+
{ key: 'checkin_checkout', type: 'hotel', label: 'Check-in / Check-out', subtitle: 'Dates · Times', icon: 'pi pi-calendar',
|
|
1039
|
+
apiField: 'checkin_checkout',
|
|
1040
|
+
formatter: (data) => ({ title: `${na(data?.checkin_checkout?.checkin_date)} → ${na(data?.checkin_checkout?.checkout_date)}`, subtitle: 'Stay dates and times',
|
|
1041
|
+
points: [{ label: 'Check-in date', value: na(data?.checkin_checkout?.checkin_date) }, { label: 'Check-in time', value: na(data?.checkin_checkout?.checkin_time) }, { label: 'Check-out date', value: na(data?.checkin_checkout?.checkout_date) }, { label: 'Check-out time', value: na(data?.checkin_checkout?.checkout_time) }, { label: 'Nights', value: na(data?.checkin_checkout?.nights) }] }) },
|
|
1042
|
+
{ key: 'hotel_information', type: 'hotel', label: 'Hotel Information', subtitle: 'Name · Stars · Contact', icon: 'pi pi-building',
|
|
1043
|
+
apiField: 'hotel_information',
|
|
1044
|
+
formatter: (data) => ({ title: na(data?.hotel_information?.name), subtitle: `${na(data?.hotel_information?.stars)}★ · ${na(data?.hotel_information?.city)}`,
|
|
1045
|
+
points: [{ label: 'Hotel', value: na(data?.hotel_information?.name) }, { label: 'Stars', value: na(data?.hotel_information?.stars) }, { label: 'Phone', value: na(data?.hotel_information?.phone) }, { label: 'Email', value: na(data?.hotel_information?.email) }, { label: 'Website', value: na(data?.hotel_information?.website) }] }) },
|
|
1046
|
+
{ key: 'location_info', type: 'hotel', label: 'Location Info', subtitle: 'Address · Map', icon: 'pi pi-map-marker',
|
|
1047
|
+
apiField: 'location_info',
|
|
1048
|
+
formatter: (data) => ({ title: na(data?.location_info?.address), subtitle: na(data?.location_info?.city),
|
|
1049
|
+
points: [{ label: 'Address', value: na(data?.location_info?.address) }, { label: 'City', value: na(data?.location_info?.city) }, { label: 'Country', value: na(data?.location_info?.country) }, { label: 'Zip code', value: na(data?.location_info?.zip) }, { label: 'Landmark', value: na(data?.location_info?.landmark) }] }) },
|
|
1050
|
+
{ key: 'hotel_amenities', type: 'hotel', label: 'Hotel Amenities', subtitle: 'Pool · Gym · Wi-Fi', icon: 'pi pi-star',
|
|
1051
|
+
apiField: 'hotel_amenities',
|
|
1052
|
+
formatter: (data) => { const amenities = data?.hotel_amenities?.list ?? []; return { title: 'Amenities', subtitle: `${amenities.length} amenities available`, points: amenities.length > 0 ? amenities.map((a) => ({ label: '✓', value: a })) : [{ label: 'Amenities', value: 'Not available' }] }; } },
|
|
1053
|
+
{ key: 'nearby_places', type: 'hotel', label: 'Nearby Places', subtitle: 'Airport · Attractions', icon: 'pi pi-compass',
|
|
1054
|
+
apiField: 'nearby_places',
|
|
1055
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1056
|
+
formatter: (data) => { const places = data?.nearby_places?.places ?? []; return { title: 'Nearby Places', points: places.length > 0 ? places.flatMap((p) => [{ label: na(p?.type), value: `${na(p?.name)} (${na(p?.distance)})` }]) : [{ label: 'Places', value: 'Not available' }] }; } },
|
|
1057
|
+
{ key: 'experiences', type: 'hotel', label: 'Experiences', subtitle: 'Tours · Activities', icon: 'pi pi-star-fill',
|
|
1058
|
+
apiField: 'experiences',
|
|
1059
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1060
|
+
formatter: (data) => { const items = data?.experiences?.list ?? []; return { title: 'Available Experiences', points: items.length > 0 ? items.map((e) => ({ label: na(e?.name), value: na(e?.price) })) : [{ label: 'Experiences', value: 'Not available' }] }; } },
|
|
1061
|
+
{ key: 'payment_status', type: 'hotel', label: 'Payment Status', subtitle: 'Charges · Receipt', icon: 'pi pi-credit-card',
|
|
1062
|
+
apiField: 'payment_status',
|
|
1063
|
+
formatter: (data) => ({ title: 'Payment Details', subtitle: na(data?.payment_status?.method),
|
|
1064
|
+
points: [{ label: 'Status', value: na(data?.payment_status?.status) }, { label: 'Amount paid', value: na(data?.payment_status?.amount) }, { label: 'Currency', value: na(data?.payment_status?.currency) }, { label: 'Transaction', value: na(data?.payment_status?.transaction_id) }, { label: 'Date', value: na(data?.payment_status?.date) }] }) },
|
|
1065
|
+
{ key: 'wallet', type: 'hotel', label: 'Wallet & Credits', subtitle: 'Balance · Cashback', icon: 'pi pi-wallet',
|
|
1066
|
+
apiField: 'wallet',
|
|
1067
|
+
formatter: (data) => ({ title: 'Wallet Summary',
|
|
1068
|
+
points: [{ label: 'Balance', value: na(data?.wallet?.balance) }, { label: 'Cashback', value: na(data?.wallet?.cashback) }, { label: 'Promo credit', value: na(data?.wallet?.promo_credit) }, { label: 'Last updated', value: na(data?.wallet?.updated_at) }] }) },
|
|
1069
|
+
{ key: 'cancellation_policy', type: 'hotel', label: 'Cancellation Policy', subtitle: 'Fees · Deadline', icon: 'pi pi-ban',
|
|
1070
|
+
apiField: 'cancellation_policy',
|
|
1071
|
+
formatter: (data) => ({ title: 'Cancellation Rules', subtitle: data?.cancellation_policy?.refundable ? 'Refundable' : 'Non-refundable',
|
|
1072
|
+
points: [{ label: 'Refundable', value: data?.cancellation_policy?.refundable ? 'Yes' : 'No' }, { label: 'Cancel fee', value: na(data?.cancellation_policy?.fee) }, { label: 'Deadline', value: na(data?.cancellation_policy?.deadline) }, { label: 'Policy', value: na(data?.cancellation_policy?.description) }],
|
|
1073
|
+
callout: { type: 'warning', message: data?.cancellation_policy?.warning ?? 'Always verify cancellation terms with the hotel.' } }) },
|
|
1074
|
+
{ key: 'breakfast', type: 'hotel', label: 'Meals & Breakfast', subtitle: 'Board basis', icon: 'pi pi-list',
|
|
1075
|
+
apiField: 'meals',
|
|
1076
|
+
formatter: (data) => ({ title: na(data?.meals?.board_basis), subtitle: 'Meal inclusions',
|
|
1077
|
+
points: [{ label: 'Board basis', value: na(data?.meals?.board_basis) }, { label: 'Breakfast', value: na(data?.meals?.breakfast) }, { label: 'Lunch', value: na(data?.meals?.lunch) }, { label: 'Dinner', value: na(data?.meals?.dinner) }] }) },
|
|
1078
|
+
];
|
|
1079
|
+
|
|
1080
|
+
class SbdBookingInputComponent {
|
|
1081
|
+
constructor() {
|
|
1082
|
+
this.loading = input(false);
|
|
1083
|
+
this.product = model('flight');
|
|
1084
|
+
this.bookingId = model('');
|
|
1085
|
+
this.lookup = output();
|
|
1086
|
+
this.productOptions = [
|
|
1087
|
+
{ label: '✈ Flight', value: 'flight' },
|
|
1088
|
+
];
|
|
1089
|
+
this.demoIds = [
|
|
1090
|
+
{ label: 'Demo Flight: ALM-20481', value: 'ALM-20481' },
|
|
1091
|
+
{ label: 'Demo Hotel: ALM-HTL-9921', value: 'ALM-HTL-9921' },
|
|
1092
|
+
];
|
|
1093
|
+
}
|
|
1094
|
+
fillDemo(id) {
|
|
1095
|
+
const product = id.includes('HTL') ? 'hotel' : 'flight';
|
|
1096
|
+
this.product.set(product);
|
|
1097
|
+
this.bookingId.set(id);
|
|
1098
|
+
}
|
|
1099
|
+
onKeydown(event) {
|
|
1100
|
+
if (event.key === 'Enter')
|
|
1101
|
+
this.lookup.emit();
|
|
1102
|
+
}
|
|
1103
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdBookingInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1104
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.14", type: SbdBookingInputComponent, isStandalone: true, selector: "app-sbd-booking-input", inputs: { loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, product: { classPropertyName: "product", publicName: "product", isSignal: true, isRequired: false, transformFunction: null }, bookingId: { classPropertyName: "bookingId", publicName: "bookingId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { product: "productChange", bookingId: "bookingIdChange", lookup: "lookup" }, ngImport: i0, template: "<div class=\"sbd-booking-input\">\n <p-selectButton\n [(ngModel)]=\"product\"\n [options]=\"productOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n optionDisabled=\"disabled\">\n <ng-template pTemplate=\"item\" let-item>\n <span\n [pTooltip]=\"item.disabled ? 'Coming soon' : null\"\n tooltipPosition=\"bottom\"\n style=\"pointer-events: auto; display: inline-block;\">\n {{ item.label }}\n </span>\n </ng-template>\n </p-selectButton>\n <div class=\"sbd-booking-input__row\">\n <div class=\"sbd-booking-input__field-wrap\">\n <i class=\"pi pi-search sbd-booking-input__icon\"></i>\n <input pInputText class=\"sbd-booking-input__field\" placeholder=\"Enter Booking ID\"\n [(ngModel)]=\"bookingId\" (keydown)=\"onKeydown($event)\" autocomplete=\"off\" spellcheck=\"false\" />\n </div>\n <button pButton label=\"Lookup \u2192\" [disabled]=\"!bookingId().trim()\" (click)=\"lookup.emit()\" class=\"sbd-booking-input__btn\"></button>\n </div>\n</div>\n", styles: [".sbd-booking-input{background:#fff;border-radius:12px;border:1px solid #e2e8f0;padding:20px;display:flex;flex-direction:column;gap:14px}.sbd-booking-input__row{display:flex;gap:10px;align-items:center}.sbd-booking-input__field-wrap{flex:1;position:relative;display:flex;align-items:center}.sbd-booking-input__icon{position:absolute;left:12px;color:#94a3b8;font-size:14px;pointer-events:none;z-index:1}.sbd-booking-input__field{width:100%!important;padding-left:36px!important;height:44px;font-size:14px!important}.sbd-booking-input__btn{height:44px;white-space:nowrap}.sbd-booking-input__demos{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.sbd-booking-input__demos-label{font-size:11.5px;color:#94a3b8}.sbd-booking-input__chip{padding:4px 10px;border-radius:20px;border:1px solid #e2e8f0;background:#f8fafc;color:#1e2e4a;font-size:11.5px;font-weight:500;cursor:pointer;transition:border-color .15s,background .15s}.sbd-booking-input__chip:hover{border-color:#3b82f6;background:#eff6ff;color:#3b82f6}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: SelectButtonModule }, { kind: "component", type: i2.SelectButton, selector: "p-selectButton", inputs: ["options", "optionLabel", "optionValue", "optionDisabled", "tabindex", "multiple", "style", "styleClass", "ariaLabelledBy", "disabled", "dataKey"], outputs: ["onOptionClick", "onChange"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i3.InputText, selector: "[pInputText]" }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3$1.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "fitContent", "pTooltip", "tooltipDisabled", "tooltipOptions"] }] }); }
|
|
1105
|
+
}
|
|
1106
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdBookingInputComponent, decorators: [{
|
|
1107
|
+
type: Component,
|
|
1108
|
+
args: [{ selector: 'app-sbd-booking-input', standalone: true, imports: [FormsModule, SelectButtonModule, InputTextModule, ButtonModule, TooltipModule], template: "<div class=\"sbd-booking-input\">\n <p-selectButton\n [(ngModel)]=\"product\"\n [options]=\"productOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n optionDisabled=\"disabled\">\n <ng-template pTemplate=\"item\" let-item>\n <span\n [pTooltip]=\"item.disabled ? 'Coming soon' : null\"\n tooltipPosition=\"bottom\"\n style=\"pointer-events: auto; display: inline-block;\">\n {{ item.label }}\n </span>\n </ng-template>\n </p-selectButton>\n <div class=\"sbd-booking-input__row\">\n <div class=\"sbd-booking-input__field-wrap\">\n <i class=\"pi pi-search sbd-booking-input__icon\"></i>\n <input pInputText class=\"sbd-booking-input__field\" placeholder=\"Enter Booking ID\"\n [(ngModel)]=\"bookingId\" (keydown)=\"onKeydown($event)\" autocomplete=\"off\" spellcheck=\"false\" />\n </div>\n <button pButton label=\"Lookup \u2192\" [disabled]=\"!bookingId().trim()\" (click)=\"lookup.emit()\" class=\"sbd-booking-input__btn\"></button>\n </div>\n</div>\n", styles: [".sbd-booking-input{background:#fff;border-radius:12px;border:1px solid #e2e8f0;padding:20px;display:flex;flex-direction:column;gap:14px}.sbd-booking-input__row{display:flex;gap:10px;align-items:center}.sbd-booking-input__field-wrap{flex:1;position:relative;display:flex;align-items:center}.sbd-booking-input__icon{position:absolute;left:12px;color:#94a3b8;font-size:14px;pointer-events:none;z-index:1}.sbd-booking-input__field{width:100%!important;padding-left:36px!important;height:44px;font-size:14px!important}.sbd-booking-input__btn{height:44px;white-space:nowrap}.sbd-booking-input__demos{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.sbd-booking-input__demos-label{font-size:11.5px;color:#94a3b8}.sbd-booking-input__chip{padding:4px 10px;border-radius:20px;border:1px solid #e2e8f0;background:#f8fafc;color:#1e2e4a;font-size:11.5px;font-weight:500;cursor:pointer;transition:border-color .15s,background .15s}.sbd-booking-input__chip:hover{border-color:#3b82f6;background:#eff6ff;color:#3b82f6}\n"] }]
|
|
1109
|
+
}] });
|
|
1110
|
+
|
|
1111
|
+
class SbdWorkflowActionsComponent {
|
|
1112
|
+
constructor() {
|
|
1113
|
+
this.bookingId = input.required();
|
|
1114
|
+
this.cancelBooking = output();
|
|
1115
|
+
}
|
|
1116
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdWorkflowActionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1117
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.14", type: SbdWorkflowActionsComponent, isStandalone: true, selector: "app-sbd-workflow-actions", inputs: { bookingId: { classPropertyName: "bookingId", publicName: "bookingId", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { cancelBooking: "cancelBooking" }, ngImport: i0, template: "<div class=\"sbd-workflow-actions\">\n <button pButton label=\"Cancel Booking\" icon=\"pi pi-times-circle\"\n class=\"p-button-danger p-button-outlined p-button-sm\"\n (click)=\"cancelBooking.emit()\">\n </button>\n\n <button pButton label=\"Send Ticket\" icon=\"pi pi-send\"\n class=\"p-button-outlined p-button-sm p-button-secondary\"\n [disabled]=\"true\"\n pTooltip=\"Coming soon\" tooltipPosition=\"bottom\">\n </button>\n\n <button pButton label=\"Flight Status\" icon=\"pi pi-chart-bar\"\n class=\"p-button-outlined p-button-sm p-button-secondary\"\n [disabled]=\"true\"\n pTooltip=\"Coming soon\" tooltipPosition=\"bottom\">\n </button>\n\n <button pButton label=\"Select Seat\" icon=\"pi pi-th-large\"\n class=\"p-button-outlined p-button-sm p-button-secondary\"\n [disabled]=\"true\"\n pTooltip=\"Coming soon\" tooltipPosition=\"bottom\">\n </button>\n\n <button pButton label=\"Add Baggage\" icon=\"pi pi-briefcase\"\n class=\"p-button-outlined p-button-sm p-button-secondary\"\n [disabled]=\"true\"\n pTooltip=\"Coming soon\" tooltipPosition=\"bottom\">\n </button>\n\n <button pButton label=\"Add Service\" icon=\"pi pi-plus-circle\"\n class=\"p-button-outlined p-button-sm p-button-secondary\"\n [disabled]=\"true\"\n pTooltip=\"Coming soon\" tooltipPosition=\"bottom\">\n </button>\n</div>\n", styles: [".sbd-workflow-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap}\n"], dependencies: [{ kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3$1.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "fitContent", "pTooltip", "tooltipDisabled", "tooltipOptions"] }] }); }
|
|
1118
|
+
}
|
|
1119
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdWorkflowActionsComponent, decorators: [{
|
|
1120
|
+
type: Component,
|
|
1121
|
+
args: [{ selector: 'app-sbd-workflow-actions', standalone: true, imports: [ButtonModule, TooltipModule], template: "<div class=\"sbd-workflow-actions\">\n <button pButton label=\"Cancel Booking\" icon=\"pi pi-times-circle\"\n class=\"p-button-danger p-button-outlined p-button-sm\"\n (click)=\"cancelBooking.emit()\">\n </button>\n\n <button pButton label=\"Send Ticket\" icon=\"pi pi-send\"\n class=\"p-button-outlined p-button-sm p-button-secondary\"\n [disabled]=\"true\"\n pTooltip=\"Coming soon\" tooltipPosition=\"bottom\">\n </button>\n\n <button pButton label=\"Flight Status\" icon=\"pi pi-chart-bar\"\n class=\"p-button-outlined p-button-sm p-button-secondary\"\n [disabled]=\"true\"\n pTooltip=\"Coming soon\" tooltipPosition=\"bottom\">\n </button>\n\n <button pButton label=\"Select Seat\" icon=\"pi pi-th-large\"\n class=\"p-button-outlined p-button-sm p-button-secondary\"\n [disabled]=\"true\"\n pTooltip=\"Coming soon\" tooltipPosition=\"bottom\">\n </button>\n\n <button pButton label=\"Add Baggage\" icon=\"pi pi-briefcase\"\n class=\"p-button-outlined p-button-sm p-button-secondary\"\n [disabled]=\"true\"\n pTooltip=\"Coming soon\" tooltipPosition=\"bottom\">\n </button>\n\n <button pButton label=\"Add Service\" icon=\"pi pi-plus-circle\"\n class=\"p-button-outlined p-button-sm p-button-secondary\"\n [disabled]=\"true\"\n pTooltip=\"Coming soon\" tooltipPosition=\"bottom\">\n </button>\n</div>\n", styles: [".sbd-workflow-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap}\n"] }]
|
|
1122
|
+
}] });
|
|
1123
|
+
|
|
1124
|
+
class SbdActionCardComponent {
|
|
1125
|
+
constructor() {
|
|
1126
|
+
this.action = input.required();
|
|
1127
|
+
this.active = input(false);
|
|
1128
|
+
this.loading = input(false);
|
|
1129
|
+
this.selected = output();
|
|
1130
|
+
}
|
|
1131
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdActionCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1132
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SbdActionCardComponent, isStandalone: true, selector: "app-sbd-action-card", inputs: { action: { classPropertyName: "action", publicName: "action", isSignal: true, isRequired: true, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selected" }, ngImport: i0, template: "<button class=\"sbd-action-card\" [class.sbd-action-card--active]=\"active()\"\n [class.sbd-action-card--loading]=\"loading()\" [pTooltip]=\"action().subtitle\"\n tooltipPosition=\"top\" (click)=\"selected.emit(action().key)\" type=\"button\">\n @if (active()) { <span class=\"sbd-action-card__dot\"></span> }\n <div class=\"sbd-action-card__icon-tile\">\n <i [class]=\"action().icon\"></i>\n </div>\n <div class=\"sbd-action-card__body\">\n <span class=\"sbd-action-card__label\">{{ action().label }}</span>\n <span class=\"sbd-action-card__sub\">{{ action().subtitle }}</span>\n </div>\n</button>\n", styles: [".sbd-action-card{position:relative;display:flex;flex-direction:column;align-items:flex-start;gap:10px;padding:14px;background:#fff;border:1.5px solid #e2e8f0;border-radius:10px;cursor:pointer;text-align:left;width:100%;transition:transform .15s,box-shadow .15s,border-color .15s;font-family:inherit}.sbd-action-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px #00000014;border-color:#1e2e4a}.sbd-action-card--active{border-color:#3b82f6!important;background:#eff6ff!important}.sbd-action-card--active .sbd-action-card__icon-tile{background:#3b82f61f;color:#3b82f6}.sbd-action-card--active .sbd-action-card__label{color:#3b82f6}.sbd-action-card--loading{pointer-events:none;opacity:.7}.sbd-action-card__dot{position:absolute;top:8px;right:8px;width:7px;height:7px;border-radius:50%;background:#3b82f6}.sbd-action-card__icon-tile{width:38px;height:38px;border-radius:8px;background:#f1f5f9;color:#1e2e4a;display:flex;align-items:center;justify-content:center;flex-shrink:0}.sbd-action-card__icon-tile i{font-size:18px}.sbd-action-card__body{display:flex;flex-direction:column;gap:2px;min-width:0;width:100%}.sbd-action-card__label{font-size:12.5px;font-weight:700;color:#1e2e4a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sbd-action-card__sub{font-size:11px;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "fitContent", "pTooltip", "tooltipDisabled", "tooltipOptions"] }] }); }
|
|
1133
|
+
}
|
|
1134
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdActionCardComponent, decorators: [{
|
|
1135
|
+
type: Component,
|
|
1136
|
+
args: [{ selector: 'app-sbd-action-card', standalone: true, imports: [TooltipModule], template: "<button class=\"sbd-action-card\" [class.sbd-action-card--active]=\"active()\"\n [class.sbd-action-card--loading]=\"loading()\" [pTooltip]=\"action().subtitle\"\n tooltipPosition=\"top\" (click)=\"selected.emit(action().key)\" type=\"button\">\n @if (active()) { <span class=\"sbd-action-card__dot\"></span> }\n <div class=\"sbd-action-card__icon-tile\">\n <i [class]=\"action().icon\"></i>\n </div>\n <div class=\"sbd-action-card__body\">\n <span class=\"sbd-action-card__label\">{{ action().label }}</span>\n <span class=\"sbd-action-card__sub\">{{ action().subtitle }}</span>\n </div>\n</button>\n", styles: [".sbd-action-card{position:relative;display:flex;flex-direction:column;align-items:flex-start;gap:10px;padding:14px;background:#fff;border:1.5px solid #e2e8f0;border-radius:10px;cursor:pointer;text-align:left;width:100%;transition:transform .15s,box-shadow .15s,border-color .15s;font-family:inherit}.sbd-action-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px #00000014;border-color:#1e2e4a}.sbd-action-card--active{border-color:#3b82f6!important;background:#eff6ff!important}.sbd-action-card--active .sbd-action-card__icon-tile{background:#3b82f61f;color:#3b82f6}.sbd-action-card--active .sbd-action-card__label{color:#3b82f6}.sbd-action-card--loading{pointer-events:none;opacity:.7}.sbd-action-card__dot{position:absolute;top:8px;right:8px;width:7px;height:7px;border-radius:50%;background:#3b82f6}.sbd-action-card__icon-tile{width:38px;height:38px;border-radius:8px;background:#f1f5f9;color:#1e2e4a;display:flex;align-items:center;justify-content:center;flex-shrink:0}.sbd-action-card__icon-tile i{font-size:18px}.sbd-action-card__body{display:flex;flex-direction:column;gap:2px;min-width:0;width:100%}.sbd-action-card__label{font-size:12.5px;font-weight:700;color:#1e2e4a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sbd-action-card__sub{font-size:11px;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
|
|
1137
|
+
}] });
|
|
1138
|
+
|
|
1139
|
+
class SbdActionGridComponent {
|
|
1140
|
+
constructor() {
|
|
1141
|
+
this.actions = input.required();
|
|
1142
|
+
this.activeKey = input(null);
|
|
1143
|
+
this.loadingKey = input(null);
|
|
1144
|
+
this.actionSelected = output();
|
|
1145
|
+
}
|
|
1146
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdActionGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1147
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SbdActionGridComponent, isStandalone: true, selector: "app-sbd-action-grid", inputs: { actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: true, transformFunction: null }, activeKey: { classPropertyName: "activeKey", publicName: "activeKey", isSignal: true, isRequired: false, transformFunction: null }, loadingKey: { classPropertyName: "loadingKey", publicName: "loadingKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { actionSelected: "actionSelected" }, ngImport: i0, template: "<div class=\"sbd-action-grid\">\n <div class=\"sbd-action-grid__header\">\n <h3 class=\"sbd-action-grid__title\">Booking Details</h3>\n </div>\n <div class=\"sbd-action-grid__grid\">\n @for (action of actions(); track action.key) {\n <app-sbd-action-card [action]=\"action\" [active]=\"activeKey() === action.key\"\n [loading]=\"loadingKey() === action.key\" (selected)=\"actionSelected.emit($event)\" />\n }\n </div>\n</div>\n", styles: [".sbd-action-grid{background:#fff;border-radius:12px;border:1px solid #e2e8f0;padding:20px}.sbd-action-grid__header{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.sbd-action-grid__title{font-size:13.5px;font-weight:700;color:#1e2e4a;margin:0}.sbd-action-grid__count{font-size:11.5px;color:#94a3b8;background:#f8fafc;border:1px solid #e2e8f0;border-radius:20px;padding:2px 10px}.sbd-action-grid__grid{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}@media (max-width: 1200px){.sbd-action-grid__grid{grid-template-columns:repeat(4,1fr)}}@media (max-width: 900px){.sbd-action-grid__grid{grid-template-columns:repeat(3,1fr)}}@media (max-width: 640px){.sbd-action-grid__grid{grid-template-columns:repeat(2,1fr)}}\n"], dependencies: [{ kind: "component", type: SbdActionCardComponent, selector: "app-sbd-action-card", inputs: ["action", "active", "loading"], outputs: ["selected"] }] }); }
|
|
1148
|
+
}
|
|
1149
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdActionGridComponent, decorators: [{
|
|
1150
|
+
type: Component,
|
|
1151
|
+
args: [{ selector: 'app-sbd-action-grid', standalone: true, imports: [SbdActionCardComponent], template: "<div class=\"sbd-action-grid\">\n <div class=\"sbd-action-grid__header\">\n <h3 class=\"sbd-action-grid__title\">Booking Details</h3>\n </div>\n <div class=\"sbd-action-grid__grid\">\n @for (action of actions(); track action.key) {\n <app-sbd-action-card [action]=\"action\" [active]=\"activeKey() === action.key\"\n [loading]=\"loadingKey() === action.key\" (selected)=\"actionSelected.emit($event)\" />\n }\n </div>\n</div>\n", styles: [".sbd-action-grid{background:#fff;border-radius:12px;border:1px solid #e2e8f0;padding:20px}.sbd-action-grid__header{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.sbd-action-grid__title{font-size:13.5px;font-weight:700;color:#1e2e4a;margin:0}.sbd-action-grid__count{font-size:11.5px;color:#94a3b8;background:#f8fafc;border:1px solid #e2e8f0;border-radius:20px;padding:2px 10px}.sbd-action-grid__grid{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}@media (max-width: 1200px){.sbd-action-grid__grid{grid-template-columns:repeat(4,1fr)}}@media (max-width: 900px){.sbd-action-grid__grid{grid-template-columns:repeat(3,1fr)}}@media (max-width: 640px){.sbd-action-grid__grid{grid-template-columns:repeat(2,1fr)}}\n"] }]
|
|
1152
|
+
}] });
|
|
1153
|
+
|
|
1154
|
+
const DEDUCTION_DEFS = [
|
|
1155
|
+
{ key: 'totalPenaltyPoints', label: 'Airline Cancellation Fee', badge: '' },
|
|
1156
|
+
{ key: 'handlingFee', label: 'Handling Fee', badge: '' },
|
|
1157
|
+
{ key: 'totalNonRefundableServiceCharge', label: 'Service Charge', badge: '' },
|
|
1158
|
+
{ key: 'totalAncillaryRefundAmount', label: 'Ancillary Fee', badge: '' },
|
|
1159
|
+
{ key: 'totalGiftAmount', label: 'Add on Fee', badge: '' },
|
|
1160
|
+
// `totalLoyaltyPoints` (points spent at booking time, now forfeited) is
|
|
1161
|
+
// intentionally not listed here — it's already surfaced under its own
|
|
1162
|
+
// program name (e.g. "Qitaf") in the "Total Paid" breakdown, so listing it
|
|
1163
|
+
// again here would double up the same amount under two different sections.
|
|
1164
|
+
];
|
|
1165
|
+
const REFUND_DEFS = [
|
|
1166
|
+
{ key: 'totalCardRefundAmount', label: 'Refund Amount' },
|
|
1167
|
+
{ key: 'totalCreditRefundAmount', label: 'Credit Shell Refund' },
|
|
1168
|
+
{ key: 'walletRefundAmount', label: 'Almatar Wallet' },
|
|
1169
|
+
{ key: 'totalLoyaltyPointsRefundAmount', label: 'Loyalty Points Refund' },
|
|
1170
|
+
];
|
|
1171
|
+
/** Labels for `paymentMethodCustomerUsed` gateway codes returned by the backend. */
|
|
1172
|
+
const PAYMENT_GATEWAY_LABELS = {
|
|
1173
|
+
CH: 'Card',
|
|
1174
|
+
HP: 'Hyperpay',
|
|
1175
|
+
MF: 'My Fatoora',
|
|
1176
|
+
WT: 'Wire Transfer',
|
|
1177
|
+
STC: 'STC',
|
|
1178
|
+
TM: 'Tamara',
|
|
1179
|
+
TB: 'Tabby',
|
|
1180
|
+
RAJHI: 'Al Rajhi Mokafaa',
|
|
1181
|
+
PPB: 'Customer Balance',
|
|
1182
|
+
PP: 'Postpay',
|
|
1183
|
+
TAMAM: 'Tamam',
|
|
1184
|
+
QITAF: 'Qitaf',
|
|
1185
|
+
MIS: 'MIS Pay',
|
|
1186
|
+
MATAR: 'Almatar Pay',
|
|
1187
|
+
CB: 'Company Balance',
|
|
1188
|
+
ANB: 'ANB',
|
|
1189
|
+
AMWAL: 'Amwal',
|
|
1190
|
+
WALA: 'Wala',
|
|
1191
|
+
YGG: 'YGG',
|
|
1192
|
+
};
|
|
1193
|
+
/** Gateway codes that refund to an installment plan rather than the original card. */
|
|
1194
|
+
const INSTALLMENT_GATEWAY_CODES = ['TM', 'TB'];
|
|
1195
|
+
/** Gateway codes that refund to a loyalty-program account rather than the original card. */
|
|
1196
|
+
const LOYALTY_GATEWAY_CODES = ['RAJHI', 'QITAF', 'STC', 'WALA'];
|
|
1197
|
+
/**
|
|
1198
|
+
* Internal payment-processor/gateway names, never customer-recognizable
|
|
1199
|
+
* brands — must not be surfaced verbatim (e.g. a card refund processed
|
|
1200
|
+
* through Hyperpay must read as "Card"/"Original Payment Method", never
|
|
1201
|
+
* "Hyperpay"). Callers fall back to a generic label whenever the normalized
|
|
1202
|
+
* lookup returns ''.
|
|
1203
|
+
*/
|
|
1204
|
+
const INTERNAL_GATEWAY_CODES = ['HP', 'MF', 'WT', 'PPB', 'PP', 'TAMAM'];
|
|
1205
|
+
/**
|
|
1206
|
+
* Result/confirmation-screen-only override: these gateways process card
|
|
1207
|
+
* payments through Mada — shown specifically as "Mada Card" instead of
|
|
1208
|
+
* falling back to the generic "Original Payment Method" label used
|
|
1209
|
+
* everywhere else `INTERNAL_GATEWAY_CODES` applies.
|
|
1210
|
+
*/
|
|
1211
|
+
const MADA_GATEWAY_CODES = ['HP', 'CH'];
|
|
1212
|
+
const CREDIT_SHELL_SUFFIX = 'It can be used only for future booking on the airline website.';
|
|
1213
|
+
const GIFT_HINT = 'Please note: If you cancel either leg of your round-trip booking, the add-on/gift service will be cancelled automatically.';
|
|
1214
|
+
class SbdCancelDialogComponent {
|
|
1215
|
+
/**
|
|
1216
|
+
* The return leg is matched by an explicit 'Return' direction; the onward
|
|
1217
|
+
* leg is whatever's left (some bookings don't tag the outbound leg with an
|
|
1218
|
+
* explicit 'Departure' value), falling back to the first entry.
|
|
1219
|
+
*/
|
|
1220
|
+
itineraryLeg(kind) {
|
|
1221
|
+
const legs = this.itineraryLegs();
|
|
1222
|
+
if (!legs.length)
|
|
1223
|
+
return null;
|
|
1224
|
+
const returnLeg = legs.find((l) => (l.direction ?? '').toLowerCase() === 'return') ?? null;
|
|
1225
|
+
if (kind === 'return')
|
|
1226
|
+
return returnLeg;
|
|
1227
|
+
return (legs.find((l) => (l.direction ?? '').toLowerCase() === 'departure') ??
|
|
1228
|
+
legs.find((l) => l !== returnLeg) ??
|
|
1229
|
+
legs[0] ??
|
|
1230
|
+
null);
|
|
1231
|
+
}
|
|
1232
|
+
fmtTime(dt) {
|
|
1233
|
+
if (!dt)
|
|
1234
|
+
return '—';
|
|
1235
|
+
try {
|
|
1236
|
+
return new Date(dt).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
1237
|
+
}
|
|
1238
|
+
catch {
|
|
1239
|
+
return dt;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
fmtShortDate(dt) {
|
|
1243
|
+
if (!dt)
|
|
1244
|
+
return '—';
|
|
1245
|
+
try {
|
|
1246
|
+
return new Date(dt).toLocaleDateString('en-GB', { weekday: 'short', day: '2-digit', month: 'short' });
|
|
1247
|
+
}
|
|
1248
|
+
catch {
|
|
1249
|
+
return dt;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
fmtMins(mins) {
|
|
1253
|
+
if (mins == null)
|
|
1254
|
+
return '';
|
|
1255
|
+
return `${Math.floor(mins / 60)}h ${mins % 60}m`;
|
|
1256
|
+
}
|
|
1257
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1258
|
+
applySourceOverride(base, breakdown) {
|
|
1259
|
+
const sel = this.selectedSource();
|
|
1260
|
+
if (!sel || !breakdown || !breakdown[sel])
|
|
1261
|
+
return this.reconcileSoleDestination(base);
|
|
1262
|
+
const src = breakdown[sel];
|
|
1263
|
+
const sourceRefundable = src.isRefundable !== false; // default true if not set
|
|
1264
|
+
if (!sourceRefundable) {
|
|
1265
|
+
// Source cannot process refund — zero out financial fields
|
|
1266
|
+
return this.reconcileSoleDestination({
|
|
1267
|
+
...base,
|
|
1268
|
+
isRefundable: false,
|
|
1269
|
+
totalPenaltyPoints: 0,
|
|
1270
|
+
totalSupplierPenaltyPoints: 0,
|
|
1271
|
+
totalCancellationFee: 0,
|
|
1272
|
+
totalRefundAmount: 0,
|
|
1273
|
+
totalCardRefundAmount: 0,
|
|
1274
|
+
handlingFee: 0,
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
// Prefer the source's own walletRefundAmount/totalCardRefundAmount when
|
|
1278
|
+
// the API provides them (they're already net of each other for this
|
|
1279
|
+
// source) — only fall back to deriving cardRefund when the API doesn't.
|
|
1280
|
+
const newTotal = Number(src.totalRefundAmount || 0);
|
|
1281
|
+
const walletRefund = src.walletRefundAmount !== undefined
|
|
1282
|
+
? Number(src.walletRefundAmount || 0)
|
|
1283
|
+
: Number(base.walletRefundAmount || 0);
|
|
1284
|
+
const cardRefund = src.totalCardRefundAmount !== undefined
|
|
1285
|
+
? Number(src.totalCardRefundAmount || 0)
|
|
1286
|
+
: Math.max(0, newTotal -
|
|
1287
|
+
Number(base.totalCreditRefundAmount || 0) -
|
|
1288
|
+
walletRefund -
|
|
1289
|
+
Number(base.totalLoyaltyPointsRefundAmount || 0));
|
|
1290
|
+
return this.reconcileSoleDestination({
|
|
1291
|
+
...base,
|
|
1292
|
+
isRefundable: true,
|
|
1293
|
+
totalPenaltyPoints: Number(src.totalSupplierPenaltyPoints || 0),
|
|
1294
|
+
totalSupplierPenaltyPoints: Number(src.totalSupplierPenaltyPoints || 0),
|
|
1295
|
+
totalCancellationFee: Number(src.totalCancellationFee || 0),
|
|
1296
|
+
totalRefundAmount: newTotal,
|
|
1297
|
+
walletRefundAmount: walletRefund,
|
|
1298
|
+
totalCardRefundAmount: cardRefund,
|
|
1299
|
+
handlingFee: Number(src.handlingFee || 0),
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* When exactly one of the four refund-destination fields (wallet / card /
|
|
1304
|
+
* credit / loyalty) is non-zero, the backend has occasionally sent that
|
|
1305
|
+
* field as a pre-deduction value that disagrees with the declared total
|
|
1306
|
+
* (e.g. wallet = total + cancellation fee) — there's no ambiguity about
|
|
1307
|
+
* *where* the money goes, only a stale number in that one field, so trust
|
|
1308
|
+
* the declared total and correct the sole destination to match rather
|
|
1309
|
+
* than surfacing a false discrepancy.
|
|
1310
|
+
*/
|
|
1311
|
+
reconcileSoleDestination(d) {
|
|
1312
|
+
if (!d?.isRefundable)
|
|
1313
|
+
return d;
|
|
1314
|
+
const wallet = Number(d.walletRefundAmount ?? 0);
|
|
1315
|
+
const card = Number(d.totalCardRefundAmount ?? 0);
|
|
1316
|
+
const credit = Number(d.totalCreditRefundAmount ?? 0);
|
|
1317
|
+
const loyalty = Number(d.totalLoyaltyPointsRefundAmount ?? 0);
|
|
1318
|
+
// A forfeited-points destination (e.g. Qitaf points funded separately
|
|
1319
|
+
// from the card refund) also counts as a real destination here — without
|
|
1320
|
+
// this, a legitimately split card+loyalty-points refund (e.g. 918 card +
|
|
1321
|
+
// 50 Qitaf points = 968 total) looks like a single stale "card" field to
|
|
1322
|
+
// this heuristic, which then force-overwrites the correct 918 with the
|
|
1323
|
+
// full 968 total and silently erases the split.
|
|
1324
|
+
const forfeitedLoyalty = this.forfeitedLoyaltyPointsFallback(d);
|
|
1325
|
+
const nonZero = [wallet, card, credit, loyalty, forfeitedLoyalty].filter((v) => v > 0);
|
|
1326
|
+
if (nonZero.length !== 1)
|
|
1327
|
+
return d;
|
|
1328
|
+
const total = Number(d.totalRefundAmountBeforeCeil ?? d.totalRefundAmount ?? 0);
|
|
1329
|
+
if (wallet > 0 && Math.abs(wallet - total) >= 0.01)
|
|
1330
|
+
return { ...d, walletRefundAmount: total };
|
|
1331
|
+
if (card > 0 && Math.abs(card - total) >= 0.01)
|
|
1332
|
+
return { ...d, totalCardRefundAmount: total };
|
|
1333
|
+
if (credit > 0 && Math.abs(credit - total) >= 0.01)
|
|
1334
|
+
return { ...d, totalCreditRefundAmount: total };
|
|
1335
|
+
if (loyalty > 0 && Math.abs(loyalty - total) >= 0.01)
|
|
1336
|
+
return { ...d, totalLoyaltyPointsRefundAmount: total };
|
|
1337
|
+
return d;
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Per-leg refund view (independent of the checkbox inclusion state) — used
|
|
1341
|
+
* to render both legs' own breakdown cards simultaneously.
|
|
1342
|
+
*/
|
|
1343
|
+
legAdjustedData(legType) {
|
|
1344
|
+
const root = this.refundData();
|
|
1345
|
+
const leg = root?.legInfo?.[legType];
|
|
1346
|
+
if (!root || !leg)
|
|
1347
|
+
return null;
|
|
1348
|
+
const base = this.buildLegData(root, leg, legType);
|
|
1349
|
+
return this.applySourceOverride(base, leg.sourceBreakdown ?? null);
|
|
1350
|
+
}
|
|
1351
|
+
/** Deduction rows for a single leg's card. */
|
|
1352
|
+
legDeductions(legType) {
|
|
1353
|
+
return this.deductionRows(this.legAdjustedData(legType));
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Refundable pool ("Fare") for a leg — that leg's own paid amount
|
|
1357
|
+
* (`totalSupplierBookingAmount`). `totalRefundAmount + totalCancellationFee`
|
|
1358
|
+
* isn't reliable: `totalCancellationFee` doesn't include every deduction
|
|
1359
|
+
* (e.g. `handlingFee`), which produced a Fare that didn't reconcile with
|
|
1360
|
+
* Total Refund + the shown deductions.
|
|
1361
|
+
*/
|
|
1362
|
+
legRefundPool(legType) {
|
|
1363
|
+
const d = this.legAdjustedData(legType);
|
|
1364
|
+
if (!d)
|
|
1365
|
+
return 0;
|
|
1366
|
+
return Number(d.totalSupplierBookingAmount || 0);
|
|
1367
|
+
}
|
|
1368
|
+
/** True once a leg's own departure time has passed. */
|
|
1369
|
+
legAlreadyFlown(legType) {
|
|
1370
|
+
const depTime = this.itineraryLeg(legType)?.depTime;
|
|
1371
|
+
if (!depTime)
|
|
1372
|
+
return false;
|
|
1373
|
+
const t = new Date(depTime).getTime();
|
|
1374
|
+
return !Number.isNaN(t) && t < Date.now();
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Toggle a leg's inclusion. A non-refundable leg can't be checked, and the
|
|
1378
|
+
* last checked leg can't be unchecked. Used on both the select-leg screen
|
|
1379
|
+
* and the round-trip leg cards.
|
|
1380
|
+
*/
|
|
1381
|
+
toggleLeg(legType) {
|
|
1382
|
+
const refundable = legType === 'onward' ? this.onwardRefundable() : this.returnRefundable();
|
|
1383
|
+
if (!refundable)
|
|
1384
|
+
return;
|
|
1385
|
+
const self = legType === 'onward' ? this.onwardChecked : this.returnChecked;
|
|
1386
|
+
const other = legType === 'onward' ? this.returnChecked : this.onwardChecked;
|
|
1387
|
+
if (self() && !other())
|
|
1388
|
+
return;
|
|
1389
|
+
const isTurningOff = self();
|
|
1390
|
+
if (isTurningOff &&
|
|
1391
|
+
this.isRoundTrip() &&
|
|
1392
|
+
!this.isCrossSeller() &&
|
|
1393
|
+
!this.legAlreadyFlown(legType)) {
|
|
1394
|
+
// Airline doesn't allow cancelling this leg on its own — block the
|
|
1395
|
+
// uncheck, keep it checked+frozen, and surface the note.
|
|
1396
|
+
this.forcedLegType.set(legType);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
this.forcedLegType.set(null);
|
|
1400
|
+
self.set(!self());
|
|
1401
|
+
// A leg may carry its own source list — re-resolve the selection.
|
|
1402
|
+
const breakdown = this.activeSourceBreakdown() ?? {};
|
|
1403
|
+
const sources = Object.keys(breakdown);
|
|
1404
|
+
if (sources.length && !breakdown[this.selectedSource()]) {
|
|
1405
|
+
this.selectedSource.set(sources[0]);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
/** Toggle a leg card's expanded/collapsed state. */
|
|
1409
|
+
toggleLegExpanded(legType) {
|
|
1410
|
+
const sig = legType === 'onward' ? this.onwardExpanded : this.returnExpanded;
|
|
1411
|
+
sig.set(!sig());
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Itemized refundable rows ("Fare" + named refundable add-ons/ancillaries)
|
|
1415
|
+
* from `refundBreakdown.refundable`. Empty when the backend hasn't sent a
|
|
1416
|
+
* breakdown yet — callers fall back to the single aggregate Fare row.
|
|
1417
|
+
*/
|
|
1418
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1419
|
+
refundableBreakdownRows(d) {
|
|
1420
|
+
const b = d?.refundBreakdown?.refundable;
|
|
1421
|
+
if (!b)
|
|
1422
|
+
return [];
|
|
1423
|
+
// Fare always shows from refundBreakdown.refundable.fare, even when 0 —
|
|
1424
|
+
// never falls back to originalAmount.
|
|
1425
|
+
const rows = [{ label: 'Fare', value: Number(b.fare ?? 0) }];
|
|
1426
|
+
[...(b.addOns ?? []), ...(b.ancillaries ?? [])]
|
|
1427
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1428
|
+
.filter((item) => Number(item.amount) > 0)
|
|
1429
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1430
|
+
.forEach((item) => rows.push({ label: item.name, value: Number(item.amount) }));
|
|
1431
|
+
return rows;
|
|
1432
|
+
}
|
|
1433
|
+
/** Named refundable add-ons/ancillaries only (excludes Fare) — drives the "processed separately" note. */
|
|
1434
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1435
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1436
|
+
refundableAddonItems(d) {
|
|
1437
|
+
// Only `addOns` need manual Customer Service processing — `ancillaries`
|
|
1438
|
+
// (baggage, seat, auto check-in, …) are already itemized/refunded
|
|
1439
|
+
// automatically as part of the total, no separate note needed for them.
|
|
1440
|
+
const b = d?.refundBreakdown?.refundable;
|
|
1441
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1442
|
+
return (b?.addOns ?? []).filter((item) => Number(item.amount) > 0);
|
|
1443
|
+
}
|
|
1444
|
+
/** Whether to show the "add-on refund processed separately by Customer Service" note. */
|
|
1445
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1446
|
+
hasRefundableAddonNote(d) {
|
|
1447
|
+
return this.refundableAddonItems(d).length > 0;
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Deduction rows for the "Not Refunded" section. When an itemized
|
|
1451
|
+
* breakdown is present, its named add-ons/ancillaries replace the
|
|
1452
|
+
* aggregate `totalAncillaryRefundAmount`/`totalGiftAmount` rows (same
|
|
1453
|
+
* money, named instead of lumped); the other generic rows (cancellation
|
|
1454
|
+
* fee, handling fee, service charge) are unaffected.
|
|
1455
|
+
*/
|
|
1456
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1457
|
+
deductionRows(d) {
|
|
1458
|
+
if (!d)
|
|
1459
|
+
return [];
|
|
1460
|
+
const b = d.refundBreakdown?.nonRefundable;
|
|
1461
|
+
if (!b) {
|
|
1462
|
+
return DEDUCTION_DEFS
|
|
1463
|
+
.filter(def => Number(d[def.key]) > 0)
|
|
1464
|
+
.map(def => ({ label: def.label, value: Number(d[def.key]), badge: def.badge }));
|
|
1465
|
+
}
|
|
1466
|
+
const itemRows = [...(b.addOns ?? []), ...(b.ancillaries ?? [])]
|
|
1467
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1468
|
+
.filter((item) => Number(item.amount) > 0)
|
|
1469
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1470
|
+
.map((item) => ({ label: item.name, value: Number(item.amount), badge: '' }));
|
|
1471
|
+
const aggregateRows = DEDUCTION_DEFS
|
|
1472
|
+
.filter(def => def.key !== 'totalAncillaryRefundAmount' && def.key !== 'totalGiftAmount')
|
|
1473
|
+
.filter(def => Number(d[def.key]) > 0)
|
|
1474
|
+
.map(def => ({ label: def.label, value: Number(d[def.key]), badge: def.badge }));
|
|
1475
|
+
return [...itemRows, ...aggregateRows];
|
|
1476
|
+
}
|
|
1477
|
+
// Resolve the PNR for the currently selected leg from the booking's pnr_info list.
|
|
1478
|
+
resolvePnr() {
|
|
1479
|
+
const list = this.pnrInfo() ?? [];
|
|
1480
|
+
if (!list.length)
|
|
1481
|
+
return '';
|
|
1482
|
+
const sel = this.selectedLeg();
|
|
1483
|
+
const byDir = (dir) => list.find(p => p.LegDirectionality === dir)?.pnr;
|
|
1484
|
+
if (sel === 'return')
|
|
1485
|
+
return byDir('Return') ?? list[0]?.pnr ?? '';
|
|
1486
|
+
if (sel === 'onward')
|
|
1487
|
+
return byDir('Departure') ?? list[0]?.pnr ?? '';
|
|
1488
|
+
return byDir('Departure') ?? list[0]?.pnr ?? '';
|
|
1489
|
+
}
|
|
1490
|
+
/** Rows that land in the wallet — plain wallet, wallet+loyalty combined, or loyalty-only — share the wallet icon/layout on the result screen. */
|
|
1491
|
+
isWalletDestinationRow(r) {
|
|
1492
|
+
return r.key === 'walletRefundAmount' || r.key === 'totalLoyaltyPoints';
|
|
1493
|
+
}
|
|
1494
|
+
fmtResult(value) {
|
|
1495
|
+
const cur = this.cancelResult()?.currency ?? this.refundData()?.currency ?? 'SAR';
|
|
1496
|
+
return `${Number(value || 0).toLocaleString('en-SA', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${cur}`;
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* `totalLoyaltyPoints` is normally the forfeited-at-booking points count
|
|
1500
|
+
* (no refund destination of its own). But when the backend never
|
|
1501
|
+
* populates `totalLoyaltyPointsRefundAmount` for a `loyaltyPaymentType`
|
|
1502
|
+
* booking, this is the only signal that a loyalty program was involved,
|
|
1503
|
+
* so it doubles as a split-refund row (alongside Card/Wallet) rather than
|
|
1504
|
+
* disappearing from the refund breakdown entirely. Always shown as its
|
|
1505
|
+
* own row under the program name (e.g. "Wala") — never merged into the
|
|
1506
|
+
* generic Wallet row, even when `walletRefundAmount` is also non-zero.
|
|
1507
|
+
*/
|
|
1508
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1509
|
+
forfeitedLoyaltyPointsFallback(d) {
|
|
1510
|
+
if (!d?.loyaltyPaymentType)
|
|
1511
|
+
return 0;
|
|
1512
|
+
if (Number(d.totalLoyaltyPointsRefundAmount ?? 0) > 0)
|
|
1513
|
+
return 0;
|
|
1514
|
+
return Number(d.totalLoyaltyPoints ?? 0);
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Some `/confirm` responses for a single-leg (non-cross-seller) booking
|
|
1518
|
+
* put `walletRefundAmount`/`totalLoyaltyPoints` on the wrong side at the
|
|
1519
|
+
* root — e.g. root reads `walletRefundAmount: 40, totalLoyaltyPoints: 0`
|
|
1520
|
+
* while the single leg (which should mirror root exactly here) reads
|
|
1521
|
+
* `walletRefundAmount: 0, totalLoyaltyPoints: 40`. Detected narrowly: root
|
|
1522
|
+
* shows no forfeited loyalty despite `loyaltyPaymentType` being set, the
|
|
1523
|
+
* root wallet amount exactly equals the leg's loyalty points, and the
|
|
1524
|
+
* leg's own wallet amount disagrees — in that specific case the leg's
|
|
1525
|
+
* values are trusted instead, since with only one leg they're unambiguous.
|
|
1526
|
+
*
|
|
1527
|
+
* A second, more common shape: root doesn't swap at all, it folds the
|
|
1528
|
+
* points straight into `walletRefundAmount` as one opaque lump sum (e.g.
|
|
1529
|
+
* root wallet 340 = the leg's own cash wallet 300 + the leg's forfeited
|
|
1530
|
+
* loyalty points 40). Split that back into the two parts the leg already
|
|
1531
|
+
* tells us about too, so the result screen can show a combined wallet card
|
|
1532
|
+
* with the program named instead of one unexplained total.
|
|
1533
|
+
*/
|
|
1534
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1535
|
+
correctedConfirmWalletAndLoyalty(d) {
|
|
1536
|
+
const rootWallet = Number(d.walletRefundAmount ?? 0);
|
|
1537
|
+
const forfeitedFromRoot = this.forfeitedLoyaltyPointsFallback(d);
|
|
1538
|
+
if (forfeitedFromRoot > 0 || !d.loyaltyPaymentType) {
|
|
1539
|
+
return { wallet: rootWallet, forfeitedLoyalty: forfeitedFromRoot };
|
|
1540
|
+
}
|
|
1541
|
+
const onward = d.legInfo?.onward;
|
|
1542
|
+
const ret = d.legInfo?.return;
|
|
1543
|
+
const leg = onward && !ret ? onward : ret && !onward ? ret : null;
|
|
1544
|
+
if (!leg)
|
|
1545
|
+
return { wallet: rootWallet, forfeitedLoyalty: 0 };
|
|
1546
|
+
const legLoyalty = Number(leg.totalLoyaltyPoints ?? 0);
|
|
1547
|
+
const legWallet = Number(leg.walletRefundAmount ?? 0);
|
|
1548
|
+
// Full swap: root reads the leg's loyalty amount where its wallet should be.
|
|
1549
|
+
if (legLoyalty > 0 &&
|
|
1550
|
+
rootWallet === legLoyalty &&
|
|
1551
|
+
legWallet !== rootWallet &&
|
|
1552
|
+
Number(d.totalLoyaltyPointsRefundAmount ?? 0) === 0) {
|
|
1553
|
+
return { wallet: legWallet, forfeitedLoyalty: legLoyalty };
|
|
1554
|
+
}
|
|
1555
|
+
// Lump sum: root wallet = leg's cash wallet + leg's forfeited loyalty.
|
|
1556
|
+
if (legLoyalty > 0 &&
|
|
1557
|
+
legWallet > 0 &&
|
|
1558
|
+
Math.abs(rootWallet - (legWallet + legLoyalty)) < 1 &&
|
|
1559
|
+
Number(d.totalLoyaltyPointsRefundAmount ?? 0) === 0) {
|
|
1560
|
+
return { wallet: legWallet, forfeitedLoyalty: legLoyalty };
|
|
1561
|
+
}
|
|
1562
|
+
// Newer `/confirm` responses drop the loyalty portion from both root and
|
|
1563
|
+
// leg totals — `walletRefundAmount` arrives as one opaque lump that's the
|
|
1564
|
+
// cash-wallet portion (`totalWalletAmount`) plus a loyalty-program amount.
|
|
1565
|
+
// When `loyaltyPaymentType` is set, the lump exceeds the cash-wallet
|
|
1566
|
+
// portion, and it's a genuine card+wallet split (not a "send everything to
|
|
1567
|
+
// the wallet" refund), the difference is that loyalty amount.
|
|
1568
|
+
const cashWallet = Number(d.totalWalletAmount ?? 0);
|
|
1569
|
+
const impliedLoyalty = rootWallet - cashWallet;
|
|
1570
|
+
if (impliedLoyalty > 0.01 &&
|
|
1571
|
+
cashWallet > 0 &&
|
|
1572
|
+
Number(d.totalCardRefundAmount ?? 0) > 0 &&
|
|
1573
|
+
Number(d.totalLoyaltyPointsRefundAmount ?? 0) === 0) {
|
|
1574
|
+
return { wallet: cashWallet, forfeitedLoyalty: impliedLoyalty };
|
|
1575
|
+
}
|
|
1576
|
+
// The whole wallet refund is the loyalty-program balance — no cash-wallet
|
|
1577
|
+
// portion (`totalWalletAmount` 0) and no card portion, but
|
|
1578
|
+
// `loyaltyPaymentType` is set. Attribute all of it to the program so the
|
|
1579
|
+
// popup names it (e.g. "Wala") with the permanent-points note.
|
|
1580
|
+
if (cashWallet === 0 &&
|
|
1581
|
+
rootWallet > 0 &&
|
|
1582
|
+
Number(d.totalCardRefundAmount ?? 0) === 0 &&
|
|
1583
|
+
Number(d.totalLoyaltyPointsRefundAmount ?? 0) === 0) {
|
|
1584
|
+
return { wallet: 0, forfeitedLoyalty: rootWallet };
|
|
1585
|
+
}
|
|
1586
|
+
return { wallet: rootWallet, forfeitedLoyalty: 0 };
|
|
1587
|
+
}
|
|
1588
|
+
togglePaidSummary() {
|
|
1589
|
+
this.paidSummaryExpanded.set(!this.paidSummaryExpanded());
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* The backend has sent `paymentMethodCustomerUsed` as both a short code
|
|
1593
|
+
* (`"CH"`) and a full gateway name (`"Card"`) at different times — match
|
|
1594
|
+
* either, case-insensitively, against the known codes/labels rather than
|
|
1595
|
+
* assuming one format.
|
|
1596
|
+
*/
|
|
1597
|
+
normalizeGatewayCode(raw) {
|
|
1598
|
+
if (!raw)
|
|
1599
|
+
return '';
|
|
1600
|
+
const needle = raw.trim().toLowerCase();
|
|
1601
|
+
const codes = Object.keys(PAYMENT_GATEWAY_LABELS);
|
|
1602
|
+
const byCode = codes.find((code) => code.toLowerCase() === needle);
|
|
1603
|
+
if (byCode)
|
|
1604
|
+
return byCode;
|
|
1605
|
+
return codes.find((code) => PAYMENT_GATEWAY_LABELS[code].toLowerCase() === needle) ?? '';
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Wallet-row label for a loyalty-program redemption — `loyaltyPaymentType`
|
|
1609
|
+
* (e.g. Wala, Qitaf) names the program that redeemed into the wallet
|
|
1610
|
+
* balance and applies regardless of `paymentMethodCustomerUsed`; the
|
|
1611
|
+
* backend can report an unrelated original payment gateway (e.g. Tabby)
|
|
1612
|
+
* alongside a wallet portion that's actually a loyalty redemption.
|
|
1613
|
+
*/
|
|
1614
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1615
|
+
walletOrLoyaltyLabel(d) {
|
|
1616
|
+
const loyalty = d?.loyaltyPaymentType ?? '';
|
|
1617
|
+
return PAYMENT_GATEWAY_LABELS[loyalty] ?? '';
|
|
1618
|
+
}
|
|
1619
|
+
constructor() {
|
|
1620
|
+
this.bookingService = inject(SbdBookingService);
|
|
1621
|
+
this.toast = inject(SbdToastService);
|
|
1622
|
+
this.visible = input(false);
|
|
1623
|
+
this.bookingId = input('');
|
|
1624
|
+
this.product = input('flight');
|
|
1625
|
+
this.pnrInfo = input([]);
|
|
1626
|
+
/** Same `booking_info` payload the dashboard already fetches (getBooking(..., 'booking_info')) — reused here to render real route/time details on the select-leg cards, instead of a second API call. */
|
|
1627
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1628
|
+
this.bookingInfo = input(null);
|
|
1629
|
+
this.closed = output();
|
|
1630
|
+
this.confirmed = output();
|
|
1631
|
+
/** Per-leg route/time details, mirroring app-sbd-flight-itinerary-card's mapping of booking_info.leg_info. */
|
|
1632
|
+
this.itineraryLegs = computed(() => {
|
|
1633
|
+
const bi = this.bookingInfo()?.data?.booking_info;
|
|
1634
|
+
if (!bi)
|
|
1635
|
+
return [];
|
|
1636
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1637
|
+
return (bi.leg_info ?? []).map((leg, i) => ({
|
|
1638
|
+
direction: leg.LegDirectionality,
|
|
1639
|
+
airlineName: (leg.LegAirlinesNames ?? []).join(', '),
|
|
1640
|
+
cabin: leg.LegCabins?.[0]?.Name ?? '',
|
|
1641
|
+
from: leg.LegDepartureAirportCode,
|
|
1642
|
+
to: leg.LegArrivalAirportCode,
|
|
1643
|
+
depTime: leg.LegDepartureDateTime,
|
|
1644
|
+
arrTime: leg.LegArrivalDateTime,
|
|
1645
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1646
|
+
stops: (leg.Stops ?? []),
|
|
1647
|
+
totalMinutes: bi.duration?.legs?.[i]?.Segments
|
|
1648
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1649
|
+
?.reduce((sum, s) => sum + (s.SegmentElapsedTimeMinutes ?? 0), 0) ?? null,
|
|
1650
|
+
operatedBy: leg.supplierName ? [leg.supplierName] : [],
|
|
1651
|
+
}));
|
|
1652
|
+
});
|
|
1653
|
+
this.refundData = signal(null);
|
|
1654
|
+
this.cancelResult = signal(null);
|
|
1655
|
+
this.loading = signal(false);
|
|
1656
|
+
this.cancelling = signal(false);
|
|
1657
|
+
this.confirming = signal(false);
|
|
1658
|
+
/**
|
|
1659
|
+
* 'select-leg' shown once the refund preview resolves, for round-trip
|
|
1660
|
+
* bookings the backend hasn't already restricted to a single leg — mirrors
|
|
1661
|
+
* almatar-b2c's select-leg screen. Skipped straight to 'details' otherwise.
|
|
1662
|
+
*/
|
|
1663
|
+
this.step = signal('loading');
|
|
1664
|
+
/** Per-leg inclusion (checkbox) and expand/collapse state — round-trip only. */
|
|
1665
|
+
this.onwardChecked = signal(true);
|
|
1666
|
+
this.returnChecked = signal(true);
|
|
1667
|
+
this.onwardExpanded = signal(true);
|
|
1668
|
+
this.returnExpanded = signal(true);
|
|
1669
|
+
this.selectedSource = signal('');
|
|
1670
|
+
this.cancelHint = signal('');
|
|
1671
|
+
this.paidSummaryExpanded = signal(false);
|
|
1672
|
+
this.preferredRefundMethod = signal('originalPaymentMethod');
|
|
1673
|
+
this.CANCEL_HINTS = [
|
|
1674
|
+
'Loading your data...',
|
|
1675
|
+
'Fetching Sabre data...',
|
|
1676
|
+
'Fetching ATC data...',
|
|
1677
|
+
'Calculating refund amount...',
|
|
1678
|
+
'We are almost done, please wait...',
|
|
1679
|
+
];
|
|
1680
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1681
|
+
this.hintTimer = null;
|
|
1682
|
+
this.hasOnward = computed(() => !!this.refundData()?.legInfo?.onward);
|
|
1683
|
+
this.hasReturn = computed(() => !!this.refundData()?.legInfo?.return);
|
|
1684
|
+
this.isRoundTrip = computed(() => this.hasOnward() && this.hasReturn());
|
|
1685
|
+
/** Which leg(s) the backend allows cancelling, from the refund preview response. */
|
|
1686
|
+
this.legDirectionality = computed(() => this.refundData()?.legDirectionality);
|
|
1687
|
+
/**
|
|
1688
|
+
* Whether to render the round-trip "two leg cards" UI. Only when the
|
|
1689
|
+
* backend explicitly allows cancelling both legs — otherwise (ONWARD-only /
|
|
1690
|
+
* RETURN-only) show a single card for that leg, same as a one-way booking.
|
|
1691
|
+
*/
|
|
1692
|
+
this.showRoundTripLegCards = computed(() => this.isRoundTrip() && (this.legDirectionality() ?? 'BOTH') === 'BOTH');
|
|
1693
|
+
/** Derived from the per-leg checkboxes — 'both' unless exactly one is unchecked. */
|
|
1694
|
+
this.selectedLeg = computed(() => {
|
|
1695
|
+
if (!this.isRoundTrip())
|
|
1696
|
+
return 'both';
|
|
1697
|
+
const dir = this.legDirectionality();
|
|
1698
|
+
if (dir === 'ONWARD')
|
|
1699
|
+
return 'onward';
|
|
1700
|
+
if (dir === 'RETURN')
|
|
1701
|
+
return 'return';
|
|
1702
|
+
const onward = this.onwardChecked();
|
|
1703
|
+
const ret = this.returnChecked();
|
|
1704
|
+
if (onward && ret)
|
|
1705
|
+
return 'both';
|
|
1706
|
+
return onward ? 'onward' : 'return';
|
|
1707
|
+
});
|
|
1708
|
+
// Returns the sourceBreakdown for the currently selected leg (or root for "both")
|
|
1709
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1710
|
+
this.activeSourceBreakdown = computed(() => {
|
|
1711
|
+
const sel = this.selectedLeg();
|
|
1712
|
+
const root = this.refundData();
|
|
1713
|
+
if (!root)
|
|
1714
|
+
return null;
|
|
1715
|
+
// A leg-scoped view (single leg, or both legs summed) must not fall back
|
|
1716
|
+
// to root.sourceBreakdown — its totals are for the whole/original
|
|
1717
|
+
// itinerary, not scoped to the leg(s) actually shown, and applying it
|
|
1718
|
+
// would clobber the correctly leg-scoped totals computed in
|
|
1719
|
+
// `currentData()`.
|
|
1720
|
+
if (sel === 'onward')
|
|
1721
|
+
return root.legInfo?.onward?.sourceBreakdown ?? null;
|
|
1722
|
+
if (sel === 'return')
|
|
1723
|
+
return root.legInfo?.return?.sourceBreakdown ?? null;
|
|
1724
|
+
if (root.legInfo?.onward && root.legInfo?.return)
|
|
1725
|
+
return null;
|
|
1726
|
+
return root.sourceBreakdown ?? null;
|
|
1727
|
+
});
|
|
1728
|
+
this.availableSources = computed(() => Object.keys(this.activeSourceBreakdown() ?? {}));
|
|
1729
|
+
this.currentData = computed(() => {
|
|
1730
|
+
const root = this.refundData();
|
|
1731
|
+
if (!root)
|
|
1732
|
+
return null;
|
|
1733
|
+
const sel = this.selectedLeg();
|
|
1734
|
+
if (sel === 'onward' && root.legInfo?.onward)
|
|
1735
|
+
return this.buildLegData(root, root.legInfo.onward, 'onward');
|
|
1736
|
+
if (sel === 'return' && root.legInfo?.return)
|
|
1737
|
+
return this.buildLegData(root, root.legInfo.return, 'return');
|
|
1738
|
+
// Payment/refund figures come from `legInfo`, never root (root isn't
|
|
1739
|
+
// reliably scoped to whichever leg(s) ended up selected) — combine both
|
|
1740
|
+
// legs when both are in play, otherwise use whichever one exists.
|
|
1741
|
+
const onward = root.legInfo?.onward;
|
|
1742
|
+
const ret = root.legInfo?.return;
|
|
1743
|
+
if (onward && ret)
|
|
1744
|
+
return this.buildCombinedLegData(root, onward, ret);
|
|
1745
|
+
if (onward && !ret)
|
|
1746
|
+
return this.buildLegData(root, onward, 'onward');
|
|
1747
|
+
if (ret && !onward)
|
|
1748
|
+
return this.buildLegData(root, ret, 'return');
|
|
1749
|
+
return root;
|
|
1750
|
+
});
|
|
1751
|
+
// currentData with source-specific penalty/refund/refundability overrides applied
|
|
1752
|
+
this.adjustedData = computed(() => {
|
|
1753
|
+
const base = this.currentData();
|
|
1754
|
+
if (!base)
|
|
1755
|
+
return null;
|
|
1756
|
+
return this.applySourceOverride(base, this.activeSourceBreakdown());
|
|
1757
|
+
});
|
|
1758
|
+
/**
|
|
1759
|
+
* Non-CrossSeller airlines require the whole round-trip/multicity
|
|
1760
|
+
* itinerary to be cancelled together — a leg can't be excluded on its own
|
|
1761
|
+
* unless it has already flown (which naturally degrades to a one-way
|
|
1762
|
+
* cancellation of just the remaining leg). Set when an uncheck attempt was
|
|
1763
|
+
* blocked for this reason, so the leg's checkbox can be frozen (locked)
|
|
1764
|
+
* and the note shown.
|
|
1765
|
+
*/
|
|
1766
|
+
this.forcedLegType = signal(null);
|
|
1767
|
+
this.sourceNonRefundableHint = computed(() => {
|
|
1768
|
+
const sel = this.selectedSource();
|
|
1769
|
+
if (!sel)
|
|
1770
|
+
return '';
|
|
1771
|
+
const src = (this.activeSourceBreakdown() ?? {})[sel];
|
|
1772
|
+
if (!src || src.isRefundable !== false)
|
|
1773
|
+
return '';
|
|
1774
|
+
return `Cancellation via ${sel} is not eligible for a refund. Please select a different source.`;
|
|
1775
|
+
});
|
|
1776
|
+
this.deductions = computed(() => this.deductionRows(this.adjustedData()));
|
|
1777
|
+
this.refunds = computed(() => {
|
|
1778
|
+
const d = this.adjustedData();
|
|
1779
|
+
if (!d)
|
|
1780
|
+
return [];
|
|
1781
|
+
const isLoyalty = this.isLoyaltyMethod();
|
|
1782
|
+
const paymentLabel = this.customerPaymentLabel();
|
|
1783
|
+
const rows = REFUND_DEFS
|
|
1784
|
+
.filter(def => Number(d[def.key]) > 0)
|
|
1785
|
+
.map(def => ({
|
|
1786
|
+
// Wallet row always reads "Almatar Wallet" (def.label) — the loyalty
|
|
1787
|
+
// program that funded it gets its own dedicated row below via
|
|
1788
|
+
// `forfeitedLoyaltyPointsFallback`, so the two stay visually
|
|
1789
|
+
// distinct instead of merging into one label.
|
|
1790
|
+
// Card row: card/loyalty stay distinct — don't relabel the card
|
|
1791
|
+
// portion with the loyalty program name too, and never surface an
|
|
1792
|
+
// internal processor name (empty `paymentLabel` falls back to the
|
|
1793
|
+
// generic def.label, e.g. "Refund Amount").
|
|
1794
|
+
label: def.key === 'totalCardRefundAmount' && !isLoyalty && paymentLabel
|
|
1795
|
+
? `${def.label} ${paymentLabel}`
|
|
1796
|
+
: def.label,
|
|
1797
|
+
value: Number(d[def.key]),
|
|
1798
|
+
}));
|
|
1799
|
+
const forfeitedLoyalty = this.forfeitedLoyaltyPointsFallback(d);
|
|
1800
|
+
if (forfeitedLoyalty > 0) {
|
|
1801
|
+
rows.push({ label: this.walletOrLoyaltyLabel(d), value: forfeitedLoyalty });
|
|
1802
|
+
}
|
|
1803
|
+
return rows;
|
|
1804
|
+
});
|
|
1805
|
+
this.creditShellNote = computed(() => {
|
|
1806
|
+
// Golden Guarantee overrides credit-shell messaging entirely — the
|
|
1807
|
+
// dedicated success notice (`goldenGuaranteeNote`) is shown instead.
|
|
1808
|
+
if (this.hasGoldenGuaranteeGift())
|
|
1809
|
+
return '';
|
|
1810
|
+
const d = this.currentData();
|
|
1811
|
+
if (!d)
|
|
1812
|
+
return '';
|
|
1813
|
+
const legInfo = d.legInfo ?? {};
|
|
1814
|
+
const hasO = !!legInfo.onward;
|
|
1815
|
+
const hasR = !!legInfo.return;
|
|
1816
|
+
const oShell = legInfo.onward?.isCreditShell === true;
|
|
1817
|
+
const rShell = legInfo.return?.isCreditShell === true;
|
|
1818
|
+
const isShell = (hasO && hasR && oShell && rShell) ||
|
|
1819
|
+
(hasO && !hasR && oShell) ||
|
|
1820
|
+
(!hasO && hasR && rShell) ||
|
|
1821
|
+
(hasO && hasR && (oShell || rShell));
|
|
1822
|
+
if (!isShell)
|
|
1823
|
+
return '';
|
|
1824
|
+
const amount = this.fmt(Number(this.adjustedData()?.totalRefundAmount ?? d.totalRefundAmount ?? 0));
|
|
1825
|
+
const pnr = this.resolvePnr();
|
|
1826
|
+
const pnrText = pnr ? `the same PNR (${pnr})` : 'the same PNR';
|
|
1827
|
+
return `The refund of ${amount} will be issued as airline credit under ${pnrText}. ${CREDIT_SHELL_SUFFIX}`;
|
|
1828
|
+
});
|
|
1829
|
+
/**
|
|
1830
|
+
* Booking includes a Golden Guarantee gift ("Cancel for any reason") —
|
|
1831
|
+
* overrides all credit-shell handling below, since a Golden Guarantee
|
|
1832
|
+
* cancellation always refunds 100% through the standard payment method
|
|
1833
|
+
* rather than the airline credit shell. May be carried on either the
|
|
1834
|
+
* preview (`refundData`) or the confirm (`cancelResult`) response — both
|
|
1835
|
+
* share the same shape, and either one may set it independently of the
|
|
1836
|
+
* other.
|
|
1837
|
+
*/
|
|
1838
|
+
this.hasGoldenGuaranteeGift = computed(() => this.refundData()?.hasGoldenGuaranteeGift === true ||
|
|
1839
|
+
this.cancelResult()?.hasGoldenGuaranteeGift === true);
|
|
1840
|
+
// Whether the refund is a credit-shell refund (whole booking or either leg).
|
|
1841
|
+
// Such refunds can only return to the airline credit shell, so the
|
|
1842
|
+
// refund-method picker is replaced with a single read-only credit notice.
|
|
1843
|
+
// A Golden Guarantee gift overrides this to `false` — that portion needs an
|
|
1844
|
+
// actual destination choice, so the booking falls back to the standard
|
|
1845
|
+
// refund-method flow despite being flagged credit-shell.
|
|
1846
|
+
this.isCreditShell = computed(() => {
|
|
1847
|
+
if (this.hasGoldenGuaranteeGift())
|
|
1848
|
+
return false;
|
|
1849
|
+
const d = this.refundData();
|
|
1850
|
+
return (d?.isCreditShell === true ||
|
|
1851
|
+
d?.legInfo?.onward?.isCreditShell === true ||
|
|
1852
|
+
d?.legInfo?.return?.isCreditShell === true);
|
|
1853
|
+
});
|
|
1854
|
+
/**
|
|
1855
|
+
* Refundable ancillaries (baggage, seats, …) that carry their own
|
|
1856
|
+
* `isCreditShell` flag, independent of the booking-level flag — rendered
|
|
1857
|
+
* as individual cards on the credit-shell refund-method screen so each
|
|
1858
|
+
* one can show either the credit-shell card style or the normal wallet
|
|
1859
|
+
* card style (both include the same nested credit-shell note when true).
|
|
1860
|
+
*/
|
|
1861
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1862
|
+
this.creditShellAncillaryItems = computed(() => (this.adjustedData()?.refundBreakdown?.refundable?.ancillaries ?? [])
|
|
1863
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1864
|
+
.filter((item) => Number(item.amount) > 0));
|
|
1865
|
+
// Airline that issues the credit shell, for the "<supplier> website credit" card.
|
|
1866
|
+
this.creditShellSupplierName = computed(() => {
|
|
1867
|
+
const d = this.refundData();
|
|
1868
|
+
return (d?.legInfo?.onward?.supplierName ??
|
|
1869
|
+
d?.legInfo?.return?.supplierName ??
|
|
1870
|
+
d?.supplierName ??
|
|
1871
|
+
d?.appliedSource ??
|
|
1872
|
+
'');
|
|
1873
|
+
});
|
|
1874
|
+
// Header title reflecting the refund type for the active selection.
|
|
1875
|
+
this.refundTypeLabel = computed(() => {
|
|
1876
|
+
if (!this.refundData()?.isRefundable)
|
|
1877
|
+
return 'Refund Details';
|
|
1878
|
+
return this.creditShellNote() ? 'Credit Shell Refund' : 'Refund Details';
|
|
1879
|
+
});
|
|
1880
|
+
this.currency = computed(() => this.refundData()?.currency ?? 'SAR');
|
|
1881
|
+
// Self-consistency check between the paid amount, deductions and the system refund.
|
|
1882
|
+
// Surfaced to the agent so mismatched/over-deducted refunds can be caught before confirming.
|
|
1883
|
+
this.discrepancyNote = computed(() => {
|
|
1884
|
+
const d = this.adjustedData();
|
|
1885
|
+
if (!d || !d.isRefundable)
|
|
1886
|
+
return '';
|
|
1887
|
+
const paid = Number(d.originalAmount || 0);
|
|
1888
|
+
const deduction = Number(d.totalCancellationFee || 0);
|
|
1889
|
+
const refund = Number(d.totalRefundAmount || 0);
|
|
1890
|
+
const issues = [];
|
|
1891
|
+
if (deduction > paid + 0.01)
|
|
1892
|
+
issues.push(`Total deduction (${this.fmt(deduction)}) exceeds the paid amount (${this.fmt(paid)}).`);
|
|
1893
|
+
if (Math.abs((paid - deduction) - refund) > 0.01)
|
|
1894
|
+
issues.push(`Computed refund (${this.fmt(paid - deduction)}) doesn't match the system refund (${this.fmt(refund)}).`);
|
|
1895
|
+
return issues.join(' ');
|
|
1896
|
+
});
|
|
1897
|
+
/** Real per-booking CrossSeller flag from the refund preview — CrossSeller suppliers allow cancelling either leg independently. */
|
|
1898
|
+
this.isCrossSeller = computed(() => this.refundData()?.isCrossSeller === true);
|
|
1899
|
+
// Non-CrossSeller round trips are one itinerary sold as a single fare —
|
|
1900
|
+
// both legs share the same refundability, sourced from root, even when
|
|
1901
|
+
// the backend's legInfo entry for a leg is missing or inconsistent.
|
|
1902
|
+
// CrossSeller legs are independently sellable, so each leg's own field
|
|
1903
|
+
// still governs there.
|
|
1904
|
+
this.onwardRefundable = computed(() => {
|
|
1905
|
+
if (!this.hasOnward())
|
|
1906
|
+
return false;
|
|
1907
|
+
return this.isCrossSeller()
|
|
1908
|
+
? this.refundData()?.legInfo?.onward?.isRefundable === true
|
|
1909
|
+
: this.refundData()?.isRefundable === true;
|
|
1910
|
+
});
|
|
1911
|
+
this.returnRefundable = computed(() => {
|
|
1912
|
+
if (!this.hasReturn())
|
|
1913
|
+
return false;
|
|
1914
|
+
return this.isCrossSeller()
|
|
1915
|
+
? this.refundData()?.legInfo?.return?.isRefundable === true
|
|
1916
|
+
: this.refundData()?.isRefundable === true;
|
|
1917
|
+
});
|
|
1918
|
+
this.isMixed = computed(() => this.isRoundTrip() && this.onwardRefundable() !== this.returnRefundable());
|
|
1919
|
+
/** Both legs present and BOTH non-refundable — nothing can be refunded for this booking. */
|
|
1920
|
+
this.allLegsNonRefundable = computed(() => this.isRoundTrip() && !this.onwardRefundable() && !this.returnRefundable());
|
|
1921
|
+
this.mixedHint = computed(() => {
|
|
1922
|
+
// Only relevant when agent is about to cancel both legs together —
|
|
1923
|
+
// individual leg tabs already show their own non-refundable notice.
|
|
1924
|
+
if (!this.isMixed() || this.selectedLeg() !== 'both')
|
|
1925
|
+
return '';
|
|
1926
|
+
if (!this.onwardRefundable())
|
|
1927
|
+
return '⚠️ Onward leg is not eligible for refund. Please contact customer service if you want to cancel it.';
|
|
1928
|
+
if (!this.returnRefundable())
|
|
1929
|
+
return '⚠️ Return leg is not eligible for refund. Please contact customer service if you want to cancel it.';
|
|
1930
|
+
return '';
|
|
1931
|
+
});
|
|
1932
|
+
this.giftHint = computed(() => {
|
|
1933
|
+
const root = this.refundData();
|
|
1934
|
+
if (!root || !this.isRoundTrip())
|
|
1935
|
+
return '';
|
|
1936
|
+
const hasGift = Number(root.totalGiftAmount || 0) > 0 ||
|
|
1937
|
+
Number(root.legInfo?.onward?.totalGiftAmount || 0) > 0 ||
|
|
1938
|
+
Number(root.legInfo?.return?.totalGiftAmount || 0) > 0;
|
|
1939
|
+
return hasGift ? GIFT_HINT : '';
|
|
1940
|
+
});
|
|
1941
|
+
this.selectionHint = computed(() => {
|
|
1942
|
+
if (!this.isRoundTrip())
|
|
1943
|
+
return '';
|
|
1944
|
+
const map = {
|
|
1945
|
+
onward: 'You are about to cancel: Onward leg only',
|
|
1946
|
+
return: 'You are about to cancel: Return leg only',
|
|
1947
|
+
both: 'You are about to cancel: Both legs',
|
|
1948
|
+
};
|
|
1949
|
+
return map[this.selectedLeg()];
|
|
1950
|
+
});
|
|
1951
|
+
this.confirmDeductions = computed(() => {
|
|
1952
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1953
|
+
const d = this.cancelResult();
|
|
1954
|
+
if (!d)
|
|
1955
|
+
return [];
|
|
1956
|
+
return DEDUCTION_DEFS.filter(def => Number(d[def.key]) > 0).map(def => ({ label: def.label, value: Number(d[def.key]) }));
|
|
1957
|
+
});
|
|
1958
|
+
this.confirmRefunds = computed(() => {
|
|
1959
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1960
|
+
const d = this.cancelResult();
|
|
1961
|
+
if (!d)
|
|
1962
|
+
return [];
|
|
1963
|
+
const code = this.normalizeGatewayCode(d.paymentMethodCustomerUsed);
|
|
1964
|
+
const isLoyalty = LOYALTY_GATEWAY_CODES.includes(code);
|
|
1965
|
+
const isMada = this.confirmIsMadaMethod();
|
|
1966
|
+
const paymentLabel = code && !INTERNAL_GATEWAY_CODES.includes(code) ? PAYMENT_GATEWAY_LABELS[code] : '';
|
|
1967
|
+
const rows = [];
|
|
1968
|
+
const { wallet, forfeitedLoyalty } = this.correctedConfirmWalletAndLoyalty(d);
|
|
1969
|
+
// Golden Guarantee overrides credit-shell handling on the result screen
|
|
1970
|
+
// too — whatever landed as a credit-shell amount folds into the normal
|
|
1971
|
+
// original-payment-method row instead of getting its own "airline
|
|
1972
|
+
// credit" card, so the confirmation reads like an ordinary Sabre/Amadeus
|
|
1973
|
+
// refund.
|
|
1974
|
+
const goldenGuaranteeOverride = this.hasGoldenGuaranteeGift();
|
|
1975
|
+
// Credit-shell row goes first — matches the design (airline credit above
|
|
1976
|
+
// the wallet row) — before the card and wallet rows below.
|
|
1977
|
+
if (!goldenGuaranteeOverride) {
|
|
1978
|
+
const credit = Number(d.totalCreditRefundAmount ?? 0);
|
|
1979
|
+
if (credit > 0) {
|
|
1980
|
+
const supplier = d.appliedSource || this.creditShellSupplierName();
|
|
1981
|
+
rows.push({
|
|
1982
|
+
key: 'totalCreditRefundAmount',
|
|
1983
|
+
label: supplier ? `${supplier} website credit` : 'Credit Shell Refund',
|
|
1984
|
+
value: credit,
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
const cardAmount = Number(d.totalCardRefundAmount ?? 0) +
|
|
1989
|
+
(goldenGuaranteeOverride ? Number(d.totalCreditRefundAmount ?? 0) : 0);
|
|
1990
|
+
if (cardAmount > 0) {
|
|
1991
|
+
rows.push({
|
|
1992
|
+
key: 'totalCardRefundAmount',
|
|
1993
|
+
label: isMada
|
|
1994
|
+
? 'Mada Card'
|
|
1995
|
+
: !isLoyalty && paymentLabel
|
|
1996
|
+
? `Refund Amount ${paymentLabel}`
|
|
1997
|
+
: 'Refund Amount',
|
|
1998
|
+
value: cardAmount,
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
if (wallet > 0 && forfeitedLoyalty > 0) {
|
|
2002
|
+
// Wallet cash + a loyalty program's points both landed in the wallet —
|
|
2003
|
+
// shown as one combined card with the two sources broken out as named
|
|
2004
|
+
// sub-rows, rather than two separate top-level cards.
|
|
2005
|
+
rows.push({
|
|
2006
|
+
key: 'walletRefundAmount',
|
|
2007
|
+
label: 'Almatar Wallet',
|
|
2008
|
+
value: wallet + forfeitedLoyalty,
|
|
2009
|
+
subRows: [
|
|
2010
|
+
{ label: 'Almatar wallet points', value: wallet },
|
|
2011
|
+
{ label: `${this.walletOrLoyaltyLabel(d)} points`, value: forfeitedLoyalty },
|
|
2012
|
+
],
|
|
2013
|
+
hasLoyaltyNote: true,
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
else if (wallet > 0) {
|
|
2017
|
+
rows.push({ key: 'walletRefundAmount', label: 'Almatar Wallet', value: wallet });
|
|
2018
|
+
}
|
|
2019
|
+
else if (forfeitedLoyalty > 0) {
|
|
2020
|
+
// Loyalty-only — no wallet cash involved, still lands as permanent
|
|
2021
|
+
// wallet points, so it keeps the wallet-style card + note.
|
|
2022
|
+
rows.push({
|
|
2023
|
+
key: 'totalLoyaltyPoints',
|
|
2024
|
+
label: this.walletOrLoyaltyLabel(d),
|
|
2025
|
+
value: forfeitedLoyalty,
|
|
2026
|
+
hasLoyaltyNote: true,
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
REFUND_DEFS
|
|
2030
|
+
.filter((def) => def.key !== 'walletRefundAmount' &&
|
|
2031
|
+
def.key !== 'totalCardRefundAmount' &&
|
|
2032
|
+
def.key !== 'totalCreditRefundAmount' &&
|
|
2033
|
+
Number(d[def.key]) > 0)
|
|
2034
|
+
.forEach((def) => {
|
|
2035
|
+
rows.push({ key: def.key, label: def.label, value: Number(d[def.key]) });
|
|
2036
|
+
});
|
|
2037
|
+
return rows;
|
|
2038
|
+
});
|
|
2039
|
+
/**
|
|
2040
|
+
* The result screen's "issued as an Airline Credit Shell" note. Golden
|
|
2041
|
+
* Guarantee overrides this to '' — the confirmation renders like a
|
|
2042
|
+
* standard Sabre/Amadeus refund instead (see `confirmRefunds`' folding
|
|
2043
|
+
* of any credit-shell amount into the normal payment-method row).
|
|
2044
|
+
*/
|
|
2045
|
+
this.confirmCreditShellNote = computed(() => {
|
|
2046
|
+
if (this.hasGoldenGuaranteeGift())
|
|
2047
|
+
return '';
|
|
2048
|
+
const d = this.cancelResult();
|
|
2049
|
+
if (!d)
|
|
2050
|
+
return '';
|
|
2051
|
+
const legInfo = d.legInfo ?? {};
|
|
2052
|
+
const hasO = !!legInfo.onward;
|
|
2053
|
+
const hasR = !!legInfo.return;
|
|
2054
|
+
const oShell = legInfo.onward?.isCreditShell === true;
|
|
2055
|
+
const rShell = legInfo.return?.isCreditShell === true;
|
|
2056
|
+
const topShell = d.isCreditShell === true;
|
|
2057
|
+
if ((hasO && hasR && oShell && rShell) || (hasO && !hasR && oShell) || (!hasO && hasR && rShell) || topShell)
|
|
2058
|
+
return 'The refund has been issued as an Airline Credit Shell and can be used for future bookings on the airline website.';
|
|
2059
|
+
if (hasO && hasR) {
|
|
2060
|
+
if (oShell)
|
|
2061
|
+
return 'The refund for the onward flight has been issued as an Airline Credit Shell.';
|
|
2062
|
+
if (rShell)
|
|
2063
|
+
return 'The refund for the return flight has been issued as an Airline Credit Shell.';
|
|
2064
|
+
}
|
|
2065
|
+
return '';
|
|
2066
|
+
});
|
|
2067
|
+
/**
|
|
2068
|
+
* The customer-facing total (ceiled) must match the pre-rounding total the
|
|
2069
|
+
* refund is actually computed from — i.e. `totalRefundAmount` must be
|
|
2070
|
+
* `totalRefundAmountBeforeCeil` rounded up to a whole currency unit (e.g.
|
|
2071
|
+
* 1454.5 → 1455). A mismatch here means the supplier response itself is
|
|
2072
|
+
* internally inconsistent and must block confirmation.
|
|
2073
|
+
*/
|
|
2074
|
+
this.refundReconciles = computed(() => {
|
|
2075
|
+
const d = this.adjustedData();
|
|
2076
|
+
if (!d || !d.isRefundable)
|
|
2077
|
+
return true;
|
|
2078
|
+
const beforeCeil = Number(d.totalRefundAmountBeforeCeil ?? d.totalRefundAmount ?? 0);
|
|
2079
|
+
const total = Number(d.totalRefundAmount ?? beforeCeil);
|
|
2080
|
+
return Math.abs(Math.ceil(beforeCeil) - total) < 1;
|
|
2081
|
+
});
|
|
2082
|
+
this.canConfirm = computed(() => {
|
|
2083
|
+
const d = this.adjustedData();
|
|
2084
|
+
return !!d?.isRefundable && this.refundReconciles() && !this.cancelling();
|
|
2085
|
+
});
|
|
2086
|
+
/** How many of {wallet, card, loyalty} the refund is actually split across. */
|
|
2087
|
+
this.splitPartsCount = computed(() => {
|
|
2088
|
+
const d = this.adjustedData();
|
|
2089
|
+
if (!d)
|
|
2090
|
+
return 0;
|
|
2091
|
+
return [
|
|
2092
|
+
d.walletRefundAmount,
|
|
2093
|
+
d.totalCardRefundAmount,
|
|
2094
|
+
d.totalLoyaltyPointsRefundAmount,
|
|
2095
|
+
this.forfeitedLoyaltyPointsFallback(d),
|
|
2096
|
+
].filter((v) => Number(v ?? 0) > 0).length;
|
|
2097
|
+
});
|
|
2098
|
+
/**
|
|
2099
|
+
* A portion of the refund is forced to the wallet (e.g. loyalty points used)
|
|
2100
|
+
* — in that case offer a "Split Refund" option instead of a pure
|
|
2101
|
+
* original-payment-method option, since the whole amount can't go to card.
|
|
2102
|
+
*/
|
|
2103
|
+
this.hasSplitRefund = computed(() => Number(this.adjustedData()?.totalRefundAmount ?? 0) > 0 && this.splitPartsCount() >= 2);
|
|
2104
|
+
/**
|
|
2105
|
+
* Wallet covers the whole refund (no card or loyalty portion at all) — show
|
|
2106
|
+
* the wallet option alone rather than a "split" with empty other sides.
|
|
2107
|
+
*/
|
|
2108
|
+
this.isWalletOnlyRefund = computed(() => {
|
|
2109
|
+
const d = this.adjustedData();
|
|
2110
|
+
return (Number(d?.walletRefundAmount ?? 0) > 0 &&
|
|
2111
|
+
Number(d?.totalCardRefundAmount ?? 0) === 0 &&
|
|
2112
|
+
Number(d?.totalLoyaltyPointsRefundAmount ?? 0) === 0 &&
|
|
2113
|
+
this.forfeitedLoyaltyPointsFallback(d) === 0);
|
|
2114
|
+
});
|
|
2115
|
+
/**
|
|
2116
|
+
* The entire refund is forfeited loyalty points (e.g. Wala) landing back as
|
|
2117
|
+
* wallet balance — no cash-wallet, card, credit or real
|
|
2118
|
+
* loyalty-points-refund portion at all. Without this the picker falls
|
|
2119
|
+
* through to the "Original Payment Method" card and mislabels the
|
|
2120
|
+
* destination as the original gateway (e.g. "Tabby"), even though
|
|
2121
|
+
* `totalCardRefundAmount` is 0 and nothing goes back to that card.
|
|
2122
|
+
*/
|
|
2123
|
+
this.isLoyaltyOnlyRefund = computed(() => {
|
|
2124
|
+
const d = this.adjustedData();
|
|
2125
|
+
return (this.forfeitedLoyaltyPointsFallback(d) > 0 &&
|
|
2126
|
+
Number(d?.walletRefundAmount ?? 0) === 0 &&
|
|
2127
|
+
Number(d?.totalCardRefundAmount ?? 0) === 0 &&
|
|
2128
|
+
Number(d?.totalCreditRefundAmount ?? 0) === 0 &&
|
|
2129
|
+
Number(d?.totalLoyaltyPointsRefundAmount ?? 0) === 0);
|
|
2130
|
+
});
|
|
2131
|
+
/** A real cash-wallet portion is part of the refund (distinct from forfeited loyalty points landing in the wallet). */
|
|
2132
|
+
this.hasCashWalletRefund = computed(() => Number(this.adjustedData()?.walletRefundAmount ?? 0) > 0);
|
|
2133
|
+
/**
|
|
2134
|
+
* Wallet card's title — normally "Almatar Wallet", but the loyalty program
|
|
2135
|
+
* name (Wala, Qitaf, …) when the whole refund is that program's forfeited
|
|
2136
|
+
* balance landing back in the wallet.
|
|
2137
|
+
*/
|
|
2138
|
+
this.walletCardLabel = computed(() => this.isLoyaltyOnlyRefund()
|
|
2139
|
+
? this.walletOrLoyaltyLabel(this.adjustedData()) || 'Almatar Wallet'
|
|
2140
|
+
: 'Almatar Wallet');
|
|
2141
|
+
/**
|
|
2142
|
+
* Part of the refund came from converting external loyalty points into
|
|
2143
|
+
* wallet funds — surfaced as a note under the wallet destination so the
|
|
2144
|
+
* agent knows those points land as permanent wallet balance, not cash.
|
|
2145
|
+
*/
|
|
2146
|
+
this.hasLoyaltyPointsRefund = computed(() => Number(this.adjustedData()?.totalLoyaltyPoints ?? 0) > 0);
|
|
2147
|
+
/**
|
|
2148
|
+
* The booking is refundable but nets to 0.00 after deductions — a
|
|
2149
|
+
* dedicated NIL Refund state, distinct from "not refundable": no refund
|
|
2150
|
+
* method to choose between, just confirmation that nothing is owed back.
|
|
2151
|
+
*/
|
|
2152
|
+
this.isNilRefund = computed(() => {
|
|
2153
|
+
const d = this.adjustedData();
|
|
2154
|
+
return !!d?.isRefundable && Number(d.totalRefundAmount ?? 0) === 0;
|
|
2155
|
+
});
|
|
2156
|
+
/** Same as `isNilRefund`, scoped to the confirmed cancellation result — drives the result screen's title. */
|
|
2157
|
+
this.confirmIsNilRefund = computed(() => {
|
|
2158
|
+
const d = this.cancelResult();
|
|
2159
|
+
return !!d?.isRefundable && Number(d.totalRefundAmount ?? 0) === 0;
|
|
2160
|
+
});
|
|
2161
|
+
/** Whether the customer may send the whole refund to their Almatar Wallet. */
|
|
2162
|
+
this.isFullWalletRefundAllowed = computed(() => !!this.adjustedData()?.isFullWalletRefundAllowed);
|
|
2163
|
+
/** Whether the customer paid via an installment gateway (Tamara, Tabby, …). */
|
|
2164
|
+
this.isInstallmentMethod = computed(() => INSTALLMENT_GATEWAY_CODES.includes(this.normalizeGatewayCode(this.adjustedData()?.paymentMethodCustomerUsed)));
|
|
2165
|
+
/** Whether the customer paid via a loyalty-program gateway (Al Rajhi Mokafaa, …). */
|
|
2166
|
+
this.isLoyaltyMethod = computed(() => LOYALTY_GATEWAY_CODES.includes(this.normalizeGatewayCode(this.adjustedData()?.paymentMethodCustomerUsed)));
|
|
2167
|
+
/** Any gateway that refunds somewhere other than a plain card — used for the wallet-style icon. */
|
|
2168
|
+
this.isSpecialGatewayMethod = computed(() => this.isInstallmentMethod() || this.isLoyaltyMethod());
|
|
2169
|
+
/**
|
|
2170
|
+
* Card-refund settlement-timeline badge — Tabby and Tamara each have their
|
|
2171
|
+
* own processing window, distinct from a plain card's generic bank policy.
|
|
2172
|
+
*/
|
|
2173
|
+
this.cardBadgeText = computed(() => {
|
|
2174
|
+
const code = this.normalizeGatewayCode(this.adjustedData()?.paymentMethodCustomerUsed);
|
|
2175
|
+
if (code === 'TB')
|
|
2176
|
+
return 'From 5-7 business days';
|
|
2177
|
+
if (code === 'TM')
|
|
2178
|
+
return 'Up to 10 business days';
|
|
2179
|
+
if (this.isLoyaltyMethod())
|
|
2180
|
+
return 'Loyalty Program Policy';
|
|
2181
|
+
return 'Up to 15 business days, bank policy';
|
|
2182
|
+
});
|
|
2183
|
+
/** Same as `cardBadgeText`, scoped to the confirmed cancellation result. */
|
|
2184
|
+
this.confirmCardBadgeText = computed(() => {
|
|
2185
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2186
|
+
const d = this.cancelResult();
|
|
2187
|
+
const code = this.normalizeGatewayCode(d?.paymentMethodCustomerUsed);
|
|
2188
|
+
if (code === 'TB')
|
|
2189
|
+
return 'From 5-7 business days';
|
|
2190
|
+
if (code === 'TM')
|
|
2191
|
+
return 'Up to 10 business days';
|
|
2192
|
+
if (LOYALTY_GATEWAY_CODES.includes(code))
|
|
2193
|
+
return 'Loyalty Program Policy';
|
|
2194
|
+
return 'Up to 15 business days, bank policy';
|
|
2195
|
+
});
|
|
2196
|
+
/**
|
|
2197
|
+
* Per-method breakdown of the "Total paid" amount. The backend doesn't
|
|
2198
|
+
* (yet) expose a dedicated original-payment breakdown, so the known
|
|
2199
|
+
* refund-destination fields (card / wallet / loyalty) are used as a proxy
|
|
2200
|
+
* for how the booking was originally paid, and whatever's left over after
|
|
2201
|
+
* those (e.g. a non-refundable leg/fee that never shows up in any refund
|
|
2202
|
+
* field) is folded into the primary payment method — so the rows always
|
|
2203
|
+
* add up to the full `originalAmount` shown above them, instead of only
|
|
2204
|
+
* covering the refundable portion.
|
|
2205
|
+
*/
|
|
2206
|
+
this.paidBreakdownRows = computed(() => {
|
|
2207
|
+
const d = this.adjustedData();
|
|
2208
|
+
if (!d)
|
|
2209
|
+
return [];
|
|
2210
|
+
// Card and loyalty stay distinct labels (same categorization as
|
|
2211
|
+
// `refunds`) so a gateway like Al Rajhi Mokafaa doesn't swallow an
|
|
2212
|
+
// actual separate card portion under one label.
|
|
2213
|
+
const cardLabel = (!this.isLoyaltyMethod() && this.customerPaymentLabel()) || 'Original Payment Method';
|
|
2214
|
+
// The wallet destination row always reads "Almatar Wallet" — unlike
|
|
2215
|
+
// `refunds`/`confirmRefunds`, this row never swaps to the loyalty
|
|
2216
|
+
// program name, since `forfeitedLoyaltyLabel` below is that program's
|
|
2217
|
+
// own dedicated row and the two must stay visually distinct here.
|
|
2218
|
+
const walletLabel = 'Almatar Wallet';
|
|
2219
|
+
const forfeitedLoyaltyLabel = this.walletOrLoyaltyLabel(d) || 'Loyalty Points';
|
|
2220
|
+
const loyaltyLabel = this.customerPaymentLabel() || 'Loyalty Points Refund';
|
|
2221
|
+
const amounts = new Map();
|
|
2222
|
+
const add = (label, value) => {
|
|
2223
|
+
if (value <= 0)
|
|
2224
|
+
return;
|
|
2225
|
+
amounts.set(label, (amounts.get(label) ?? 0) + value);
|
|
2226
|
+
};
|
|
2227
|
+
add(cardLabel, Number(d.totalCardRefundAmount ?? 0));
|
|
2228
|
+
// This row shows how much of the *original* payment went through the
|
|
2229
|
+
// wallet — `totalWalletAmount`, not `walletRefundAmount` (a refund
|
|
2230
|
+
// destination amount that can differ, e.g. only part of the wallet
|
|
2231
|
+
// payment is refundable after deductions).
|
|
2232
|
+
add(walletLabel, Number(d.totalWalletAmount ?? 0));
|
|
2233
|
+
add(loyaltyLabel, Number(d.totalLoyaltyPointsRefundAmount ?? 0));
|
|
2234
|
+
// `totalLoyaltyPoints` is the portion of the *original* payment made with
|
|
2235
|
+
// loyalty points (now forfeited/deducted, not refunded) — distinct from
|
|
2236
|
+
// `totalLoyaltyPointsRefundAmount` above and from `walletRefundAmount`
|
|
2237
|
+
// (an unrelated refund-destination amount). Always shown as its own row
|
|
2238
|
+
// under the program name (e.g. "Qitaf"/"Wala") rather than merged into
|
|
2239
|
+
// the generic Wallet row or silently folded into the leftover remainder.
|
|
2240
|
+
add(forfeitedLoyaltyLabel, Number(d.totalLoyaltyPoints ?? 0));
|
|
2241
|
+
const total = Number(d.originalAmount ?? 0);
|
|
2242
|
+
const known = [...amounts.values()].reduce((a, b) => a + b, 0);
|
|
2243
|
+
const remainder = total - known;
|
|
2244
|
+
if (remainder > 0.01) {
|
|
2245
|
+
// Unaccounted leftover (e.g. a non-refundable portion) is folded into
|
|
2246
|
+
// the primary/default payment channel rather than a fabricated 4th row.
|
|
2247
|
+
add(cardLabel, remainder);
|
|
2248
|
+
}
|
|
2249
|
+
return [...amounts.entries()].map(([label, value]) => ({ label, value }));
|
|
2250
|
+
});
|
|
2251
|
+
/** The "Total paid" row always expands — there's always at least one method to show. */
|
|
2252
|
+
this.hasPaidBreakdown = computed(() => this.paidBreakdownRows().length > 0);
|
|
2253
|
+
/**
|
|
2254
|
+
* Display label for `paymentMethodCustomerUsed`, or '' if unknown/internal.
|
|
2255
|
+
* Never surfaces an internal processor name (e.g. Hyperpay) to the agent —
|
|
2256
|
+
* callers fall back to a generic "Original Payment Method" label whenever
|
|
2257
|
+
* this returns ''.
|
|
2258
|
+
*/
|
|
2259
|
+
this.customerPaymentLabel = computed(() => {
|
|
2260
|
+
const code = this.normalizeGatewayCode(this.adjustedData()?.paymentMethodCustomerUsed);
|
|
2261
|
+
if (code && INTERNAL_GATEWAY_CODES.includes(code))
|
|
2262
|
+
return '';
|
|
2263
|
+
return code ? PAYMENT_GATEWAY_LABELS[code] : '';
|
|
2264
|
+
});
|
|
2265
|
+
/** Whether the confirmed cancellation's card refund settles via Mada. */
|
|
2266
|
+
this.isMadaMethod = computed(() => {
|
|
2267
|
+
const code = this.normalizeGatewayCode(this.adjustedData()?.paymentMethodCustomerUsed);
|
|
2268
|
+
return !!code && MADA_GATEWAY_CODES.includes(code);
|
|
2269
|
+
});
|
|
2270
|
+
/**
|
|
2271
|
+
* Label for the standalone "Original Payment Method" option card (shown
|
|
2272
|
+
* when the whole refund goes to one destination). Mada wins over
|
|
2273
|
+
* everything else, reading "Mada Card" instead of the generic gateway
|
|
2274
|
+
* name or the "Original Payment Method" fallback.
|
|
2275
|
+
*/
|
|
2276
|
+
this.originalPaymentLabel = computed(() => {
|
|
2277
|
+
if (this.isMadaMethod())
|
|
2278
|
+
return 'Mada Card';
|
|
2279
|
+
return this.customerPaymentLabel() || 'Original Payment Method';
|
|
2280
|
+
});
|
|
2281
|
+
/** Same as `isMadaMethod`, scoped to the confirmed cancellation result. */
|
|
2282
|
+
this.confirmIsMadaMethod = computed(() => {
|
|
2283
|
+
const code = this.normalizeGatewayCode(this.cancelResult()?.paymentMethodCustomerUsed);
|
|
2284
|
+
return !!code && MADA_GATEWAY_CODES.includes(code);
|
|
2285
|
+
});
|
|
2286
|
+
/**
|
|
2287
|
+
* Split-refund destination rows — up to four: Almatar Wallet (generic
|
|
2288
|
+
* label), the original payment method (card/installment gateway), a
|
|
2289
|
+
* separate loyalty-points-refund row when the backend sends one, and a
|
|
2290
|
+
* dedicated loyalty-program row (Wala, Qitaf, …) for forfeited points
|
|
2291
|
+
* (`forfeitedLoyaltyPointsFallback`) — kept distinct from the Wallet row
|
|
2292
|
+
* even when both are non-zero.
|
|
2293
|
+
*/
|
|
2294
|
+
this.splitRefundRows = computed(() => {
|
|
2295
|
+
const d = this.adjustedData();
|
|
2296
|
+
if (!d)
|
|
2297
|
+
return [];
|
|
2298
|
+
const rows = [];
|
|
2299
|
+
const wallet = Number(d.walletRefundAmount ?? 0);
|
|
2300
|
+
if (wallet > 0) {
|
|
2301
|
+
rows.push({ label: 'Almatar Wallet', value: wallet, instant: true, isLoyalty: false, badgeText: 'Instant' });
|
|
2302
|
+
}
|
|
2303
|
+
const card = Number(d.totalCardRefundAmount ?? 0);
|
|
2304
|
+
if (card > 0) {
|
|
2305
|
+
const badgeText = this.cardBadgeText();
|
|
2306
|
+
rows.push({
|
|
2307
|
+
label: this.isMadaMethod()
|
|
2308
|
+
? 'Mada Card'
|
|
2309
|
+
: (!this.isLoyaltyMethod() && this.customerPaymentLabel()) || 'Original Payment Method',
|
|
2310
|
+
value: card,
|
|
2311
|
+
instant: false,
|
|
2312
|
+
isLoyalty: false,
|
|
2313
|
+
badgeText,
|
|
2314
|
+
isMada: this.isMadaMethod(),
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
const loyalty = Number(d.totalLoyaltyPointsRefundAmount ?? 0);
|
|
2318
|
+
if (loyalty > 0) {
|
|
2319
|
+
rows.push({
|
|
2320
|
+
label: this.customerPaymentLabel() || 'Loyalty Points Refund',
|
|
2321
|
+
value: loyalty,
|
|
2322
|
+
instant: true,
|
|
2323
|
+
isLoyalty: true,
|
|
2324
|
+
badgeText: 'Instant',
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
const forfeitedLoyalty = this.forfeitedLoyaltyPointsFallback(d);
|
|
2328
|
+
if (forfeitedLoyalty > 0) {
|
|
2329
|
+
rows.push({
|
|
2330
|
+
label: this.walletOrLoyaltyLabel(d) || 'Loyalty Points',
|
|
2331
|
+
value: forfeitedLoyalty,
|
|
2332
|
+
instant: true,
|
|
2333
|
+
isLoyalty: true,
|
|
2334
|
+
badgeText: 'Instant',
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
return rows;
|
|
2338
|
+
});
|
|
2339
|
+
effect(() => {
|
|
2340
|
+
if (this.visible() && this.bookingId()) {
|
|
2341
|
+
this.loadRefund();
|
|
2342
|
+
}
|
|
2343
|
+
else {
|
|
2344
|
+
this.stopHints();
|
|
2345
|
+
this.refundData.set(null);
|
|
2346
|
+
this.cancelResult.set(null);
|
|
2347
|
+
this.onwardChecked.set(true);
|
|
2348
|
+
this.returnChecked.set(true);
|
|
2349
|
+
this.forcedLegType.set(null);
|
|
2350
|
+
this.onwardExpanded.set(true);
|
|
2351
|
+
this.returnExpanded.set(true);
|
|
2352
|
+
this.selectedSource.set('');
|
|
2353
|
+
this.preferredRefundMethod.set('originalPaymentMethod');
|
|
2354
|
+
this.confirming.set(false);
|
|
2355
|
+
this.cancelling.set(false);
|
|
2356
|
+
this.step.set('loading');
|
|
2357
|
+
}
|
|
2358
|
+
}, { allowSignalWrites: true });
|
|
2359
|
+
}
|
|
2360
|
+
loadRefund() {
|
|
2361
|
+
this.loading.set(true);
|
|
2362
|
+
this.startHints();
|
|
2363
|
+
this.bookingService.getRefundDetails(this.bookingId(), this.product()).subscribe({
|
|
2364
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2365
|
+
next: (raw) => {
|
|
2366
|
+
let data = raw?.data ?? raw;
|
|
2367
|
+
this.stopHints();
|
|
2368
|
+
this.refundData.set(data);
|
|
2369
|
+
// Auto-select source: prefer appliedSource, otherwise pick first available
|
|
2370
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2371
|
+
const breakdown = data?.sourceBreakdown ?? {};
|
|
2372
|
+
const sources = Object.keys(breakdown);
|
|
2373
|
+
if (sources.length > 0) {
|
|
2374
|
+
const applied = data?.appliedSource;
|
|
2375
|
+
this.selectedSource.set(applied && breakdown[applied] ? applied : sources[0]);
|
|
2376
|
+
}
|
|
2377
|
+
this.loading.set(false);
|
|
2378
|
+
// Only show the leg picker for a genuine round-trip the backend hasn't
|
|
2379
|
+
// already restricted to a single leg — otherwise go straight to details.
|
|
2380
|
+
if (this.showRoundTripLegCards()) {
|
|
2381
|
+
// Pre-check whichever legs are actually refundable, matching the
|
|
2382
|
+
// consumer app's "non-refundable legs can't be selected" behavior.
|
|
2383
|
+
this.onwardChecked.set(this.onwardRefundable());
|
|
2384
|
+
this.returnChecked.set(this.returnRefundable());
|
|
2385
|
+
this.step.set('select-leg');
|
|
2386
|
+
}
|
|
2387
|
+
else {
|
|
2388
|
+
this.step.set('details');
|
|
2389
|
+
}
|
|
2390
|
+
},
|
|
2391
|
+
error: () => { this.stopHints(); this.loading.set(false); this.toast.error('Could not load refund details.'); },
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Both legs in play — sum each leg's own figures instead of trusting root
|
|
2396
|
+
* (root isn't reliably scoped to the picked alternatives, e.g. rounding
|
|
2397
|
+
* can drift between leg totals and the combined root total).
|
|
2398
|
+
*/
|
|
2399
|
+
buildCombinedLegData(root, onward, ret) {
|
|
2400
|
+
const onwardData = this.buildLegData(root, onward, 'onward');
|
|
2401
|
+
const returnData = this.buildLegData(root, ret, 'return');
|
|
2402
|
+
const sum = (key) => Number(onwardData[key] || 0) + Number(returnData[key] || 0);
|
|
2403
|
+
return {
|
|
2404
|
+
...root,
|
|
2405
|
+
isRefundable: onward.isRefundable === true && ret.isRefundable === true,
|
|
2406
|
+
totalRefundAmount: sum('totalRefundAmount'),
|
|
2407
|
+
totalRefundAmountBeforeCeil: sum('totalRefundAmountBeforeCeil'),
|
|
2408
|
+
totalCardRefundAmount: sum('totalCardRefundAmount'),
|
|
2409
|
+
totalCreditRefundAmount: sum('totalCreditRefundAmount'),
|
|
2410
|
+
totalNonRefundableServiceCharge: sum('totalNonRefundableServiceCharge'),
|
|
2411
|
+
totalCouponAmount: sum('totalCouponAmount'),
|
|
2412
|
+
walletRefundAmount: sum('walletRefundAmount'),
|
|
2413
|
+
totalPenaltyPoints: sum('totalPenaltyPoints'),
|
|
2414
|
+
totalSupplierPenaltyPoints: sum('totalSupplierPenaltyPoints'),
|
|
2415
|
+
totalLoyaltyPoints: sum('totalLoyaltyPoints'),
|
|
2416
|
+
totalGiftAmount: sum('totalGiftAmount'),
|
|
2417
|
+
totalAncillaryRefundAmount: sum('totalAncillaryRefundAmount'),
|
|
2418
|
+
totalLoyaltyPointsRefundAmount: sum('totalLoyaltyPointsRefundAmount'),
|
|
2419
|
+
totalCancellationFee: sum('totalCancellationFee'),
|
|
2420
|
+
totalSupplierBookingAmount: sum('totalSupplierBookingAmount'),
|
|
2421
|
+
originalAmount: Number(root.originalAmount || 0),
|
|
2422
|
+
refundBreakdown: this.mergeBreakdowns(onwardData.refundBreakdown, returnData.refundBreakdown),
|
|
2423
|
+
legInfo: { onward, return: ret },
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
/** Combine two legs' itemized breakdowns for the "both legs" combined view. */
|
|
2427
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2428
|
+
mergeBreakdowns(a, b) {
|
|
2429
|
+
if (!a && !b)
|
|
2430
|
+
return undefined;
|
|
2431
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2432
|
+
const mergeSide = (sa, sb) => ({
|
|
2433
|
+
fare: Number(sa?.fare || 0) + Number(sb?.fare || 0) || undefined,
|
|
2434
|
+
addOns: [...(sa?.addOns ?? []), ...(sb?.addOns ?? [])],
|
|
2435
|
+
ancillaries: [...(sa?.ancillaries ?? []), ...(sb?.ancillaries ?? [])],
|
|
2436
|
+
serviceFee: Number(sa?.serviceFee || 0) + Number(sb?.serviceFee || 0) || undefined,
|
|
2437
|
+
handlingFee: Number(sa?.handlingFee || 0) + Number(sb?.handlingFee || 0) || undefined,
|
|
2438
|
+
cancellationPenalty: Number(sa?.cancellationPenalty || 0) + Number(sb?.cancellationPenalty || 0) || undefined,
|
|
2439
|
+
});
|
|
2440
|
+
return {
|
|
2441
|
+
refundable: mergeSide(a?.refundable, b?.refundable),
|
|
2442
|
+
nonRefundable: mergeSide(a?.nonRefundable, b?.nonRefundable),
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
buildLegData(root, leg, legType) {
|
|
2446
|
+
// When this leg has no sibling in `legInfo` (a genuine one-way booking,
|
|
2447
|
+
// or a narrowed CrossSeller selection where the API only ever sent the
|
|
2448
|
+
// one leg), root necessarily describes this same single leg — so a
|
|
2449
|
+
// destination total the leg's own object left at 0 can safely fall back
|
|
2450
|
+
// to root's figure instead of silently zeroing out a real refund
|
|
2451
|
+
// destination. With a sibling leg present, root aggregates both legs and
|
|
2452
|
+
// is not trustworthy per-leg, so the leg's own value (even 0) wins.
|
|
2453
|
+
const hasSiblingLeg = legType === 'onward' ? !!root.legInfo?.return : !!root.legInfo?.onward;
|
|
2454
|
+
const fallback = (value, rootValue) => Number(value || (hasSiblingLeg ? 0 : rootValue) || 0);
|
|
2455
|
+
const isCreditShell = leg.isCreditShell === true;
|
|
2456
|
+
const legRefund = Number(leg.totalRefundAmount ?? 0);
|
|
2457
|
+
const cardRefund = isCreditShell ? 0 : fallback(leg.totalCardRefundAmount, root.totalCardRefundAmount);
|
|
2458
|
+
const creditRefund = isCreditShell ? legRefund : fallback(leg.totalCreditRefundAmount, root.totalCreditRefundAmount);
|
|
2459
|
+
// The backend sometimes omits `isRefundable` on a single-leg booking's
|
|
2460
|
+
// leg object entirely (not `false`, just absent) while the root/source
|
|
2461
|
+
// breakdown both say the booking is refundable — trust the root flag
|
|
2462
|
+
// rather than defaulting a missing field to "not eligible for a refund".
|
|
2463
|
+
const legIsRefundable = leg.isRefundable === undefined ? root.isRefundable === true : leg.isRefundable === true;
|
|
2464
|
+
return {
|
|
2465
|
+
...root,
|
|
2466
|
+
isRefundable: legIsRefundable,
|
|
2467
|
+
totalRefundAmount: legRefund,
|
|
2468
|
+
// Pre-rounding total — the backend ceils `totalRefundAmount` to a whole
|
|
2469
|
+
// currency unit, so wallet+card+credit+loyalty legitimately fall a few
|
|
2470
|
+
// cents short of it; reconciliation must compare against this instead.
|
|
2471
|
+
totalRefundAmountBeforeCeil: Number(leg.totalRefundAmountBeforeCeil ?? leg.totalRefundAmount ?? 0),
|
|
2472
|
+
totalCardRefundAmount: cardRefund,
|
|
2473
|
+
totalCreditRefundAmount: creditRefund,
|
|
2474
|
+
totalNonRefundableServiceCharge: Number(leg.totalNonRefundableServiceCharge || 0),
|
|
2475
|
+
totalCouponAmount: Number(leg.totalCouponAmount || 0),
|
|
2476
|
+
walletRefundAmount: fallback(leg.walletRefundAmount, root.walletRefundAmount),
|
|
2477
|
+
totalPenaltyPoints: Number(leg.totalPenaltyPoints || 0),
|
|
2478
|
+
totalSupplierPenaltyPoints: Number(leg.totalSupplierPenaltyPoints || 0),
|
|
2479
|
+
totalLoyaltyPoints: Number(leg.totalLoyaltyPoints || 0),
|
|
2480
|
+
totalGiftAmount: Number(leg.totalGiftAmount || 0),
|
|
2481
|
+
totalAncillaryRefundAmount: Number(leg.totalAncillaryRefundAmount || 0),
|
|
2482
|
+
totalLoyaltyPointsRefundAmount: fallback(leg.totalLoyaltyPointsRefundAmount, root.totalLoyaltyPointsRefundAmount),
|
|
2483
|
+
totalCancellationFee: Number(leg.totalCancellationFee ?? leg.totalPenaltyPoints ?? 0),
|
|
2484
|
+
totalSupplierBookingAmount: Number(leg.totalSupplierBookingAmount || 0),
|
|
2485
|
+
originalAmount: Number(root.originalAmount || 0),
|
|
2486
|
+
// Leg-scoped breakdown wins when the backend sends one; a single-leg
|
|
2487
|
+
// booking may only carry it at the root instead.
|
|
2488
|
+
refundBreakdown: leg.refundBreakdown ?? root.refundBreakdown,
|
|
2489
|
+
legInfo: legType === 'onward' ? { onward: leg } : { return: leg },
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
startHints() {
|
|
2493
|
+
let i = 0;
|
|
2494
|
+
this.cancelHint.set(this.CANCEL_HINTS[0]);
|
|
2495
|
+
this.hintTimer = setInterval(() => {
|
|
2496
|
+
if (i < this.CANCEL_HINTS.length - 1) {
|
|
2497
|
+
i++;
|
|
2498
|
+
this.cancelHint.set(this.CANCEL_HINTS[i]);
|
|
2499
|
+
}
|
|
2500
|
+
else {
|
|
2501
|
+
clearInterval(this.hintTimer);
|
|
2502
|
+
this.hintTimer = null;
|
|
2503
|
+
}
|
|
2504
|
+
}, 1800);
|
|
2505
|
+
}
|
|
2506
|
+
stopHints() {
|
|
2507
|
+
if (this.hintTimer) {
|
|
2508
|
+
clearInterval(this.hintTimer);
|
|
2509
|
+
this.hintTimer = null;
|
|
2510
|
+
}
|
|
2511
|
+
this.cancelHint.set('');
|
|
2512
|
+
}
|
|
2513
|
+
fmt(value, currency) {
|
|
2514
|
+
const cur = currency ?? this.refundData()?.currency ?? 'SAR';
|
|
2515
|
+
return `${this.fmtNum(value)} ${cur}`;
|
|
2516
|
+
}
|
|
2517
|
+
// Plain formatted number without a currency suffix (used by the calc divider / formula box).
|
|
2518
|
+
fmtNum(value) {
|
|
2519
|
+
return Number(value || 0).toLocaleString('en-SA', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
2520
|
+
}
|
|
2521
|
+
fmtNeg(value, currency) {
|
|
2522
|
+
const n = Number(value || 0);
|
|
2523
|
+
const str = this.fmt(n, currency);
|
|
2524
|
+
return n > 0 ? `− ${str}` : str;
|
|
2525
|
+
}
|
|
2526
|
+
fmtNegResult(value) {
|
|
2527
|
+
const n = Number(value || 0);
|
|
2528
|
+
const str = this.fmtResult(n);
|
|
2529
|
+
return n > 0 ? `− ${str}` : str;
|
|
2530
|
+
}
|
|
2531
|
+
// Entry point from the details screen: always goes to the refund-method
|
|
2532
|
+
// step. For credit-shell refunds that step shows a single read-only credit
|
|
2533
|
+
// notice instead of a picker, since the refund can only go to the shell —
|
|
2534
|
+
// but the agent still sees it before confirming, same as the standard flow.
|
|
2535
|
+
requestCancel() {
|
|
2536
|
+
// NIL refund: nothing to refund at all — skip the refund-method popup
|
|
2537
|
+
// and cancel directly.
|
|
2538
|
+
if (this.isNilRefund()) {
|
|
2539
|
+
this.confirm();
|
|
2540
|
+
}
|
|
2541
|
+
else {
|
|
2542
|
+
this.confirming.set(true);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
confirm() {
|
|
2546
|
+
const sel = this.selectedLeg();
|
|
2547
|
+
// Build legInfo array: omit when cancelling both (backend treats absence as both)
|
|
2548
|
+
const legInfo = sel === 'onward' ? ['onward'] :
|
|
2549
|
+
sel === 'return' ? ['return'] :
|
|
2550
|
+
undefined; // both legs — omit the field
|
|
2551
|
+
const source = this.selectedSource() || undefined;
|
|
2552
|
+
const preferredRefundMethod = this.preferredRefundMethod();
|
|
2553
|
+
const splitAmounts = preferredRefundMethod === 'split'
|
|
2554
|
+
? {
|
|
2555
|
+
walletRefundAmount: Number(this.adjustedData()?.walletRefundAmount ?? 0),
|
|
2556
|
+
totalCardRefundAmount: Number(this.adjustedData()?.totalCardRefundAmount ?? 0),
|
|
2557
|
+
}
|
|
2558
|
+
: undefined;
|
|
2559
|
+
this.confirming.set(false);
|
|
2560
|
+
this.stopHints();
|
|
2561
|
+
this.cancelling.set(true);
|
|
2562
|
+
this.bookingService.confirmCancel(this.bookingId(), this.product(), legInfo, source, preferredRefundMethod, splitAmounts).subscribe({
|
|
2563
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2564
|
+
next: (raw) => {
|
|
2565
|
+
this.stopHints();
|
|
2566
|
+
this.cancelling.set(false);
|
|
2567
|
+
this.cancelResult.set(raw?.data ?? raw);
|
|
2568
|
+
this.confirmed.emit(sel);
|
|
2569
|
+
},
|
|
2570
|
+
error: () => {
|
|
2571
|
+
this.stopHints();
|
|
2572
|
+
this.cancelling.set(false);
|
|
2573
|
+
this.toast.error('Cancellation failed. Please try again.');
|
|
2574
|
+
},
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdCancelDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2578
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SbdCancelDialogComponent, isStandalone: true, selector: "app-sbd-cancel-dialog", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, bookingId: { classPropertyName: "bookingId", publicName: "bookingId", isSignal: true, isRequired: false, transformFunction: null }, product: { classPropertyName: "product", publicName: "product", isSignal: true, isRequired: false, transformFunction: null }, pnrInfo: { classPropertyName: "pnrInfo", publicName: "pnrInfo", isSignal: true, isRequired: false, transformFunction: null }, bookingInfo: { classPropertyName: "bookingInfo", publicName: "bookingInfo", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", confirmed: "confirmed" }, ngImport: i0, template: "<p-dialog [visible]=\"visible()\" (visibleChange)=\"$event ? null : closed.emit()\"\n [modal]=\"true\" [blockScroll]=\"true\" [draggable]=\"false\" [resizable]=\"false\"\n [showHeader]=\"false\" [style]=\"{ width: '560px', padding: '0' }\" styleClass=\"sbd-cancel-dialog-wrapper\">\n\n <div class=\"sbd-cancel\">\n\n <!-- Loading refund details \u2014 shows cycling hints -->\n @if (loading()) {\n <div class=\"sbd-cancel__are-you-sure\">\n <button type=\"button\" class=\"sbd-cancel__close-btn sbd-cancel__close-btn--floating\" (click)=\"closed.emit()\" aria-label=\"Close\">\n <i class=\"pi pi-times\"></i>\n </button>\n <div class=\"sbd-cancel__ays-icon\">\n <i class=\"pi pi-spin pi-spinner\"></i>\n </div>\n @for (h of [cancelHint()]; track h) {\n <div class=\"sbd-cancel__ays-hint\">{{ h }}</div>\n }\n <div class=\"sbd-cancel__ays-dots\">\n <span></span><span></span><span></span>\n </div>\n </div>\n }\n\n <!-- Select leg \u2014 shown once the refund preview has fully loaded, before the\n refund breakdown, for round-trip bookings the backend hasn't already\n restricted to a single leg. Same card design as almatar-b2c's\n select-leg screen (checkbox overlay, refundable badge, disclaimer). -->\n @else if (step() === 'select-leg') {\n <div class=\"sbd-cancel__method-header\">\n <button type=\"button\" class=\"sbd-cancel__method-close\" (click)=\"closed.emit()\" aria-label=\"Close\">\n <i class=\"pi pi-times\"></i>\n </button>\n <div class=\"sbd-cancel__method-header-text\">\n <h3 class=\"sbd-cancel__method-title\">Select Leg to Cancel</h3>\n <span class=\"sbd-cancel__method-sub\">Booking ID #{{ bookingId() }}</span>\n </div>\n </div>\n\n <div class=\"sbd-cancel__select-leg-body\">\n @if (forcedLegType(); as forced) {\n <div class=\"sbd-cancel__single-leg-note\">\n <i class=\"pi pi-info-circle\"></i>\n <span>This airline doesn't allow cancelling a single flight \u2014 {{ itineraryLeg(forced)?.from }} to {{ itineraryLeg(forced)?.to }} will be included automatically.</span>\n </div>\n }\n @if (allLegsNonRefundable()) {\n <div class=\"sbd-cancel__all-non-refundable-note\">\n <i class=\"pi pi-exclamation-triangle\"></i>\n <span>Sorry, we're unable to provide a refund for this booking, as both flights are non-refundable based on airline rules.</span>\n </div>\n }\n <!-- Onward -->\n <div class=\"sbd-cancel__leg-select-card\"\n [ngClass]=\"{\n 'sbd-cancel__leg-select-card--selected': onwardChecked() && onwardRefundable(),\n 'sbd-cancel__leg-select-card--disabled': !onwardRefundable(),\n 'sbd-cancel__leg-select-card--frozen': forcedLegType() === 'onward'\n }\">\n @if (forcedLegType() !== 'onward') {\n <label class=\"sbd-cancel__leg-select-checkbox\">\n <input type=\"checkbox\" [checked]=\"onwardChecked()\" [disabled]=\"!onwardRefundable()\"\n (change)=\"toggleLeg('onward')\" />\n <span class=\"sbd-cancel__leg-select-icon\"><i class=\"pi pi-check\"></i></span>\n </label>\n } @else {\n <i class=\"pi pi-lock sbd-cancel__leg-select-lock\"></i>\n }\n <div class=\"sbd-cancel__leg-select-content\">\n @if (itineraryLeg('onward'); as leg) {\n <div class=\"sbd-cancel__leg-select-top\">\n <div>\n <span class=\"sbd-cancel__leg-select-name\">{{ leg.from }} to {{ leg.to }}</span>\n <div class=\"sbd-cancel__leg-select-date\">{{ fmtShortDate(leg.depTime) }}</div>\n </div>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline\"\n [ngClass]=\"onwardRefundable() && forcedLegType() !== 'onward' ? 'sbd-cancel__badge--green' : 'sbd-cancel__badge--muted'\">\n {{ onwardRefundable() ? 'Refundable' : 'Non-Refundable' }}\n </span>\n </div>\n <div class=\"sbd-cancel__leg-select-times\">\n <span class=\"sbd-cancel__leg-select-time\">{{ fmtTime(leg.depTime) }}</span>\n <span class=\"sbd-cancel__leg-select-line\">\n <span class=\"sbd-cancel__leg-select-line-stops\">{{ leg.stops.length > 0 ? (leg.stops.length + ' stop' + (leg.stops.length > 1 ? 's' : '')) : 'Direct' }}</span>\n @if (fmtMins(leg.totalMinutes)) { <span class=\"sbd-cancel__leg-select-line-dur\">{{ fmtMins(leg.totalMinutes) }}</span> }\n <i class=\"pi pi-arrow-right sbd-cancel__leg-select-line-arrow\"></i>\n </span>\n <span class=\"sbd-cancel__leg-select-time\">{{ fmtTime(leg.arrTime) }}</span>\n </div>\n @if (leg.airlineName || leg.cabin) {\n <span class=\"sbd-cancel__leg-select-airline\">\n {{ leg.airlineName }}{{ leg.airlineName && leg.cabin ? ' \u00B7 ' : '' }}{{ leg.cabin }}\n </span>\n }\n <div class=\"sbd-cancel__leg-select-footer\">\n @if (leg.operatedBy.length) {\n <span class=\"sbd-cancel__leg-select-operated-by\">Operated by {{ leg.operatedBy.join(', ') }}</span>\n }\n <!-- <i class=\"pi pi-chevron-down sbd-cancel__leg-select-chevron\"></i> -->\n </div>\n } @else {\n <div class=\"sbd-cancel__leg-select-top\">\n <span class=\"sbd-cancel__leg-select-name\">Onward</span>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline\"\n [ngClass]=\"onwardRefundable() && forcedLegType() !== 'onward' ? 'sbd-cancel__badge--green' : 'sbd-cancel__badge--muted'\">\n {{ onwardRefundable() ? 'Refundable' : 'Non-Refundable' }}\n </span>\n </div>\n }\n @if (!onwardRefundable()) {\n <div class=\"sbd-cancel__leg-select-notice\">\n <i class=\"pi pi-info-circle\"></i>\n <span>This flight type doesn't allow refunds on cancellation, based on the airline's fare rules.</span>\n </div>\n }\n </div>\n </div>\n\n <!-- Return -->\n <div class=\"sbd-cancel__leg-select-card\"\n [ngClass]=\"{\n 'sbd-cancel__leg-select-card--selected': returnChecked() && returnRefundable(),\n 'sbd-cancel__leg-select-card--disabled': !returnRefundable(),\n 'sbd-cancel__leg-select-card--frozen': forcedLegType() === 'return'\n }\">\n @if (forcedLegType() !== 'return') {\n <label class=\"sbd-cancel__leg-select-checkbox\">\n <input type=\"checkbox\" [checked]=\"returnChecked()\" [disabled]=\"!returnRefundable()\"\n (change)=\"toggleLeg('return')\" />\n <span class=\"sbd-cancel__leg-select-icon\"><i class=\"pi pi-check\"></i></span>\n </label>\n } @else {\n <i class=\"pi pi-lock sbd-cancel__leg-select-lock\"></i>\n }\n <div class=\"sbd-cancel__leg-select-content\">\n @if (itineraryLeg('return'); as leg) {\n <div class=\"sbd-cancel__leg-select-top\">\n <div>\n <span class=\"sbd-cancel__leg-select-name\">{{ leg.from }} to {{ leg.to }}</span>\n <div class=\"sbd-cancel__leg-select-date\">{{ fmtShortDate(leg.depTime) }}</div>\n </div>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline\"\n [ngClass]=\"returnRefundable() && forcedLegType() !== 'return' ? 'sbd-cancel__badge--green' : 'sbd-cancel__badge--muted'\">\n {{ returnRefundable() ? 'Refundable' : 'Non-Refundable' }}\n </span>\n </div>\n <div class=\"sbd-cancel__leg-select-times\">\n <span class=\"sbd-cancel__leg-select-time\">{{ fmtTime(leg.depTime) }}</span>\n <span class=\"sbd-cancel__leg-select-line\">\n <span class=\"sbd-cancel__leg-select-line-stops\">{{ leg.stops.length > 0 ? (leg.stops.length + ' stop' + (leg.stops.length > 1 ? 's' : '')) : 'Direct' }}</span>\n @if (fmtMins(leg.totalMinutes)) { <span class=\"sbd-cancel__leg-select-line-dur\">{{ fmtMins(leg.totalMinutes) }}</span> }\n <i class=\"pi pi-arrow-right sbd-cancel__leg-select-line-arrow\"></i>\n </span>\n <span class=\"sbd-cancel__leg-select-time\">{{ fmtTime(leg.arrTime) }}</span>\n </div>\n @if (leg.airlineName || leg.cabin) {\n <span class=\"sbd-cancel__leg-select-airline\">\n {{ leg.airlineName }}{{ leg.airlineName && leg.cabin ? ' \u00B7 ' : '' }}{{ leg.cabin }}\n </span>\n }\n <div class=\"sbd-cancel__leg-select-footer\">\n @if (leg.operatedBy.length) {\n <span class=\"sbd-cancel__leg-select-operated-by\">Operated by {{ leg.operatedBy.join(', ') }}</span>\n }\n <!-- <i class=\"pi pi-chevron-down sbd-cancel__leg-select-chevron\"></i> -->\n </div>\n } @else {\n <div class=\"sbd-cancel__leg-select-top\">\n <span class=\"sbd-cancel__leg-select-name\">Return</span>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline\"\n [ngClass]=\"returnRefundable() && forcedLegType() !== 'return' ? 'sbd-cancel__badge--green' : 'sbd-cancel__badge--muted'\">\n {{ returnRefundable() ? 'Refundable' : 'Non-Refundable' }}\n </span>\n </div>\n }\n @if (!returnRefundable()) {\n <div class=\"sbd-cancel__leg-select-notice\">\n <i class=\"pi pi-info-circle\"></i>\n <span>This flight type doesn't allow refunds on cancellation, based on the airline's fare rules.</span>\n </div>\n }\n </div>\n </div>\n </div>\n\n @if (!allLegsNonRefundable() && !onwardChecked() && !returnChecked()) {\n <div class=\"sbd-cancel__leg-select-hint\">Select at least one flight to continue</div>\n }\n\n <div class=\"sbd-cancel__footer\">\n <button pButton\n styleClass=\"sbd-cancel__btn-primary\"\n label=\"Continue\"\n [disabled]=\"allLegsNonRefundable() || (!onwardChecked() && !returnChecked())\"\n (click)=\"step.set('details')\"></button>\n </div>\n }\n\n <!-- Refund method \u2014 replaces the old are-you-sure + radio picker.\n Stays mounted (dimmed) behind the result overlay once confirmed. -->\n @else if (confirming() || cancelResult()) {\n <div class=\"sbd-cancel__method-header\">\n <button type=\"button\" class=\"sbd-cancel__method-close\" (click)=\"confirming.set(false)\" aria-label=\"Close\">\n <i class=\"pi pi-times\"></i>\n </button>\n <div class=\"sbd-cancel__method-header-text\">\n <h3 class=\"sbd-cancel__method-title\">Refund Method</h3>\n <span class=\"sbd-cancel__method-sub\">Booking ID #{{ bookingId() }}</span>\n </div>\n </div>\n\n <div class=\"sbd-cancel__method-body\">\n <div class=\"sbd-cancel__method-amount\">\n <span>You will get back</span>\n <strong>{{ fmt(adjustedData()?.totalRefundAmount) }}</strong>\n </div>\n\n @if (isCreditShell()) {\n <!-- Credit-shell refunds have exactly one destination \u2014 the airline\n credit shell \u2014 so show it as a read-only card, not a picker. -->\n <div class=\"sbd-cancel__method-option sbd-cancel__method-option--active\">\n <span class=\"sbd-cancel__method-icon\"><i class=\"pi pi-wallet\"></i></span>\n <span class=\"sbd-cancel__method-body-text\">\n <span class=\"sbd-cancel__method-name\">{{ creditShellSupplierName() }} website credit</span>\n <span class=\"sbd-cancel__method-desc\">Your refund will be issued instantly by {{ creditShellSupplierName() }}.</span>\n <span class=\"sbd-cancel__method-badge sbd-cancel__method-badge--instant\">\n <svg width=\"12\" height=\"9\" viewBox=\"0 0 12 9\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M7.31818 0C8.42727 0 9.49093 0.440583 10.2752 1.22483C11.0594 2.00907 11.5 3.07273 11.5 4.18182C11.5 5.2909 11.0594 6.35457 10.2752 7.13881C9.49093 7.92305 8.42727 8.36364 7.31818 8.36364C6.20909 8.36364 5.14543 7.92305 4.36119 7.13881C3.57695 6.35457 3.13636 5.2909 3.13636 4.18182C3.13636 3.07273 3.57695 2.00907 4.36119 1.22483C5.14543 0.440583 6.20909 0 7.31818 0ZM7.31818 1.04545C6.48637 1.04545 5.68862 1.37589 5.10044 1.96407C4.51226 2.55226 4.18182 3.35 4.18182 4.18182C4.18182 5.01363 4.51226 5.81138 5.10044 6.39956C5.68862 6.98774 6.48637 7.31818 7.31818 7.31818C8.15 7.31818 8.94774 6.98774 9.53593 6.39956C10.1241 5.81138 10.4545 5.01363 10.4545 4.18182C10.4545 3.35 10.1241 2.55226 9.53593 1.96407C8.94774 1.37589 8.15 1.04545 7.31818 1.04545ZM6.79545 2.09091H7.57955V4.06682L8.7975 5.28477L8.24341 5.83886L6.79545 4.39091V2.09091ZM0.522727 7.31818C0.384091 7.31818 0.251134 7.26311 0.153103 7.16508C0.0550729 7.06705 0 6.93409 0 6.79545C0 6.65682 0.0550729 6.52386 0.153103 6.42583C0.251134 6.3278 0.384091 6.27273 0.522727 6.27273H2.52477C2.68682 6.64386 2.89591 6.99409 3.13636 7.31818H0.522727ZM1.04545 4.70455C0.906819 4.70455 0.773861 4.64947 0.67583 4.55144C0.5778 4.45341 0.522727 4.32045 0.522727 4.18182C0.522727 4.04318 0.5778 3.91022 0.67583 3.81219C0.773861 3.71416 0.906819 3.65909 1.04545 3.65909H2.11705L2.09091 4.18182L2.11705 4.70455H1.04545ZM1.56818 2.09091C1.42955 2.09091 1.29659 2.03584 1.19856 1.93781C1.10053 1.83978 1.04545 1.70682 1.04545 1.56818C1.04545 1.42955 1.10053 1.29659 1.19856 1.19856C1.29659 1.10053 1.42955 1.04545 1.56818 1.04545H3.13636C2.89591 1.36955 2.68682 1.71977 2.52477 2.09091H1.56818Z\"\n fill=\"#15803C\"\n />\n </svg>\n Instant\n </span>\n </span>\n </div>\n\n <div class=\"sbd-cancel__notice sbd-cancel__notice--credit\">\n <i class=\"pi pi-info-circle\"></i>\n <span>\n Your refund will be issued as airline credit under the same PNR.\n It can be used only for future booking on {{ creditShellSupplierName() }} website.\n </span>\n </div>\n\n <!-- Refundable ancillaries (baggage, seats, \u2026) can independently be\n flagged credit-shell or not \u2014 each renders as its own card,\n matching either the credit-shell style above or the normal\n wallet style, both with the same nested note when credit-shell. -->\n @for (item of creditShellAncillaryItems(); track item.name) {\n <div class=\"sbd-cancel__method-option sbd-cancel__method-option--active\">\n <span class=\"sbd-cancel__method-icon\"><i class=\"pi pi-wallet\"></i></span>\n <span class=\"sbd-cancel__method-body-text\">\n <span class=\"sbd-cancel__method-name\">\n {{ item.isCreditShell ? (creditShellSupplierName() + ' website credit') : 'Almatar Wallet' }}\n \u2014 {{ item.name }}\n </span>\n <span class=\"sbd-cancel__method-desc\">\n @if (item.isCreditShell) {\n Your refund will be issued instantly by {{ creditShellSupplierName() }}.\n } @else {\n Usable for future bookings\n }\n </span>\n <span class=\"sbd-cancel__method-badge sbd-cancel__method-badge--instant\">\n <svg width=\"12\" height=\"9\" viewBox=\"0 0 12 9\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M7.31818 0C8.42727 0 9.49093 0.440583 10.2752 1.22483C11.0594 2.00907 11.5 3.07273 11.5 4.18182C11.5 5.2909 11.0594 6.35457 10.2752 7.13881C9.49093 7.92305 8.42727 8.36364 7.31818 8.36364C6.20909 8.36364 5.14543 7.92305 4.36119 7.13881C3.57695 6.35457 3.13636 5.2909 3.13636 4.18182C3.13636 3.07273 3.57695 2.00907 4.36119 1.22483C5.14543 0.440583 6.20909 0 7.31818 0ZM7.31818 1.04545C6.48637 1.04545 5.68862 1.37589 5.10044 1.96407C4.51226 2.55226 4.18182 3.35 4.18182 4.18182C4.18182 5.01363 4.51226 5.81138 5.10044 6.39956C5.68862 6.98774 6.48637 7.31818 7.31818 7.31818C8.15 7.31818 8.94774 6.98774 9.53593 6.39956C10.1241 5.81138 10.4545 5.01363 10.4545 4.18182C10.4545 3.35 10.1241 2.55226 9.53593 1.96407C8.94774 1.37589 8.15 1.04545 7.31818 1.04545ZM6.79545 2.09091H7.57955V4.06682L8.7975 5.28477L8.24341 5.83886L6.79545 4.39091V2.09091ZM0.522727 7.31818C0.384091 7.31818 0.251134 7.26311 0.153103 7.16508C0.0550729 7.06705 0 6.93409 0 6.79545C0 6.65682 0.0550729 6.52386 0.153103 6.42583C0.251134 6.3278 0.384091 6.27273 0.522727 6.27273H2.52477C2.68682 6.64386 2.89591 6.99409 3.13636 7.31818H0.522727ZM1.04545 4.70455C0.906819 4.70455 0.773861 4.64947 0.67583 4.55144C0.5778 4.45341 0.522727 4.32045 0.522727 4.18182C0.522727 4.04318 0.5778 3.91022 0.67583 3.81219C0.773861 3.71416 0.906819 3.65909 1.04545 3.65909H2.11705L2.09091 4.18182L2.11705 4.70455H1.04545ZM1.56818 2.09091C1.42955 2.09091 1.29659 2.03584 1.19856 1.93781C1.10053 1.83978 1.04545 1.70682 1.04545 1.56818C1.04545 1.42955 1.10053 1.29659 1.19856 1.19856C1.29659 1.10053 1.42955 1.04545 1.56818 1.04545H3.13636C2.89591 1.36955 2.68682 1.71977 2.52477 2.09091H1.56818Z\"\n fill=\"#15803C\"\n />\n </svg>\n Instant\n </span>\n @if (item.isCreditShell) {\n <span class=\"sbd-cancel__notice sbd-cancel__notice--info\">\n <i class=\"pi pi-info-circle\"></i>\n <span>Your refund will be issued as airline credit under the same PNR. It can be used only for future booking on {{ creditShellSupplierName() }} website.</span>\n </span>\n }\n </span>\n <strong>{{ fmt(item.amount) }}</strong>\n </div>\n }\n } @else {\n\n @if (isFullWalletRefundAllowed() || isWalletOnlyRefund() || isLoyaltyOnlyRefund()) {\n <button type=\"button\" class=\"sbd-cancel__method-option\"\n [ngClass]=\"{ 'sbd-cancel__method-option--active': preferredRefundMethod() === 'wallet' }\"\n (click)=\"preferredRefundMethod.set('wallet')\">\n <span class=\"sbd-cancel__method-icon\"><i class=\"pi pi-wallet\"></i></span>\n <span class=\"sbd-cancel__method-body-text\">\n <span class=\"sbd-cancel__method-name\">{{ walletCardLabel() }}</span>\n <span class=\"sbd-cancel__method-desc\">Usable for future bookings</span>\n <span class=\"sbd-cancel__method-badge sbd-cancel__method-badge--instant\">\n <svg width=\"12\" height=\"9\" viewBox=\"0 0 12 9\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M7.31818 0C8.42727 0 9.49093 0.440583 10.2752 1.22483C11.0594 2.00907 11.5 3.07273 11.5 4.18182C11.5 5.2909 11.0594 6.35457 10.2752 7.13881C9.49093 7.92305 8.42727 8.36364 7.31818 8.36364C6.20909 8.36364 5.14543 7.92305 4.36119 7.13881C3.57695 6.35457 3.13636 5.2909 3.13636 4.18182C3.13636 3.07273 3.57695 2.00907 4.36119 1.22483C5.14543 0.440583 6.20909 0 7.31818 0ZM7.31818 1.04545C6.48637 1.04545 5.68862 1.37589 5.10044 1.96407C4.51226 2.55226 4.18182 3.35 4.18182 4.18182C4.18182 5.01363 4.51226 5.81138 5.10044 6.39956C5.68862 6.98774 6.48637 7.31818 7.31818 7.31818C8.15 7.31818 8.94774 6.98774 9.53593 6.39956C10.1241 5.81138 10.4545 5.01363 10.4545 4.18182C10.4545 3.35 10.1241 2.55226 9.53593 1.96407C8.94774 1.37589 8.15 1.04545 7.31818 1.04545ZM6.79545 2.09091H7.57955V4.06682L8.7975 5.28477L8.24341 5.83886L6.79545 4.39091V2.09091ZM0.522727 7.31818C0.384091 7.31818 0.251134 7.26311 0.153103 7.16508C0.0550729 7.06705 0 6.93409 0 6.79545C0 6.65682 0.0550729 6.52386 0.153103 6.42583C0.251134 6.3278 0.384091 6.27273 0.522727 6.27273H2.52477C2.68682 6.64386 2.89591 6.99409 3.13636 7.31818H0.522727ZM1.04545 4.70455C0.906819 4.70455 0.773861 4.64947 0.67583 4.55144C0.5778 4.45341 0.522727 4.32045 0.522727 4.18182C0.522727 4.04318 0.5778 3.91022 0.67583 3.81219C0.773861 3.71416 0.906819 3.65909 1.04545 3.65909H2.11705L2.09091 4.18182L2.11705 4.70455H1.04545ZM1.56818 2.09091C1.42955 2.09091 1.29659 2.03584 1.19856 1.93781C1.10053 1.83978 1.04545 1.70682 1.04545 1.56818C1.04545 1.42955 1.10053 1.29659 1.19856 1.19856C1.29659 1.10053 1.42955 1.04545 1.56818 1.04545H3.13636C2.89591 1.36955 2.68682 1.71977 2.52477 2.09091H1.56818Z\"\n fill=\"#15803C\"\n />\n </svg>\n Instant Refund\n </span>\n @if (hasLoyaltyPointsRefund() && !hasCashWalletRefund()) {\n <span class=\"sbd-cancel__notice sbd-cancel__notice--info\">\n <i class=\"pi pi-info-circle\"></i>\n <span>External loyalty points are refunded to your almatar Wallet as permanent points and ready to use in your next booking.</span>\n </span>\n }\n </span>\n <span class=\"sbd-cancel__method-radio\"></span>\n </button>\n }\n\n <!-- Original payment method \u2014 replaced with a \"Split Refund\" card whenever\n part of the refund is forced to the wallet (e.g. loyalty points used),\n since the total can't go 100% to the original payment method.\n Name/desc/badge/icon become gateway-specific for installment (Tamara,\n Tabby) and loyalty (Al Rajhi Mokafaa) payment methods. -->\n @if (!hasSplitRefund() && !isWalletOnlyRefund() && !isLoyaltyOnlyRefund()) {\n <button type=\"button\" class=\"sbd-cancel__method-option\"\n [ngClass]=\"{ 'sbd-cancel__method-option--active': preferredRefundMethod() === 'originalPaymentMethod' }\"\n (click)=\"preferredRefundMethod.set('originalPaymentMethod')\">\n <span class=\"sbd-cancel__method-icon\">\n <i class=\"pi\" [ngClass]=\"isSpecialGatewayMethod() ? 'pi-wallet' : 'pi-credit-card'\"></i>\n </span>\n <span class=\"sbd-cancel__method-body-text\">\n <span class=\"sbd-cancel__method-name\">{{ originalPaymentLabel() }}</span>\n <span class=\"sbd-cancel__method-desc\">\n @if (isInstallmentMethod()) {\n Returned to your {{ customerPaymentLabel() }} installment plan\n } @else if (isLoyaltyMethod()) {\n Refunded to your {{ customerPaymentLabel() }} account\n } @else {\n Returned to your original payment method\n }\n </span>\n <span class=\"sbd-cancel__method-badge\" [ngClass]=\"isLoyaltyMethod() ? 'sbd-cancel__method-badge--loyalty' : 'sbd-cancel__method-badge--days'\">\n @if (!isLoyaltyMethod()) {\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M3.30078 6.50195C3.20312 6.50195 3.12174 6.4694 3.05664 6.4043C2.99154 6.33919 2.95898 6.25781 2.95898 6.16016C2.95898 6.06576 2.99154 5.986 3.05664 5.9209C3.12174 5.85579 3.20312 5.82324 3.30078 5.82324H5.5127V2.86914C5.5127 2.77474 5.54525 2.69499 5.61035 2.62988C5.67546 2.56478 5.75521 2.53223 5.84961 2.53223C5.94727 2.53223 6.02865 2.56478 6.09375 2.62988C6.15885 2.69499 6.19141 2.77474 6.19141 2.86914V6.16016C6.19141 6.25781 6.15885 6.33919 6.09375 6.4043C6.02865 6.4694 5.94727 6.50195 5.84961 6.50195H3.30078ZM5.85449 10.9551C5.16764 10.9551 4.52311 10.8249 3.9209 10.5645C3.31868 10.3073 2.78971 9.95085 2.33398 9.49512C1.87826 9.03939 1.52018 8.51042 1.25977 7.9082C1.0026 7.30599 0.874023 6.66146 0.874023 5.97461C0.874023 5.28776 1.0026 4.64323 1.25977 4.04102C1.52018 3.4388 1.87826 2.90983 2.33398 2.4541C2.78971 1.99512 3.31868 1.63704 3.9209 1.37988C4.52311 1.12272 5.16764 0.994141 5.85449 0.994141C6.54134 0.994141 7.18587 1.12272 7.78809 1.37988C8.3903 1.63704 8.91927 1.99512 9.375 2.4541C9.83073 2.90983 10.1872 3.4388 10.4443 4.04102C10.7048 4.64323 10.835 5.28776 10.835 5.97461C10.835 6.66146 10.7048 7.30599 10.4443 7.9082C10.1872 8.51042 9.83073 9.03939 9.375 9.49512C8.91927 9.95085 8.3903 10.3073 7.78809 10.5645C7.18587 10.8249 6.54134 10.9551 5.85449 10.9551ZM5.85449 10.125C6.42741 10.125 6.96452 10.0176 7.46582 9.80273C7.96712 9.58789 8.4082 9.29004 8.78906 8.90918C9.16992 8.52832 9.46777 8.08724 9.68262 7.58594C9.89746 7.08464 10.0049 6.54753 10.0049 5.97461C10.0049 5.40169 9.89746 4.86458 9.68262 4.36328C9.46777 3.85872 9.16992 3.41764 8.78906 3.04004C8.4082 2.65918 7.96712 2.36133 7.46582 2.14648C6.96452 1.93164 6.42741 1.82422 5.85449 1.82422C5.28158 1.82422 4.74447 1.93164 4.24316 2.14648C3.74186 2.36133 3.30078 2.65918 2.91992 3.04004C2.53906 3.41764 2.24121 3.85872 2.02637 4.36328C1.81152 4.86458 1.7041 5.40169 1.7041 5.97461C1.7041 6.54753 1.81152 7.08464 2.02637 7.58594C2.24121 8.08724 2.53906 8.52832 2.91992 8.90918C3.30078 9.29004 3.74186 9.58789 4.24316 9.80273C4.74447 10.0176 5.28158 10.125 5.85449 10.125Z\"\n fill=\"#0B54C2\"\n />\n </svg>\n } @else {\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <circle cx=\"6\" cy=\"6\" r=\"5\" stroke=\"#0B54C2\" stroke-width=\"1.2\" />\n <path d=\"M6 5.5v3M6 3.9v.01\" stroke=\"#0B54C2\" stroke-width=\"1.2\" stroke-linecap=\"round\" />\n </svg>\n }\n {{ cardBadgeText() }}\n </span>\n </span>\n <span class=\"sbd-cancel__option-end\">\n @if (isMadaMethod()) {\n <span class=\"sbd-cancel__mada-logo\" aria-hidden=\"true\">\n <ng-container *ngTemplateOutlet=\"madaLogo\"></ng-container>\n </span>\n }\n <span class=\"sbd-cancel__method-radio\"></span>\n </span>\n </button>\n }\n\n <!-- Split Refund -->\n @if (hasSplitRefund()) {\n <button type=\"button\" class=\"sbd-cancel__method-option sbd-cancel__method-option--split\"\n [ngClass]=\"{ 'sbd-cancel__method-option--active': preferredRefundMethod() === 'split' }\"\n (click)=\"preferredRefundMethod.set('split')\">\n <span class=\"sbd-cancel__method-icon\"><i class=\"pi pi-wallet\"></i></span>\n <span class=\"sbd-cancel__method-body-text\">\n <span class=\"sbd-cancel__method-name\">Split Refund</span>\n <span class=\"sbd-cancel__method-desc\">Returned to your original payment method</span>\n </span>\n <strong class=\"sbd-cancel__method-split-total\">{{ fmt(adjustedData()?.totalRefundAmount) }}</strong>\n <span class=\"sbd-cancel__method-radio\"></span>\n\n <span class=\"sbd-cancel__method-split-breakdown\">\n @for (row of splitRefundRows(); track row.label) {\n <span class=\"sbd-cancel__method-split-row\">\n <span class=\"sbd-cancel__method-split-label\">\n {{ row.label }}\n @if (row.isMada) {\n <span class=\"sbd-cancel__mada-logo\" aria-hidden=\"true\">\n <ng-container *ngTemplateOutlet=\"madaLogo\"></ng-container>\n </span>\n }\n </span>\n <strong>{{ fmt(row.value) }}</strong>\n </span>\n <span class=\"sbd-cancel__method-badge\"\n [ngClass]=\"row.instant ? 'sbd-cancel__method-badge--instant' : (isLoyaltyMethod() ? 'sbd-cancel__method-badge--loyalty' : 'sbd-cancel__method-badge--days')\">\n {{ row.badgeText }}\n </span>\n @if (row.isLoyalty) {\n <span class=\"sbd-cancel__notice sbd-cancel__notice--info\">\n <i class=\"pi pi-info-circle\"></i>\n <span>External loyalty points are refunded to your almatar Wallet as permanent points and ready to use in your next booking.</span>\n </span>\n }\n }\n </span>\n </button>\n }\n }\n </div>\n\n <div class=\"sbd-cancel__footer\">\n <button pButton\n styleClass=\"sbd-cancel__btn-danger-pill\"\n [label]=\"cancelling() ? 'Cancelling...' : 'Confirm Cancellation'\"\n [disabled]=\"cancelling()\" (click)=\"confirm()\"></button>\n </div>\n }\n\n <!-- Main card -->\n @else if (refundData() && step() === 'details') {\n <!-- Card header -->\n <div class=\"sbd-cancel__header sbd-cancel__header--stack\">\n <div class=\"sbd-cancel__header-titles\">\n <span class=\"sbd-cancel__header-title\">Cancel Booking Request</span>\n <span class=\"sbd-cancel__header-sub\">#{{ refundData()?.bookingId ?? bookingId() }}</span>\n </div>\n <button type=\"button\" class=\"sbd-cancel__close-btn\" (click)=\"closed.emit()\" aria-label=\"Close\">\n <i class=\"pi pi-times\"></i>\n </button>\n </div>\n\n <div class=\"sbd-cancel__body\">\n\n <!-- Non-refundable -->\n @if (!refundData()?.isRefundable) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-exclamation-triangle\"></i>\n <div>\n <strong>This booking is not eligible for a refund.</strong>\n @if (refundData()?.cause) {\n <p>{{ refundData()?.cause }}</p>\n } @else {\n <p>Please escalate to the relevant team for manual handling.</p>\n }\n </div>\n </div>\n }\n\n <!-- Refundable flow -->\n @else {\n\n <!-- Top summary: paid / get back -->\n <div class=\"sbd-cancel__summary\">\n <div class=\"sbd-cancel__summary-row\"\n [ngClass]=\"{ 'sbd-cancel__summary-row--toggle': hasPaidBreakdown() && !!adjustedData()?.totalWalletAmount }\"\n (click)=\"hasPaidBreakdown() && !!adjustedData()?.totalWalletAmount && togglePaidSummary()\">\n <span class=\"sbd-cancel__summary-label\"><i class=\"pi pi-wallet\"></i> Total paid</span>\n <span class=\"sbd-cancel__summary-value\">\n <strong>{{ fmt(adjustedData()?.originalAmount) }}</strong>\n @if (hasPaidBreakdown() && !!adjustedData()?.totalWalletAmount) {\n <button type=\"button\" class=\"sbd-cancel__leg-chevron\"\n [ngClass]=\"{ 'sbd-cancel__leg-chevron--collapsed': !paidSummaryExpanded() }\"\n aria-label=\"Toggle paid breakdown\">\n <i class=\"pi pi-chevron-up\"></i>\n </button>\n }\n </span>\n </div>\n\n @if (hasPaidBreakdown() && paidSummaryExpanded() && !!adjustedData()?.totalWalletAmount) {\n <div class=\"sbd-cancel__summary-breakdown\">\n @for (row of paidBreakdownRows(); track row.label) {\n @if (row.label === 'Almatar Wallet') {\n <div class=\"sbd-cancel__summary-breakdown-row\">\n <span>{{ row.label }}</span>\n <span>{{ fmt(row.value) }}</span>\n </div>\n }\n }\n </div>\n }\n\n <div class=\"sbd-cancel__summary-row sbd-cancel__summary-row--refund\">\n <span class=\"sbd-cancel__summary-label\"><i class=\"pi pi-dollar\"></i> You will get back</span>\n <strong>{{ fmt(adjustedData()?.totalRefundAmount) }}</strong>\n </div>\n </div>\n\n <!-- Golden Guarantee gift \u2014 overrides credit-shell messaging below -->\n @if (hasGoldenGuaranteeGift()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--success\">\n <i class=\"pi pi-shield\"></i>\n <span>You're covered by Cancel for any reason service, you'll get 100% refund, minus any applicable fees. See the breakdown below.</span>\n </div>\n }\n\n <!-- One-way (or a round-trip restricted to a single leg): single card -->\n @if (!showRoundTripLegCards()) {\n <!-- Source selector (multiple suppliers) -->\n @if (availableSources().length > 1) {\n <div class=\"sbd-cancel__source-selector\">\n <span class=\"sbd-cancel__source-label\">Cancel via</span>\n @for (src of availableSources(); track src) {\n <button class=\"sbd-cancel__source-chip\"\n [ngClass]=\"{\n 'sbd-cancel__source-chip--active': selectedSource() === src,\n 'sbd-cancel__source-chip--no-refund': activeSourceBreakdown()?.[src]?.isRefundable === false\n }\"\n (click)=\"selectedSource.set(src)\">\n {{ src }}\n @if (activeSourceBreakdown()?.[src]?.isRefundable === false) {\n <i class=\"pi pi-ban\" style=\"font-size:10px; margin-left:4px;\"></i>\n }\n </button>\n }\n </div>\n @if (sourceNonRefundableHint()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-ban\"></i>\n <span>{{ sourceNonRefundableHint() }}</span>\n </div>\n }\n }\n\n <!-- Not refundable -->\n @if (!currentData()?.isRefundable) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-ban\"></i>\n <div>\n <strong>This leg is not eligible for a refund.</strong>\n <p>Please escalate to the relevant team for manual handling.</p>\n </div>\n </div>\n } @else {\n <div class=\"sbd-cancel__section-title\">Refund Details</div>\n\n <div class=\"sbd-cancel__refund-card\">\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline sbd-cancel__badge--green\">Refundable</span>\n\n @if (refundableBreakdownRows(adjustedData()).length) {\n @for (r of refundableBreakdownRows(adjustedData()); track r.label) {\n <div class=\"sbd-cancel__refund-card-row\">\n <span>{{ r.label }}</span>\n <strong>{{ fmt(r.value) }}</strong>\n </div>\n }\n } @else {\n <div class=\"sbd-cancel__refund-card-row\">\n <span>Fare</span>\n <strong>{{ fmt(adjustedData()?.originalAmount) }}</strong>\n </div>\n }\n\n <!-- hide addon note new CR from CX -->\n <!-- @if (hasRefundableAddonNote(adjustedData())) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--neutral\">\n <i class=\"pi pi-info-circle\"></i>\n <span>Your add-on service/s refund will be processed separately by our Customer Service team shortly.</span>\n </div>\n } -->\n\n <!-- Deductions -->\n @if (deductions().length) {\n <div class=\"sbd-cancel__refund-card-divider\"></div>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline sbd-cancel__badge--muted\">Not Refunded - Deductions</span>\n @for (d of deductions(); track d.label) {\n <div class=\"sbd-cancel__refund-card-line\">\n <span>\n {{ d.label }}\n @if (d.badge) { <span class=\"sbd-cancel__tag\">{{ d.badge }}</span> }\n </span>\n <span class=\"sbd-cancel__refund-card-line--neg\">{{ fmtNeg(d.value) }}</span>\n </div>\n }\n }\n\n <div class=\"sbd-cancel__refund-card-divider sbd-cancel__refund-card-divider--dashed\"></div>\n <div class=\"sbd-cancel__refund-card-total\">\n <span>Total Refund</span>\n <strong>{{ fmt(adjustedData()?.totalRefundAmount) }}</strong>\n </div>\n </div>\n\n <!-- Credit shell -->\n @if (creditShellNote()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--credit\">\n <i class=\"pi pi-wallet\"></i>\n <span>\u2139\uFE0F {{ creditShellNote() }}</span>\n </div>\n }\n }\n }\n\n <!-- Round trip: one collapsible card per leg (only when both legs are cancellable) -->\n @if (showRoundTripLegCards()) {\n <!-- Gift hint -->\n @if (giftHint()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--tip\">\n <i class=\"pi pi-info-circle\"></i>\n <span>{{ giftHint() }}</span>\n </div>\n }\n\n <!-- Mixed hint -->\n @if (mixedHint()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-exclamation-triangle\"></i>\n <span>{{ mixedHint() }}</span>\n </div>\n }\n\n <!-- Source selector (multiple suppliers) -->\n @if (availableSources().length > 1) {\n <div class=\"sbd-cancel__source-selector\">\n <span class=\"sbd-cancel__source-label\">Cancel via</span>\n @for (src of availableSources(); track src) {\n <button class=\"sbd-cancel__source-chip\"\n [ngClass]=\"{\n 'sbd-cancel__source-chip--active': selectedSource() === src,\n 'sbd-cancel__source-chip--no-refund': activeSourceBreakdown()?.[src]?.isRefundable === false\n }\"\n (click)=\"selectedSource.set(src)\">\n {{ src }}\n @if (activeSourceBreakdown()?.[src]?.isRefundable === false) {\n <i class=\"pi pi-ban\" style=\"font-size:10px; margin-left:4px;\"></i>\n }\n </button>\n }\n </div>\n @if (sourceNonRefundableHint()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-ban\"></i>\n <span>{{ sourceNonRefundableHint() }}</span>\n </div>\n }\n }\n\n <!-- Onward leg card -->\n @if (onwardChecked()) {\n <div class=\"sbd-cancel__leg-card\">\n <div class=\"sbd-cancel__leg-card-header\" (click)=\"toggleLegExpanded('onward')\">\n <span class=\"sbd-cancel__leg-checkbox\">Onward</span>\n <button type=\"button\" class=\"sbd-cancel__leg-chevron\"\n [ngClass]=\"{ 'sbd-cancel__leg-chevron--collapsed': !onwardExpanded() }\"\n aria-label=\"Toggle\">\n <i class=\"pi pi-chevron-up\"></i>\n </button>\n </div>\n\n @if (onwardExpanded()) {\n <div class=\"sbd-cancel__leg-card-body\">\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline\"\n [ngClass]=\"legAdjustedData('onward')?.isRefundable ? 'sbd-cancel__badge--green' : 'sbd-cancel__badge--red'\">\n {{ legAdjustedData('onward')?.isRefundable ? 'Refundable' : 'Non-Refundable' }}\n </span>\n\n @if (legAdjustedData('onward')?.isRefundable) {\n @if (refundableBreakdownRows(legAdjustedData('onward')).length) {\n @for (r of refundableBreakdownRows(legAdjustedData('onward')); track r.label) {\n <div class=\"sbd-cancel__refund-card-row\">\n <span>{{ r.label }}</span>\n <strong>{{ fmt(r.value) }}</strong>\n </div>\n }\n } @else {\n <div class=\"sbd-cancel__refund-card-row\">\n <span>Fare</span>\n <strong>{{ fmt(legRefundPool('onward')) }}</strong>\n </div>\n }\n\n <!-- hide addon note new CR from CX -->\n <!-- @if (hasRefundableAddonNote(legAdjustedData('onward'))) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--neutral\">\n <i class=\"pi pi-info-circle\"></i>\n <span>Your add-on service/s refund will be processed separately by our Customer Service team shortly.</span>\n </div>\n } -->\n\n @if (legDeductions('onward').length && !isCreditShell()) {\n <div class=\"sbd-cancel__refund-card-divider\"></div>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline sbd-cancel__badge--muted\">Not Refunded - Deductions</span>\n @for (d of legDeductions('onward'); track d.label) {\n <div class=\"sbd-cancel__refund-card-line\">\n <span>\n {{ d.label }}\n @if (d.badge) { <span class=\"sbd-cancel__tag\">{{ d.badge }}</span> }\n </span>\n <span class=\"sbd-cancel__refund-card-line--neg\">{{ fmtNeg(d.value) }}</span>\n </div>\n }\n }\n\n <div class=\"sbd-cancel__refund-card-divider sbd-cancel__refund-card-divider--dashed\"></div>\n <div class=\"sbd-cancel__refund-card-total\">\n <span>Departure Total Refund</span>\n <strong>{{ fmt(legAdjustedData('onward')?.totalRefundAmount) }}</strong>\n </div>\n\n @if (refundData()?.legInfo?.onward?.isCreditShell && !hasGoldenGuaranteeGift()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--credit\">\n <i class=\"pi pi-wallet\"></i>\n <span>\u2139\uFE0F The refund for the onward flight has been issued as an Airline Credit Shell.</span>\n </div>\n }\n } @else {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-ban\"></i>\n <span>This leg is not eligible for a refund.</span>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Return leg card -->\n @if (returnChecked()) {\n <div class=\"sbd-cancel__leg-card\">\n <div class=\"sbd-cancel__leg-card-header\" (click)=\"toggleLegExpanded('return')\">\n <span class=\"sbd-cancel__leg-checkbox\">Return</span>\n <button type=\"button\" class=\"sbd-cancel__leg-chevron\"\n [ngClass]=\"{ 'sbd-cancel__leg-chevron--collapsed': !returnExpanded() }\"\n aria-label=\"Toggle\">\n <i class=\"pi pi-chevron-up\"></i>\n </button>\n </div>\n\n @if (returnExpanded()) {\n <div class=\"sbd-cancel__leg-card-body\">\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline\"\n [ngClass]=\"legAdjustedData('return')?.isRefundable ? 'sbd-cancel__badge--green' : 'sbd-cancel__badge--red'\">\n {{ legAdjustedData('return')?.isRefundable ? 'Refundable' : 'Non-Refundable' }}\n </span>\n\n @if (legAdjustedData('return')?.isRefundable) {\n @if (refundableBreakdownRows(legAdjustedData('return')).length) {\n @for (r of refundableBreakdownRows(legAdjustedData('return')); track r.label) {\n <div class=\"sbd-cancel__refund-card-row\">\n <span>{{ r.label }}</span>\n <strong>{{ fmt(r.value) }}</strong>\n </div>\n }\n } @else {\n <div class=\"sbd-cancel__refund-card-row\">\n <span>Fare</span>\n <strong>{{ fmt(legRefundPool('return')) }}</strong>\n </div>\n }\n\n <!-- hide addon note new CR from CX -->\n <!-- @if (hasRefundableAddonNote(legAdjustedData('return'))) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--neutral\">\n <i class=\"pi pi-info-circle\"></i>\n <span>Your add-on service/s refund will be processed separately by our Customer Service team shortly.</span>\n </div>\n } -->\n\n @if (legDeductions('return').length && !isCreditShell()) {\n <div class=\"sbd-cancel__refund-card-divider\"></div>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline sbd-cancel__badge--muted\">Not Refunded - Deductions</span>\n @for (d of legDeductions('return'); track d.label) {\n <div class=\"sbd-cancel__refund-card-line\">\n <span>\n {{ d.label }}\n @if (d.badge) { <span class=\"sbd-cancel__tag\">{{ d.badge }}</span> }\n </span>\n <span class=\"sbd-cancel__refund-card-line--neg\">{{ fmtNeg(d.value) }}</span>\n </div>\n }\n }\n\n <div class=\"sbd-cancel__refund-card-divider sbd-cancel__refund-card-divider--dashed\"></div>\n <div class=\"sbd-cancel__refund-card-total\">\n <span>Return Total Refund</span>\n <strong>{{ fmt(legAdjustedData('return')?.totalRefundAmount) }}</strong>\n </div>\n\n @if (refundData()?.legInfo?.return?.isCreditShell && !hasGoldenGuaranteeGift()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--credit\">\n <i class=\"pi pi-wallet\"></i>\n <span>\u2139\uFE0F The refund for the return flight has been issued as an Airline Credit Shell.</span>\n </div>\n }\n } @else {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-ban\"></i>\n <span>This leg is not eligible for a refund.</span>\n </div>\n }\n </div>\n }\n </div>\n }\n }\n\n <!-- Instant refund notice -->\n @if (currentData()?.isRefundable) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--shield\">\n <i class=\"pi pi-shield\"></i>\n <span>\n @if (isNilRefund()) {\n We're sorry, as per airline policy, no refund applies because the cancellation fees equal or exceed your refundable fare amount.\n } @else {\n @if (isWalletOnlyRefund() || isLoyaltyOnlyRefund()) {\n Your refund is instant to the customer's almatar Wallet.\n } @else {\n Your refund is instant from our side, your bank may take up to 15 business days to reflect it.\n }\n }\n @if (!isNilRefund() && !isCreditShell()) {\n <strong>You can select refund option in the next step</strong>\n }\n </span>\n </div>\n }\n }\n\n </div>\n\n <!-- Footer -->\n <div class=\"sbd-cancel__footer\">\n @if (refundData()?.isRefundable) {\n <button pButton label=\"Keep booking\" styleClass=\"p-button-outlined p-button-secondary sbd-cancel__btn-keep\" (click)=\"closed.emit()\"></button>\n @if (currentData()?.isRefundable) {\n <button pButton\n styleClass=\"sbd-cancel__btn-primary\"\n [label]=\"isCreditShell() ? 'Continue Cancellation' : 'Select refund option'\"\n [disabled]=\"!canConfirm()\" (click)=\"requestCancel()\"></button>\n }\n } @else {\n <button pButton label=\"Close\" styleClass=\"p-button-outlined p-button-secondary\" (click)=\"closed.emit()\"></button>\n }\n </div>\n }\n\n <!-- \u2500\u2500 Result overlay: floating modal over the dimmed refund-method screen (scoped to this dialog, not the whole viewport) \u2500\u2500 -->\n @if (cancelResult()) {\n <div class=\"sbd-cancel__result-overlay\">\n <div class=\"sbd-cancel__result-backdrop\"></div>\n <div class=\"sbd-cancel__result-modal\">\n <div class=\"sbd-cancel__result-hero\">\n <span class=\"sbd-cancel__result-icon sbd-cancel__result-icon--green\">\n <i class=\"pi pi-check\"></i>\n </span>\n <h3 class=\"sbd-cancel__result-title\">{{ confirmIsNilRefund() ? 'Cancel Confirmed' : 'Refund Confirmed' }}</h3>\n </div>\n\n @for (r of confirmRefunds(); track r.label) {\n <div class=\"sbd-cancel__result-row\">\n <span class=\"sbd-cancel__method-icon\">\n <i class=\"pi\" [ngClass]=\"isWalletDestinationRow(r) ? 'pi-wallet' : 'pi-credit-card'\"></i>\n </span>\n <span class=\"sbd-cancel__result-body-text\">\n <span class=\"sbd-cancel__method-name\">{{ r.label }}</span>\n @if (r.subRows?.length) {\n <span class=\"sbd-cancel__result-sub-rows\">\n @for (sub of r.subRows; track sub.label) {\n <span class=\"sbd-cancel__result-sub-row\">\n <span>{{ sub.label }}</span>\n <strong>{{ fmtResult(sub.value) }}</strong>\n </span>\n }\n </span>\n }\n <span class=\"sbd-cancel__method-badge\"\n [ngClass]=\"isWalletDestinationRow(r) ? 'sbd-cancel__method-badge--instant' : 'sbd-cancel__method-badge--days'\">\n {{ isWalletDestinationRow(r) ? 'Ready Now' : (r.key === 'totalCardRefundAmount' ? confirmCardBadgeText() : 'Up to 15 business days, bank policy') }}\n </span>\n @if (r.hasLoyaltyNote) {\n <span class=\"sbd-cancel__notice sbd-cancel__notice--info\">\n <i class=\"pi pi-info-circle\"></i>\n <span>External loyalty points are refunded to your almatar Wallet as permanent points and ready to use in your next booking.</span>\n </span>\n }\n </span>\n <strong class=\"sbd-cancel__result-amount\">{{ fmtResult(r.value) }}</strong>\n </div>\n }\n\n @if (confirmCreditShellNote()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--credit\">\n <i class=\"pi pi-wallet\"></i>\n <span>\u2139\uFE0F {{ confirmCreditShellNote() }}</span>\n </div>\n }\n\n <button pButton styleClass=\"sbd-cancel__btn-primary sbd-cancel__result-done\" label=\"Close\" (click)=\"closed.emit()\"></button>\n </div>\n </div>\n }\n\n </div>\n\n</p-dialog>\n\n<ng-template #madaLogo>\n <svg width=\"43\" height=\"15\" viewBox=\"0 0 43 15\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <g clip-path=\"url(#clip0_sbd_mada)\">\n <path d=\"M18.1894 8.6543H0V14.9933H18.1894V8.6543Z\" fill=\"#84B740\" />\n <path d=\"M18.1894 0H0V6.34463H18.1894V0Z\" fill=\"#259BD6\" />\n <path\n d=\"M36.3779 13.7034L36.2969 13.7203C36.016 13.7768 35.9134 13.7994 35.7082 13.7994C35.233 13.7994 34.6713 13.5452 34.6713 12.3475C34.6713 11.7316 34.7685 10.9124 35.6542 10.9124H35.6596C35.8108 10.9237 35.9836 10.9407 36.3077 11.0424L36.3779 11.065V13.7034ZM36.5237 7.73164L36.3779 7.75989V9.97458L36.2483 9.93503L36.2105 9.92373C36.0647 9.87853 35.7298 9.77684 35.4058 9.77684C33.6344 9.77684 33.2617 11.178 33.2617 12.3531C33.2617 13.9633 34.1258 14.8898 35.6326 14.8898C36.2699 14.8898 36.7397 14.822 37.215 14.6582C37.6524 14.5113 37.8091 14.3023 37.8091 13.8559V7.5C37.3878 7.5791 36.9504 7.65819 36.5237 7.73164Z\"\n fill=\"#747583\"\n />\n <path\n d=\"M41.651 13.7385L41.5754 13.7611L41.3053 13.8345C41.0515 13.9023 40.8247 13.9419 40.6519 13.9419C40.236 13.9419 39.9876 13.7272 39.9876 13.36C39.9876 13.1227 40.0902 12.7215 40.7707 12.7215H41.651V13.7385ZM41.0299 9.74414C40.4844 9.74414 39.9228 9.84584 39.2261 10.0718L38.7724 10.2131L38.9236 11.2865L39.3665 11.134C39.831 10.9758 40.4088 10.8741 40.8409 10.8741C41.0353 10.8741 41.6294 10.8741 41.6294 11.5464V11.8402H40.8031C39.2963 11.8402 38.5996 12.343 38.5996 13.4221C38.5996 14.343 39.2423 14.8967 40.3224 14.8967C40.6573 14.8967 41.1217 14.8289 41.5214 14.7272L41.543 14.7215L41.5646 14.7272L41.6996 14.7498C42.1208 14.8289 42.5583 14.908 42.9849 14.9927V11.4673C42.9849 10.3261 42.3261 9.74414 41.0299 9.74414Z\"\n fill=\"#747583\"\n />\n <path\n d=\"M31.1527 13.7385L31.0771 13.7611L30.807 13.8345C30.5532 13.9023 30.3318 13.9419 30.1536 13.9419C29.7377 13.9419 29.4893 13.7272 29.4893 13.36C29.4893 13.1227 29.5919 12.7215 30.267 12.7215H31.1473L31.1527 13.7385ZM30.537 9.74414C29.9861 9.74414 29.4299 9.84584 28.7332 10.0718L28.2795 10.2131L28.4308 11.2865L28.8736 11.134C29.3381 10.9758 29.9159 10.8741 30.348 10.8741C30.5424 10.8741 31.1365 10.8741 31.1365 11.5464V11.8402H30.3102C28.8034 11.8402 28.1013 12.343 28.1013 13.4221C28.1013 14.343 28.744 14.8967 29.8295 14.8967C30.1644 14.8967 30.6288 14.8289 31.0285 14.7272L31.0501 14.7215L31.0717 14.7272L31.2013 14.7498C31.6279 14.8289 32.06 14.908 32.4866 14.9984V11.473C32.492 10.3204 31.8332 9.74414 30.537 9.74414Z\"\n fill=\"#747583\"\n />\n <path\n d=\"M25.4701 9.7576C24.7842 9.7576 24.2172 9.99489 24.0065 10.0966L23.9525 10.1248L23.9039 10.0853C23.6123 9.86495 23.1856 9.75195 22.5916 9.75195C22.0677 9.75195 21.5762 9.83105 21.0416 9.99489C20.5825 10.1418 20.4043 10.3734 20.4043 10.8085V14.8367H21.8409V11.1135L21.9111 11.0909C22.2027 10.9892 22.3755 10.9723 22.543 10.9723C22.9588 10.9723 23.1694 11.2039 23.1694 11.6559V14.8423H24.5844V11.5938C24.5844 11.4017 24.5466 11.2887 24.5412 11.2661L24.4926 11.17L24.5898 11.1248C24.8058 11.0231 25.0435 10.9723 25.2919 10.9723C25.5781 10.9723 25.9184 11.0909 25.9184 11.6559V14.8423H27.3279V11.509C27.3279 10.3282 26.7231 9.7576 25.4701 9.7576Z\"\n fill=\"#747583\"\n />\n <path\n d=\"M40.5866 4.13559C40.3759 4.13559 40.0249 4.113 39.7495 4.0565L39.6684 4.03955V1.86441C39.6684 1.68362 39.636 1.57062 39.6306 1.55367L39.5874 1.46328L39.6793 1.42373C39.7009 1.41243 39.7225 1.40678 39.7495 1.39548L39.7657 1.38418C39.7981 1.37288 39.8305 1.36158 39.8629 1.35028C39.8791 1.34463 39.8899 1.33898 39.9007 1.33898C40.2193 1.24859 40.511 1.25989 40.6406 1.24859H40.646C41.5263 1.24859 41.6289 2.0678 41.6289 2.68362C41.6235 3.88136 41.0564 4.13559 40.5866 4.13559ZM40.5812 0C40.5704 0 40.5542 0 40.5434 0C39.7171 0 38.8692 0.237288 38.5667 0.700565C38.4047 0.926554 38.3129 1.20904 38.3075 1.54237V3.78531C38.3075 3.9774 38.2697 4.05085 38.2643 4.0678L38.2157 4.16384H35.6072V2.60452H35.6018C35.5694 0.960452 34.6404 0.0564972 33.2957 0.0564972H33.1391H31.9833C31.9293 0.457627 31.8861 0.740113 31.8321 1.14124H33.1391C33.8249 1.14124 34.1868 1.75141 34.1868 2.68927V4.25989L34.095 4.20904C34.0788 4.20339 33.9654 4.16384 33.7871 4.16384H31.5297C31.4865 4.46328 31.4325 4.85311 31.373 5.24294H38.3129C38.5505 5.19209 38.8259 5.14689 39.0636 5.10735C39.4146 5.28814 40.0681 5.38418 40.5164 5.38418C42.0231 5.38418 43.0007 4.32768 43.0007 2.70057C42.9953 1.0904 42.0501 0.0338983 40.5812 0Z\"\n fill=\"#747583\"\n />\n <path\n d=\"M28.4189 5.90821H28.4837C29.9905 5.90821 30.6925 5.38844 30.6925 4.10595C30.6925 3.18505 30.0499 2.45059 28.9697 2.45059H27.5818C27.1659 2.45059 26.9175 2.202 26.9175 1.78392C26.9175 1.50143 27.0201 1.15115 27.7006 1.15115H30.7357C30.8006 0.738723 30.833 0.478836 30.8924 0.0664062H27.7384C26.2694 0.0664062 25.5295 0.710474 25.5295 1.78392C25.5295 2.84607 26.1722 3.39974 27.2523 3.39974H28.6403C29.0561 3.39974 29.3046 3.74437 29.3046 4.10595C29.3046 4.34324 29.202 4.83477 28.5269 4.83477H28.2947L23.8499 4.82347H23.0398C22.3539 4.82347 21.8733 4.41669 21.8733 3.47319V2.82347C21.8733 1.84042 22.2459 1.23025 23.0398 1.23025H24.3576C24.417 0.812169 24.4548 0.546632 24.5088 0.151152H22.867H22.7104C21.3656 0.151152 20.4367 1.09466 20.4043 2.73872V2.80087V3.47319C20.4367 5.11725 21.3656 5.90256 22.7104 5.90256H22.867H24.0227L26.4314 5.90821H27.868H28.4189Z\"\n fill=\"#747583\"\n />\n </g>\n <defs>\n <clipPath id=\"clip0_sbd_mada\">\n <rect width=\"43\" height=\"15\" fill=\"white\" />\n </clipPath>\n </defs>\n </svg>\n</ng-template>\n", styles: [":host ::ng-deep .sbd-cancel-dialog-wrapper{border-radius:16px;overflow:hidden;box-shadow:0 24px 60px #0f172a38}:host ::ng-deep .sbd-cancel-dialog-wrapper .p-dialog-content{padding:0;border-radius:16px;overflow-y:auto;overflow-x:hidden}.sbd-cancel{font-family:inherit;border-radius:16px;overflow:visible;position:relative}.sbd-cancel__loading{padding:24px}.sbd-cancel__loading-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.sbd-cancel__loading-rows{display:flex;flex-direction:column;gap:10px;margin-top:20px}.sbd-cancel__loading-row{display:flex;justify-content:space-between}.sbd-cancel__are-you-sure{padding:36px 24px 28px;display:flex;flex-direction:column;align-items:center;gap:14px;text-align:center;border-top:1px solid #e2e8f0}.sbd-cancel__ays-icon{width:56px;height:56px;border-radius:50%;background:#fff7ed;border:2px solid #fed7aa;display:flex;align-items:center;justify-content:center}.sbd-cancel__ays-icon .pi{font-size:24px;color:#f97316}.sbd-cancel__ays-icon--warn{background:#fef2f2;border-color:#fecaca}.sbd-cancel__ays-icon--warn .pi{color:#ef4444}.sbd-cancel__ays-hint{font-size:13px;color:#64748b;min-height:20px;animation:sbd-hint-fade .35s ease-out}.sbd-cancel__ays-dots{display:flex;gap:6px}.sbd-cancel__ays-dots span{width:7px;height:7px;border-radius:50%;background:#f97316;opacity:.3;animation:sbd-dot-pulse 1.2s ease-in-out infinite}.sbd-cancel__ays-dots span:nth-child(2){animation-delay:.2s}.sbd-cancel__ays-dots span:nth-child(3){animation-delay:.4s}@keyframes sbd-hint-fade{0%{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes sbd-dot-pulse{0%,to{opacity:.3;transform:scale(1)}50%{opacity:1;transform:scale(1.4)}}.sbd-cancel__header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #e2e8f0;background:#f8fafc}.sbd-cancel__header-left{display:flex;align-items:center;gap:10px}.sbd-cancel__header-icon{font-size:18px;color:#475569}.sbd-cancel__header-title{font-size:15px;font-weight:700;color:#1e2e4a}.sbd-cancel__header--stack{align-items:flex-start}.sbd-cancel__header-titles{display:flex;flex-direction:column;gap:2px}.sbd-cancel__header-sub{font-size:12.5px;font-weight:600;color:#94a3b8;font-variant-numeric:tabular-nums}.sbd-cancel__badge{font-size:11.5px;font-weight:700;border-radius:20px;padding:3px 12px;letter-spacing:.03em;width:max-content;text-transform:uppercase}.sbd-cancel__badge--green{background:#dcfce7;color:#166534}.sbd-cancel__badge--red{background:#fee2e2;color:#991b1b}.sbd-cancel__badge--orange{background:#fff7ed;color:#9a3412}.sbd-cancel__badge--outline{background:transparent;box-shadow:inset 0 0 0 1.5px currentColor;padding:4px 14px}.sbd-cancel__badge--outline.sbd-cancel__badge--green{color:#16a34a}.sbd-cancel__badge--outline.sbd-cancel__badge--red{color:#dc2626}.sbd-cancel__badge--muted{background:#f8fafc;color:#64748b;box-shadow:inset 0 0 0 1px #e2e8f0}.sbd-cancel__close-btn{width:30px;height:30px;border-radius:8px;border:none;background:transparent;color:#64748b;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s,color .15s}.sbd-cancel__close-btn .pi{font-size:14px}.sbd-cancel__close-btn:hover{background:#e2e8f0;color:#1e2e4a}.sbd-cancel__close-btn--floating{position:absolute;top:12px;inset-inline-end:12px}.sbd-cancel__summary{background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.sbd-cancel__summary-row{display:flex;align-items:center;justify-content:space-between;font-size:13.5px}.sbd-cancel__summary-row strong{font-size:15px;font-weight:800;color:#1e2e4a;font-variant-numeric:tabular-nums}.sbd-cancel__summary-row--refund .sbd-cancel__summary-label,.sbd-cancel__summary-row--refund strong{color:#16a34a}.sbd-cancel__summary-row--toggle{cursor:pointer}.sbd-cancel__summary-label{display:inline-flex;align-items:center;gap:8px;font-weight:600;color:#475569}.sbd-cancel__summary-label .pi{font-size:13px}.sbd-cancel__summary-value{display:flex;align-items:center;gap:4px}.sbd-cancel__summary-breakdown{display:flex;flex-direction:column;gap:6px;padding:2px 0 2px 22px}.sbd-cancel__summary-breakdown-row{display:flex;align-items:center;justify-content:space-between;font-size:12.5px;color:#64748b}.sbd-cancel__summary-breakdown-row span:last-child{font-variant-numeric:tabular-nums;font-weight:600;color:#475569}.sbd-cancel__section-title{font-size:14px;font-weight:700;color:#1e2e4a}.sbd-cancel__refund-card{border:1px solid #e2e8f0;border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:4px}.sbd-cancel__refund-card-row{display:flex;align-items:center;justify-content:space-between;margin-top:10px;font-size:14px}.sbd-cancel__refund-card-row span{color:#334155;font-weight:600}.sbd-cancel__refund-card-row strong{color:#1e2e4a;font-weight:700;font-variant-numeric:tabular-nums}.sbd-cancel__refund-card-divider{border-top:1px solid #e2e8f0;margin:10px 0}.sbd-cancel__refund-card-divider--dashed{border-top-style:dashed}.sbd-cancel__refund-card-line{display:flex;align-items:center;justify-content:space-between;padding:5px 0;font-size:13px}.sbd-cancel__refund-card-line span:first-child{color:#64748b;display:inline-flex;align-items:center}.sbd-cancel__refund-card-line--neg{color:#64748b;font-weight:600;font-variant-numeric:tabular-nums}.sbd-cancel__refund-card-total{display:flex;align-items:center;justify-content:space-between}.sbd-cancel__refund-card-total span{font-size:14.5px;font-weight:700;color:#1e2e4a}.sbd-cancel__refund-card-total strong{font-size:17px;font-weight:800;color:#1e2e4a;font-variant-numeric:tabular-nums}.sbd-cancel__btn-keep{flex:1 1 0}.sbd-cancel__btn-primary{flex:1 1 0;background:#6d4aff!important;border-color:#6d4aff!important;color:#fff!important}.sbd-cancel__btn-primary:hover:not(:disabled){background:#5c3ce6!important;border-color:#5c3ce6!important}.sbd-cancel__btn-primary:disabled{opacity:.5}.sbd-cancel__paid{display:flex;align-items:center;justify-content:space-between;margin:0 -20px;padding:14px 20px;border-radius:0;background:#f1f0fb;border-top:1px solid #e7e5f7;border-bottom:1px solid #e7e5f7}.sbd-cancel__paid span{font-size:13.5px;font-weight:600;color:#4b4670}.sbd-cancel__paid strong{font-size:18px;font-weight:800;color:#2b2747;font-variant-numeric:tabular-nums;letter-spacing:.01em}.sbd-cancel__group{display:flex;flex-direction:column}.sbd-cancel__group-label{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.07em;margin-bottom:2px}.sbd-cancel__group-label--deduct{color:#e05656}.sbd-cancel__group-label--refund{color:#16a34a}.sbd-cancel__line{display:flex;justify-content:space-between;align-items:center;padding:11px 0;font-size:13.5px;border-bottom:1px dashed #e5e7eb}.sbd-cancel__line:last-child{border-bottom:none}.sbd-cancel__line-label{display:inline-flex;align-items:center;color:#334155}.sbd-cancel__line-val{font-weight:700;font-variant-numeric:tabular-nums}.sbd-cancel__line-val--neg{color:#dc2626}.sbd-cancel__line-val--pos{color:#16a34a}.sbd-cancel__tag{margin-left:8px;font-size:10px;font-weight:700;line-height:1;padding:3px 8px;border-radius:6px;background:#ede9fe;color:#6d28d9;letter-spacing:.02em}.sbd-cancel__total-deduct{display:flex;align-items:center;justify-content:space-between;padding:13px 16px;border-radius:10px;background:#fdf2f2;border:1px solid #f6c9c9}.sbd-cancel__total-deduct span{font-size:14px;font-weight:700;color:#b3344a}.sbd-cancel__total-deduct strong{font-size:15px;font-weight:800;color:#dc2626;font-variant-numeric:tabular-nums}.sbd-cancel__calc{display:flex;align-items:center;gap:12px;margin:2px 0;color:#94a3b8;font-size:13px;font-variant-numeric:tabular-nums}.sbd-cancel__calc:before,.sbd-cancel__calc:after{content:\"\";flex:1;height:1px;background:#e5e7eb}.sbd-cancel__refund-total{display:flex;align-items:center;justify-content:space-between}.sbd-cancel__refund-total span{font-size:16px;font-weight:800;color:#1e2e4a}.sbd-cancel__refund-total strong{font-size:26px;font-weight:800;color:#16a34a;font-variant-numeric:tabular-nums;letter-spacing:.01em}.sbd-cancel__formula{text-align:center;padding:14px 16px;border-radius:10px;background:#1f2438;color:#f1f5f9;font-family:SFMono-Regular,ui-monospace,Menlo,Consolas,monospace;font-size:14px;font-weight:600;letter-spacing:.04em;font-variant-numeric:tabular-nums}.sbd-cancel__discrepancy{display:flex;align-items:flex-start;gap:10px;padding:11px 13px;border-radius:8px;font-size:12.5px;line-height:1.45;background:#fffbeb;border:1px solid #fde68a;color:#92400e}.sbd-cancel__discrepancy i{flex-shrink:0;margin-top:1px;font-size:14px;color:#d97706}.sbd-cancel__body{padding:16px 20px;display:flex;flex-direction:column;gap:12px}.sbd-cancel__leg-card{width:100%;border-radius:12px;border:1px solid #e2e8f0;background:#fff;overflow:hidden}.sbd-cancel__leg-card-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:14px 16px;cursor:pointer}.sbd-cancel__leg-checkbox{display:flex;align-items:center;gap:10px;font-size:13.5px;font-weight:700;color:#1e2e4a;cursor:pointer}.sbd-cancel__leg-checkbox input[type=checkbox]{width:17px;height:17px;accent-color:#6d4aff;cursor:pointer}.sbd-cancel__leg-chevron{flex-shrink:0;width:26px;height:26px;display:flex;align-items:center;justify-content:center;border:none;background:transparent;color:#64748b;cursor:pointer;transition:transform .15s}.sbd-cancel__leg-chevron i{font-size:13px}.sbd-cancel__leg-chevron--collapsed{transform:rotate(180deg)}.sbd-cancel__leg-card-body{display:flex;flex-direction:column;gap:4px;padding:0 16px 16px}.sbd-cancel__leg-card-body--excluded{opacity:.5}.sbd-cancel__notice{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-radius:8px;font-size:12.5px}.sbd-cancel__notice i{flex-shrink:0;margin-top:1px;font-size:14px}.sbd-cancel__notice p{margin:2px 0 0;font-size:12px;opacity:.8}.sbd-cancel__notice strong{display:block;font-weight:700}.sbd-cancel__notice--warning{background:#fff7ed;border:1px solid #fed7aa;color:#9a3412}.sbd-cancel__notice--warning i{color:#f97316}.sbd-cancel__notice--info{background:#eff6ff;border:1px solid #bfdbfe;color:#1e40af}.sbd-cancel__notice--info i{color:#3b82f6}.sbd-cancel__notice--neutral{background:#f7f7f8;border:1px solid #e5e7eb;color:#111827}.sbd-cancel__notice--neutral i{color:#111827}.sbd-cancel__notice--tip{background:#f0fdf4;border:1px solid #bbf7d0;color:#14532d}.sbd-cancel__notice--tip i{color:#22c55e}.sbd-cancel__notice--success{background:#f0fdf4;border:1px solid #bbf7d0;color:#15803c}.sbd-cancel__notice--success i{color:#16a34a}.sbd-cancel__notice--credit{background:#faf5ff;border:1px solid #e9d5ff;color:#5b21b6;font-size:12px}.sbd-cancel__notice--credit i{color:#a855f7}.sbd-cancel__notice--shield{background:#eff6ff;border:1px solid #bfdbfe;color:#1e40af}.sbd-cancel__notice--shield strong{display:inline;font-weight:700}.sbd-cancel__notice--shield i{color:#3b82f6}.sbd-cancel__source-selector{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:4px 0}.sbd-cancel__source-label{font-size:11px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;margin-right:2px}.sbd-cancel__source-chip{font-size:12px;font-weight:600;padding:4px 14px;border-radius:20px;border:1.5px solid #e2e8f0;background:#f8fafc;color:#475569;cursor:pointer;transition:border-color .15s,color .15s,background .15s;display:inline-flex;align-items:center}.sbd-cancel__source-chip:hover{border-color:#94a3b8;color:#475569;background:#f1f5f9}.sbd-cancel__source-chip--active{background:#eff6ff;color:#1d4ed8;border-color:#93c5fd}.sbd-cancel__source-chip--active:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}.sbd-cancel__source-chip--no-refund{border-color:#fecaca;color:#b91c1c;background:#fff5f5}.sbd-cancel__source-chip--no-refund:hover{border-color:#fca5a5;background:#fee2e2;color:#b91c1c}.sbd-cancel__source-chip--active.sbd-cancel__source-chip--no-refund{background:#fee2e2;color:#991b1b;border-color:#fca5a5}.sbd-cancel__source-chip--active.sbd-cancel__source-chip--no-refund:hover{background:#fecaca}.sbd-cancel__footer{display:flex;justify-content:flex-end;gap:10px;align-items:stretch;padding:14px 20px;border-top:1px solid #e2e8f0;background:#fff}.sbd-cancel__btn-danger-pill{flex:1 1 0;border-radius:999px!important;background:#fee2e2!important;border-color:#fee2e2!important;color:#dc2626!important;font-weight:700!important}.sbd-cancel__btn-danger-pill:hover:not(:disabled){background:#fecaca!important;border-color:#fecaca!important}.sbd-cancel__btn-danger-pill:disabled{opacity:.5}.sbd-cancel__method-header{display:flex;align-items:flex-start;gap:12px;padding:20px 20px 0}.sbd-cancel__method-close{flex-shrink:0;width:32px;height:32px;border-radius:50%;background:#f8fafc;border:none;color:#1e2e4a;display:flex;align-items:center;justify-content:center;cursor:pointer}.sbd-cancel__method-close .pi{font-size:13px}.sbd-cancel__method-close:hover{background:#e2e8f0}.sbd-cancel__method-header-text{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;text-align:center;margin-right:32px}.sbd-cancel__method-title{font-size:19px;font-weight:800;color:#1e2e4a;margin:0}.sbd-cancel__method-sub{font-size:12.5px;color:#94a3b8}.sbd-cancel__method-body{padding:16px 20px 20px;display:flex;flex-direction:column;gap:14px}.sbd-cancel__select-leg-body{padding:16px 20px 20px;display:flex;flex-direction:column;gap:12px}.sbd-cancel__leg-select-card{position:relative;border:1.5px solid #e2e8f0;border-radius:16px;padding:16px 16px 16px 48px;transition:border-color .15s}.sbd-cancel__leg-select-card--selected:not(.sbd-cancel__leg-select-card--frozen){border-color:#6d4aff}.sbd-cancel__leg-select-card--disabled{opacity:.7}.sbd-cancel__leg-select-card--frozen{opacity:.6;border-color:#e2e8f0}.sbd-cancel__leg-select-lock{position:absolute;top:16px;left:16px;width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:14px;color:#94a3b8}.sbd-cancel__single-leg-note{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:12px;background:#eaf2ff;border:1px solid #b9d6ff;color:#1a5fd6;font-size:13px;line-height:1.5}.sbd-cancel__single-leg-note i{flex-shrink:0;margin-top:1px}.sbd-cancel__all-non-refundable-note{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:12px;background:#fdf3e3;border:1px solid #f3d9a4;color:#8a5a1e;font-size:13px;line-height:1.5}.sbd-cancel__all-non-refundable-note i{flex-shrink:0;margin-top:1px;color:#b98a3e}.sbd-cancel__leg-select-checkbox{position:absolute;top:16px;left:16px;width:22px;height:22px;cursor:pointer}.sbd-cancel__leg-select-checkbox input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}.sbd-cancel__leg-select-checkbox input:disabled{cursor:not-allowed}.sbd-cancel__leg-select-icon{display:flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;border:1.5px solid #cbd5e1;background:#fff;color:#fff;font-size:12px}input:checked+.sbd-cancel__leg-select-icon{background:#6d4aff;border-color:#6d4aff}input:disabled+.sbd-cancel__leg-select-icon{background:#f1f5f9;border-color:#e2e8f0}.sbd-cancel__leg-select-content{display:flex;flex-direction:column;gap:8px}.sbd-cancel__leg-select-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.sbd-cancel__leg-select-name{font-size:15px;font-weight:700;color:#1e2e4a}.sbd-cancel__leg-select-date{font-size:13px;color:#64748b;margin-top:2px}.sbd-cancel__leg-select-times{display:flex;align-items:center;gap:8px}.sbd-cancel__leg-select-time{font-size:18px;font-weight:700;color:#1e2e4a;font-variant-numeric:tabular-nums}.sbd-cancel__leg-select-line{flex:1;position:relative;display:flex;flex-direction:column;align-items:center;gap:2px;padding:0 16px 0 8px}.sbd-cancel__leg-select-line:before{content:\"\";position:absolute;left:0;right:14px;top:50%;height:2px;background:#cbd5e1;z-index:1}.sbd-cancel__leg-select-line-stops{font-size:12px;color:#64748b}.sbd-cancel__leg-select-line-dur{font-size:11px;color:#94a3b8}.sbd-cancel__leg-select-line-arrow{position:absolute;right:4px;top:51%;transform:translateY(-50%);font-size:11px;color:#64748b}.sbd-cancel__leg-select-footer{display:flex;align-items:center;gap:8px}.sbd-cancel__leg-select-airline{font-size:12.5px;color:#64748b}.sbd-cancel__leg-select-operated-by{font-size:12px;color:#94a3b8}.sbd-cancel__leg-select-chevron{font-size:12px;color:#94a3b8;margin-inline-start:auto}.sbd-cancel__leg-select-hint{text-align:center;padding:8px 20px 0;font-size:13px;color:#94a3b8}.sbd-cancel__leg-select-notice{display:flex;align-items:flex-start;gap:8px;padding:10px 12px;border-radius:10px;background:#fdf1e0;color:#6b4a16;font-size:12.5px;line-height:1.4}.sbd-cancel__leg-select-notice i{flex-shrink:0;margin-top:1px;color:#b98a3e}.sbd-cancel__method-amount{display:flex;align-items:center;justify-content:space-between}.sbd-cancel__method-amount span{font-size:14px;color:#1e2e4a}.sbd-cancel__method-amount strong{font-size:22px;font-weight:800;color:#1e2e4a;font-variant-numeric:tabular-nums}.sbd-cancel__method-option{display:flex;align-items:flex-start;gap:12px;width:100%;text-align:left;padding:16px;border-radius:16px;border:1.5px solid #e2e8f0;background:#fff;cursor:pointer;transition:border-color .15s,box-shadow .15s}.sbd-cancel__method-option--active{border-color:#6d4aff;box-shadow:0 0 0 1px #6d4aff}.sbd-cancel__method-option:disabled{opacity:.5;cursor:not-allowed}.sbd-cancel__method-icon{flex-shrink:0;width:44px;height:44px;border-radius:10px;display:flex;align-items:center;justify-content:center;border:1px solid #e2e8f0;color:#1e2e4a}.sbd-cancel__method-icon .pi{font-size:16px}.sbd-cancel__method-body-text{flex:1;display:flex;flex-direction:column;align-items:flex-start;gap:4px}.sbd-cancel__method-name{font-size:16px;font-weight:700;color:#1e2e4a;display:inline-flex;align-items:center;gap:6px}.sbd-cancel__method-desc{font-size:13px;color:#64748b}.sbd-cancel__method-badge{display:inline-flex;align-items:center;gap:5px;margin-top:4px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;padding:4px 10px;border-radius:20px}.sbd-cancel__method-badge svg{flex-shrink:0}.sbd-cancel__method-badge--instant{background:#dcfce7;color:#166534}.sbd-cancel__method-badge--days,.sbd-cancel__method-badge--loyalty{background:#eff6ff;color:#1e40af}.sbd-cancel__option-end{flex-shrink:0;display:flex;align-items:center;gap:8px}.sbd-cancel__method-radio{flex-shrink:0;width:18px;height:18px;margin-top:2px;border-radius:50%;border:2px solid #7c3aed;position:relative}.sbd-cancel__method-option--active .sbd-cancel__method-radio{border-color:#6d4aff}.sbd-cancel__method-option--active .sbd-cancel__method-radio:after{content:\"\";position:absolute;inset:3px;border-radius:50%;background:#6d4aff}.sbd-cancel__method-option--split{flex-wrap:wrap}.sbd-cancel__method-split-total{flex-shrink:0;font-size:16px;font-weight:700;color:#1e2e4a;font-variant-numeric:tabular-nums}.sbd-cancel__method-split-breakdown{flex-basis:100%;display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px;margin-top:12px;padding-top:12px;border-top:1px solid #e2e8f0}.sbd-cancel__method-split-row{flex:1 1 auto;display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;font-size:14px;color:#1e2e4a}.sbd-cancel__method-split-row strong{font-weight:700;font-variant-numeric:tabular-nums}.sbd-cancel__method-split-label{color:#64748b;display:inline-flex;align-items:center;gap:6px}.sbd-cancel__mada-logo{display:inline-flex;align-items:center;flex-shrink:0}.sbd-cancel__mada-logo svg{display:block}.sbd-cancel__result-hero{display:flex;flex-direction:column;align-items:center;gap:18px;padding:28px 0 8px}.sbd-cancel__result-icon{width:84px;height:84px;border-radius:50%;display:flex;align-items:center;justify-content:center}.sbd-cancel__result-icon .pi{font-size:30px;color:#fff}.sbd-cancel__result-icon--green{background:#4ade80}.sbd-cancel__result-icon--orange{background:#fb923c}.sbd-cancel__result-title{font-size:19px;font-weight:800;color:#1e2e4a;margin:0;text-align:center}.sbd-cancel__result-row{display:flex;align-items:flex-start;gap:12px;width:100%;padding:12px 14px;border-radius:14px;background:#f8fafc}.sbd-cancel__result-body-text{flex:1;min-width:0;display:flex;flex-direction:column;align-items:flex-start;gap:5px}.sbd-cancel__result-body-text .sbd-cancel__method-badge{text-transform:none;letter-spacing:normal;line-height:1.35;white-space:normal}.sbd-cancel__result-sub-rows{display:flex;flex-direction:column;gap:2px;width:100%}.sbd-cancel__result-sub-row{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;font-size:12.5px;color:#64748b}.sbd-cancel__result-sub-row strong{font-weight:700;color:#64748b;font-variant-numeric:tabular-nums}.sbd-cancel__result-amount{font-size:16px;font-weight:800;color:#1e2e4a;font-variant-numeric:tabular-nums;white-space:nowrap}.sbd-cancel__result-overlay{position:absolute;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:24px}.sbd-cancel__result-backdrop{position:absolute;inset:0;background:#0000008c}.sbd-cancel__result-modal{position:relative;width:100%;max-width:400px;max-height:90%;overflow-y:auto;display:flex;flex-direction:column;gap:16px;padding:24px 20px;border-radius:20px;background:#fff;box-shadow:0 24px 60px #00000059}.sbd-cancel__result-done{width:100%}\n"], dependencies: [{ kind: "ngmodule", type: DialogModule }, { kind: "component", type: i1$1.Dialog, selector: "p-dialog", inputs: ["header", "draggable", "resizable", "positionLeft", "positionTop", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "responsive", "appendTo", "breakpoints", "styleClass", "maskStyleClass", "showHeader", "breakpoint", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "visible", "style", "position"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3$1.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading"] }, { kind: "ngmodule", type: SkeletonModule }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); }
|
|
2579
|
+
}
|
|
2580
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdCancelDialogComponent, decorators: [{
|
|
2581
|
+
type: Component,
|
|
2582
|
+
args: [{ selector: 'app-sbd-cancel-dialog', standalone: true, imports: [DialogModule, ButtonModule, SkeletonModule, NgTemplateOutlet, NgClass], template: "<p-dialog [visible]=\"visible()\" (visibleChange)=\"$event ? null : closed.emit()\"\n [modal]=\"true\" [blockScroll]=\"true\" [draggable]=\"false\" [resizable]=\"false\"\n [showHeader]=\"false\" [style]=\"{ width: '560px', padding: '0' }\" styleClass=\"sbd-cancel-dialog-wrapper\">\n\n <div class=\"sbd-cancel\">\n\n <!-- Loading refund details \u2014 shows cycling hints -->\n @if (loading()) {\n <div class=\"sbd-cancel__are-you-sure\">\n <button type=\"button\" class=\"sbd-cancel__close-btn sbd-cancel__close-btn--floating\" (click)=\"closed.emit()\" aria-label=\"Close\">\n <i class=\"pi pi-times\"></i>\n </button>\n <div class=\"sbd-cancel__ays-icon\">\n <i class=\"pi pi-spin pi-spinner\"></i>\n </div>\n @for (h of [cancelHint()]; track h) {\n <div class=\"sbd-cancel__ays-hint\">{{ h }}</div>\n }\n <div class=\"sbd-cancel__ays-dots\">\n <span></span><span></span><span></span>\n </div>\n </div>\n }\n\n <!-- Select leg \u2014 shown once the refund preview has fully loaded, before the\n refund breakdown, for round-trip bookings the backend hasn't already\n restricted to a single leg. Same card design as almatar-b2c's\n select-leg screen (checkbox overlay, refundable badge, disclaimer). -->\n @else if (step() === 'select-leg') {\n <div class=\"sbd-cancel__method-header\">\n <button type=\"button\" class=\"sbd-cancel__method-close\" (click)=\"closed.emit()\" aria-label=\"Close\">\n <i class=\"pi pi-times\"></i>\n </button>\n <div class=\"sbd-cancel__method-header-text\">\n <h3 class=\"sbd-cancel__method-title\">Select Leg to Cancel</h3>\n <span class=\"sbd-cancel__method-sub\">Booking ID #{{ bookingId() }}</span>\n </div>\n </div>\n\n <div class=\"sbd-cancel__select-leg-body\">\n @if (forcedLegType(); as forced) {\n <div class=\"sbd-cancel__single-leg-note\">\n <i class=\"pi pi-info-circle\"></i>\n <span>This airline doesn't allow cancelling a single flight \u2014 {{ itineraryLeg(forced)?.from }} to {{ itineraryLeg(forced)?.to }} will be included automatically.</span>\n </div>\n }\n @if (allLegsNonRefundable()) {\n <div class=\"sbd-cancel__all-non-refundable-note\">\n <i class=\"pi pi-exclamation-triangle\"></i>\n <span>Sorry, we're unable to provide a refund for this booking, as both flights are non-refundable based on airline rules.</span>\n </div>\n }\n <!-- Onward -->\n <div class=\"sbd-cancel__leg-select-card\"\n [ngClass]=\"{\n 'sbd-cancel__leg-select-card--selected': onwardChecked() && onwardRefundable(),\n 'sbd-cancel__leg-select-card--disabled': !onwardRefundable(),\n 'sbd-cancel__leg-select-card--frozen': forcedLegType() === 'onward'\n }\">\n @if (forcedLegType() !== 'onward') {\n <label class=\"sbd-cancel__leg-select-checkbox\">\n <input type=\"checkbox\" [checked]=\"onwardChecked()\" [disabled]=\"!onwardRefundable()\"\n (change)=\"toggleLeg('onward')\" />\n <span class=\"sbd-cancel__leg-select-icon\"><i class=\"pi pi-check\"></i></span>\n </label>\n } @else {\n <i class=\"pi pi-lock sbd-cancel__leg-select-lock\"></i>\n }\n <div class=\"sbd-cancel__leg-select-content\">\n @if (itineraryLeg('onward'); as leg) {\n <div class=\"sbd-cancel__leg-select-top\">\n <div>\n <span class=\"sbd-cancel__leg-select-name\">{{ leg.from }} to {{ leg.to }}</span>\n <div class=\"sbd-cancel__leg-select-date\">{{ fmtShortDate(leg.depTime) }}</div>\n </div>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline\"\n [ngClass]=\"onwardRefundable() && forcedLegType() !== 'onward' ? 'sbd-cancel__badge--green' : 'sbd-cancel__badge--muted'\">\n {{ onwardRefundable() ? 'Refundable' : 'Non-Refundable' }}\n </span>\n </div>\n <div class=\"sbd-cancel__leg-select-times\">\n <span class=\"sbd-cancel__leg-select-time\">{{ fmtTime(leg.depTime) }}</span>\n <span class=\"sbd-cancel__leg-select-line\">\n <span class=\"sbd-cancel__leg-select-line-stops\">{{ leg.stops.length > 0 ? (leg.stops.length + ' stop' + (leg.stops.length > 1 ? 's' : '')) : 'Direct' }}</span>\n @if (fmtMins(leg.totalMinutes)) { <span class=\"sbd-cancel__leg-select-line-dur\">{{ fmtMins(leg.totalMinutes) }}</span> }\n <i class=\"pi pi-arrow-right sbd-cancel__leg-select-line-arrow\"></i>\n </span>\n <span class=\"sbd-cancel__leg-select-time\">{{ fmtTime(leg.arrTime) }}</span>\n </div>\n @if (leg.airlineName || leg.cabin) {\n <span class=\"sbd-cancel__leg-select-airline\">\n {{ leg.airlineName }}{{ leg.airlineName && leg.cabin ? ' \u00B7 ' : '' }}{{ leg.cabin }}\n </span>\n }\n <div class=\"sbd-cancel__leg-select-footer\">\n @if (leg.operatedBy.length) {\n <span class=\"sbd-cancel__leg-select-operated-by\">Operated by {{ leg.operatedBy.join(', ') }}</span>\n }\n <!-- <i class=\"pi pi-chevron-down sbd-cancel__leg-select-chevron\"></i> -->\n </div>\n } @else {\n <div class=\"sbd-cancel__leg-select-top\">\n <span class=\"sbd-cancel__leg-select-name\">Onward</span>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline\"\n [ngClass]=\"onwardRefundable() && forcedLegType() !== 'onward' ? 'sbd-cancel__badge--green' : 'sbd-cancel__badge--muted'\">\n {{ onwardRefundable() ? 'Refundable' : 'Non-Refundable' }}\n </span>\n </div>\n }\n @if (!onwardRefundable()) {\n <div class=\"sbd-cancel__leg-select-notice\">\n <i class=\"pi pi-info-circle\"></i>\n <span>This flight type doesn't allow refunds on cancellation, based on the airline's fare rules.</span>\n </div>\n }\n </div>\n </div>\n\n <!-- Return -->\n <div class=\"sbd-cancel__leg-select-card\"\n [ngClass]=\"{\n 'sbd-cancel__leg-select-card--selected': returnChecked() && returnRefundable(),\n 'sbd-cancel__leg-select-card--disabled': !returnRefundable(),\n 'sbd-cancel__leg-select-card--frozen': forcedLegType() === 'return'\n }\">\n @if (forcedLegType() !== 'return') {\n <label class=\"sbd-cancel__leg-select-checkbox\">\n <input type=\"checkbox\" [checked]=\"returnChecked()\" [disabled]=\"!returnRefundable()\"\n (change)=\"toggleLeg('return')\" />\n <span class=\"sbd-cancel__leg-select-icon\"><i class=\"pi pi-check\"></i></span>\n </label>\n } @else {\n <i class=\"pi pi-lock sbd-cancel__leg-select-lock\"></i>\n }\n <div class=\"sbd-cancel__leg-select-content\">\n @if (itineraryLeg('return'); as leg) {\n <div class=\"sbd-cancel__leg-select-top\">\n <div>\n <span class=\"sbd-cancel__leg-select-name\">{{ leg.from }} to {{ leg.to }}</span>\n <div class=\"sbd-cancel__leg-select-date\">{{ fmtShortDate(leg.depTime) }}</div>\n </div>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline\"\n [ngClass]=\"returnRefundable() && forcedLegType() !== 'return' ? 'sbd-cancel__badge--green' : 'sbd-cancel__badge--muted'\">\n {{ returnRefundable() ? 'Refundable' : 'Non-Refundable' }}\n </span>\n </div>\n <div class=\"sbd-cancel__leg-select-times\">\n <span class=\"sbd-cancel__leg-select-time\">{{ fmtTime(leg.depTime) }}</span>\n <span class=\"sbd-cancel__leg-select-line\">\n <span class=\"sbd-cancel__leg-select-line-stops\">{{ leg.stops.length > 0 ? (leg.stops.length + ' stop' + (leg.stops.length > 1 ? 's' : '')) : 'Direct' }}</span>\n @if (fmtMins(leg.totalMinutes)) { <span class=\"sbd-cancel__leg-select-line-dur\">{{ fmtMins(leg.totalMinutes) }}</span> }\n <i class=\"pi pi-arrow-right sbd-cancel__leg-select-line-arrow\"></i>\n </span>\n <span class=\"sbd-cancel__leg-select-time\">{{ fmtTime(leg.arrTime) }}</span>\n </div>\n @if (leg.airlineName || leg.cabin) {\n <span class=\"sbd-cancel__leg-select-airline\">\n {{ leg.airlineName }}{{ leg.airlineName && leg.cabin ? ' \u00B7 ' : '' }}{{ leg.cabin }}\n </span>\n }\n <div class=\"sbd-cancel__leg-select-footer\">\n @if (leg.operatedBy.length) {\n <span class=\"sbd-cancel__leg-select-operated-by\">Operated by {{ leg.operatedBy.join(', ') }}</span>\n }\n <!-- <i class=\"pi pi-chevron-down sbd-cancel__leg-select-chevron\"></i> -->\n </div>\n } @else {\n <div class=\"sbd-cancel__leg-select-top\">\n <span class=\"sbd-cancel__leg-select-name\">Return</span>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline\"\n [ngClass]=\"returnRefundable() && forcedLegType() !== 'return' ? 'sbd-cancel__badge--green' : 'sbd-cancel__badge--muted'\">\n {{ returnRefundable() ? 'Refundable' : 'Non-Refundable' }}\n </span>\n </div>\n }\n @if (!returnRefundable()) {\n <div class=\"sbd-cancel__leg-select-notice\">\n <i class=\"pi pi-info-circle\"></i>\n <span>This flight type doesn't allow refunds on cancellation, based on the airline's fare rules.</span>\n </div>\n }\n </div>\n </div>\n </div>\n\n @if (!allLegsNonRefundable() && !onwardChecked() && !returnChecked()) {\n <div class=\"sbd-cancel__leg-select-hint\">Select at least one flight to continue</div>\n }\n\n <div class=\"sbd-cancel__footer\">\n <button pButton\n styleClass=\"sbd-cancel__btn-primary\"\n label=\"Continue\"\n [disabled]=\"allLegsNonRefundable() || (!onwardChecked() && !returnChecked())\"\n (click)=\"step.set('details')\"></button>\n </div>\n }\n\n <!-- Refund method \u2014 replaces the old are-you-sure + radio picker.\n Stays mounted (dimmed) behind the result overlay once confirmed. -->\n @else if (confirming() || cancelResult()) {\n <div class=\"sbd-cancel__method-header\">\n <button type=\"button\" class=\"sbd-cancel__method-close\" (click)=\"confirming.set(false)\" aria-label=\"Close\">\n <i class=\"pi pi-times\"></i>\n </button>\n <div class=\"sbd-cancel__method-header-text\">\n <h3 class=\"sbd-cancel__method-title\">Refund Method</h3>\n <span class=\"sbd-cancel__method-sub\">Booking ID #{{ bookingId() }}</span>\n </div>\n </div>\n\n <div class=\"sbd-cancel__method-body\">\n <div class=\"sbd-cancel__method-amount\">\n <span>You will get back</span>\n <strong>{{ fmt(adjustedData()?.totalRefundAmount) }}</strong>\n </div>\n\n @if (isCreditShell()) {\n <!-- Credit-shell refunds have exactly one destination \u2014 the airline\n credit shell \u2014 so show it as a read-only card, not a picker. -->\n <div class=\"sbd-cancel__method-option sbd-cancel__method-option--active\">\n <span class=\"sbd-cancel__method-icon\"><i class=\"pi pi-wallet\"></i></span>\n <span class=\"sbd-cancel__method-body-text\">\n <span class=\"sbd-cancel__method-name\">{{ creditShellSupplierName() }} website credit</span>\n <span class=\"sbd-cancel__method-desc\">Your refund will be issued instantly by {{ creditShellSupplierName() }}.</span>\n <span class=\"sbd-cancel__method-badge sbd-cancel__method-badge--instant\">\n <svg width=\"12\" height=\"9\" viewBox=\"0 0 12 9\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M7.31818 0C8.42727 0 9.49093 0.440583 10.2752 1.22483C11.0594 2.00907 11.5 3.07273 11.5 4.18182C11.5 5.2909 11.0594 6.35457 10.2752 7.13881C9.49093 7.92305 8.42727 8.36364 7.31818 8.36364C6.20909 8.36364 5.14543 7.92305 4.36119 7.13881C3.57695 6.35457 3.13636 5.2909 3.13636 4.18182C3.13636 3.07273 3.57695 2.00907 4.36119 1.22483C5.14543 0.440583 6.20909 0 7.31818 0ZM7.31818 1.04545C6.48637 1.04545 5.68862 1.37589 5.10044 1.96407C4.51226 2.55226 4.18182 3.35 4.18182 4.18182C4.18182 5.01363 4.51226 5.81138 5.10044 6.39956C5.68862 6.98774 6.48637 7.31818 7.31818 7.31818C8.15 7.31818 8.94774 6.98774 9.53593 6.39956C10.1241 5.81138 10.4545 5.01363 10.4545 4.18182C10.4545 3.35 10.1241 2.55226 9.53593 1.96407C8.94774 1.37589 8.15 1.04545 7.31818 1.04545ZM6.79545 2.09091H7.57955V4.06682L8.7975 5.28477L8.24341 5.83886L6.79545 4.39091V2.09091ZM0.522727 7.31818C0.384091 7.31818 0.251134 7.26311 0.153103 7.16508C0.0550729 7.06705 0 6.93409 0 6.79545C0 6.65682 0.0550729 6.52386 0.153103 6.42583C0.251134 6.3278 0.384091 6.27273 0.522727 6.27273H2.52477C2.68682 6.64386 2.89591 6.99409 3.13636 7.31818H0.522727ZM1.04545 4.70455C0.906819 4.70455 0.773861 4.64947 0.67583 4.55144C0.5778 4.45341 0.522727 4.32045 0.522727 4.18182C0.522727 4.04318 0.5778 3.91022 0.67583 3.81219C0.773861 3.71416 0.906819 3.65909 1.04545 3.65909H2.11705L2.09091 4.18182L2.11705 4.70455H1.04545ZM1.56818 2.09091C1.42955 2.09091 1.29659 2.03584 1.19856 1.93781C1.10053 1.83978 1.04545 1.70682 1.04545 1.56818C1.04545 1.42955 1.10053 1.29659 1.19856 1.19856C1.29659 1.10053 1.42955 1.04545 1.56818 1.04545H3.13636C2.89591 1.36955 2.68682 1.71977 2.52477 2.09091H1.56818Z\"\n fill=\"#15803C\"\n />\n </svg>\n Instant\n </span>\n </span>\n </div>\n\n <div class=\"sbd-cancel__notice sbd-cancel__notice--credit\">\n <i class=\"pi pi-info-circle\"></i>\n <span>\n Your refund will be issued as airline credit under the same PNR.\n It can be used only for future booking on {{ creditShellSupplierName() }} website.\n </span>\n </div>\n\n <!-- Refundable ancillaries (baggage, seats, \u2026) can independently be\n flagged credit-shell or not \u2014 each renders as its own card,\n matching either the credit-shell style above or the normal\n wallet style, both with the same nested note when credit-shell. -->\n @for (item of creditShellAncillaryItems(); track item.name) {\n <div class=\"sbd-cancel__method-option sbd-cancel__method-option--active\">\n <span class=\"sbd-cancel__method-icon\"><i class=\"pi pi-wallet\"></i></span>\n <span class=\"sbd-cancel__method-body-text\">\n <span class=\"sbd-cancel__method-name\">\n {{ item.isCreditShell ? (creditShellSupplierName() + ' website credit') : 'Almatar Wallet' }}\n \u2014 {{ item.name }}\n </span>\n <span class=\"sbd-cancel__method-desc\">\n @if (item.isCreditShell) {\n Your refund will be issued instantly by {{ creditShellSupplierName() }}.\n } @else {\n Usable for future bookings\n }\n </span>\n <span class=\"sbd-cancel__method-badge sbd-cancel__method-badge--instant\">\n <svg width=\"12\" height=\"9\" viewBox=\"0 0 12 9\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M7.31818 0C8.42727 0 9.49093 0.440583 10.2752 1.22483C11.0594 2.00907 11.5 3.07273 11.5 4.18182C11.5 5.2909 11.0594 6.35457 10.2752 7.13881C9.49093 7.92305 8.42727 8.36364 7.31818 8.36364C6.20909 8.36364 5.14543 7.92305 4.36119 7.13881C3.57695 6.35457 3.13636 5.2909 3.13636 4.18182C3.13636 3.07273 3.57695 2.00907 4.36119 1.22483C5.14543 0.440583 6.20909 0 7.31818 0ZM7.31818 1.04545C6.48637 1.04545 5.68862 1.37589 5.10044 1.96407C4.51226 2.55226 4.18182 3.35 4.18182 4.18182C4.18182 5.01363 4.51226 5.81138 5.10044 6.39956C5.68862 6.98774 6.48637 7.31818 7.31818 7.31818C8.15 7.31818 8.94774 6.98774 9.53593 6.39956C10.1241 5.81138 10.4545 5.01363 10.4545 4.18182C10.4545 3.35 10.1241 2.55226 9.53593 1.96407C8.94774 1.37589 8.15 1.04545 7.31818 1.04545ZM6.79545 2.09091H7.57955V4.06682L8.7975 5.28477L8.24341 5.83886L6.79545 4.39091V2.09091ZM0.522727 7.31818C0.384091 7.31818 0.251134 7.26311 0.153103 7.16508C0.0550729 7.06705 0 6.93409 0 6.79545C0 6.65682 0.0550729 6.52386 0.153103 6.42583C0.251134 6.3278 0.384091 6.27273 0.522727 6.27273H2.52477C2.68682 6.64386 2.89591 6.99409 3.13636 7.31818H0.522727ZM1.04545 4.70455C0.906819 4.70455 0.773861 4.64947 0.67583 4.55144C0.5778 4.45341 0.522727 4.32045 0.522727 4.18182C0.522727 4.04318 0.5778 3.91022 0.67583 3.81219C0.773861 3.71416 0.906819 3.65909 1.04545 3.65909H2.11705L2.09091 4.18182L2.11705 4.70455H1.04545ZM1.56818 2.09091C1.42955 2.09091 1.29659 2.03584 1.19856 1.93781C1.10053 1.83978 1.04545 1.70682 1.04545 1.56818C1.04545 1.42955 1.10053 1.29659 1.19856 1.19856C1.29659 1.10053 1.42955 1.04545 1.56818 1.04545H3.13636C2.89591 1.36955 2.68682 1.71977 2.52477 2.09091H1.56818Z\"\n fill=\"#15803C\"\n />\n </svg>\n Instant\n </span>\n @if (item.isCreditShell) {\n <span class=\"sbd-cancel__notice sbd-cancel__notice--info\">\n <i class=\"pi pi-info-circle\"></i>\n <span>Your refund will be issued as airline credit under the same PNR. It can be used only for future booking on {{ creditShellSupplierName() }} website.</span>\n </span>\n }\n </span>\n <strong>{{ fmt(item.amount) }}</strong>\n </div>\n }\n } @else {\n\n @if (isFullWalletRefundAllowed() || isWalletOnlyRefund() || isLoyaltyOnlyRefund()) {\n <button type=\"button\" class=\"sbd-cancel__method-option\"\n [ngClass]=\"{ 'sbd-cancel__method-option--active': preferredRefundMethod() === 'wallet' }\"\n (click)=\"preferredRefundMethod.set('wallet')\">\n <span class=\"sbd-cancel__method-icon\"><i class=\"pi pi-wallet\"></i></span>\n <span class=\"sbd-cancel__method-body-text\">\n <span class=\"sbd-cancel__method-name\">{{ walletCardLabel() }}</span>\n <span class=\"sbd-cancel__method-desc\">Usable for future bookings</span>\n <span class=\"sbd-cancel__method-badge sbd-cancel__method-badge--instant\">\n <svg width=\"12\" height=\"9\" viewBox=\"0 0 12 9\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M7.31818 0C8.42727 0 9.49093 0.440583 10.2752 1.22483C11.0594 2.00907 11.5 3.07273 11.5 4.18182C11.5 5.2909 11.0594 6.35457 10.2752 7.13881C9.49093 7.92305 8.42727 8.36364 7.31818 8.36364C6.20909 8.36364 5.14543 7.92305 4.36119 7.13881C3.57695 6.35457 3.13636 5.2909 3.13636 4.18182C3.13636 3.07273 3.57695 2.00907 4.36119 1.22483C5.14543 0.440583 6.20909 0 7.31818 0ZM7.31818 1.04545C6.48637 1.04545 5.68862 1.37589 5.10044 1.96407C4.51226 2.55226 4.18182 3.35 4.18182 4.18182C4.18182 5.01363 4.51226 5.81138 5.10044 6.39956C5.68862 6.98774 6.48637 7.31818 7.31818 7.31818C8.15 7.31818 8.94774 6.98774 9.53593 6.39956C10.1241 5.81138 10.4545 5.01363 10.4545 4.18182C10.4545 3.35 10.1241 2.55226 9.53593 1.96407C8.94774 1.37589 8.15 1.04545 7.31818 1.04545ZM6.79545 2.09091H7.57955V4.06682L8.7975 5.28477L8.24341 5.83886L6.79545 4.39091V2.09091ZM0.522727 7.31818C0.384091 7.31818 0.251134 7.26311 0.153103 7.16508C0.0550729 7.06705 0 6.93409 0 6.79545C0 6.65682 0.0550729 6.52386 0.153103 6.42583C0.251134 6.3278 0.384091 6.27273 0.522727 6.27273H2.52477C2.68682 6.64386 2.89591 6.99409 3.13636 7.31818H0.522727ZM1.04545 4.70455C0.906819 4.70455 0.773861 4.64947 0.67583 4.55144C0.5778 4.45341 0.522727 4.32045 0.522727 4.18182C0.522727 4.04318 0.5778 3.91022 0.67583 3.81219C0.773861 3.71416 0.906819 3.65909 1.04545 3.65909H2.11705L2.09091 4.18182L2.11705 4.70455H1.04545ZM1.56818 2.09091C1.42955 2.09091 1.29659 2.03584 1.19856 1.93781C1.10053 1.83978 1.04545 1.70682 1.04545 1.56818C1.04545 1.42955 1.10053 1.29659 1.19856 1.19856C1.29659 1.10053 1.42955 1.04545 1.56818 1.04545H3.13636C2.89591 1.36955 2.68682 1.71977 2.52477 2.09091H1.56818Z\"\n fill=\"#15803C\"\n />\n </svg>\n Instant Refund\n </span>\n @if (hasLoyaltyPointsRefund() && !hasCashWalletRefund()) {\n <span class=\"sbd-cancel__notice sbd-cancel__notice--info\">\n <i class=\"pi pi-info-circle\"></i>\n <span>External loyalty points are refunded to your almatar Wallet as permanent points and ready to use in your next booking.</span>\n </span>\n }\n </span>\n <span class=\"sbd-cancel__method-radio\"></span>\n </button>\n }\n\n <!-- Original payment method \u2014 replaced with a \"Split Refund\" card whenever\n part of the refund is forced to the wallet (e.g. loyalty points used),\n since the total can't go 100% to the original payment method.\n Name/desc/badge/icon become gateway-specific for installment (Tamara,\n Tabby) and loyalty (Al Rajhi Mokafaa) payment methods. -->\n @if (!hasSplitRefund() && !isWalletOnlyRefund() && !isLoyaltyOnlyRefund()) {\n <button type=\"button\" class=\"sbd-cancel__method-option\"\n [ngClass]=\"{ 'sbd-cancel__method-option--active': preferredRefundMethod() === 'originalPaymentMethod' }\"\n (click)=\"preferredRefundMethod.set('originalPaymentMethod')\">\n <span class=\"sbd-cancel__method-icon\">\n <i class=\"pi\" [ngClass]=\"isSpecialGatewayMethod() ? 'pi-wallet' : 'pi-credit-card'\"></i>\n </span>\n <span class=\"sbd-cancel__method-body-text\">\n <span class=\"sbd-cancel__method-name\">{{ originalPaymentLabel() }}</span>\n <span class=\"sbd-cancel__method-desc\">\n @if (isInstallmentMethod()) {\n Returned to your {{ customerPaymentLabel() }} installment plan\n } @else if (isLoyaltyMethod()) {\n Refunded to your {{ customerPaymentLabel() }} account\n } @else {\n Returned to your original payment method\n }\n </span>\n <span class=\"sbd-cancel__method-badge\" [ngClass]=\"isLoyaltyMethod() ? 'sbd-cancel__method-badge--loyalty' : 'sbd-cancel__method-badge--days'\">\n @if (!isLoyaltyMethod()) {\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M3.30078 6.50195C3.20312 6.50195 3.12174 6.4694 3.05664 6.4043C2.99154 6.33919 2.95898 6.25781 2.95898 6.16016C2.95898 6.06576 2.99154 5.986 3.05664 5.9209C3.12174 5.85579 3.20312 5.82324 3.30078 5.82324H5.5127V2.86914C5.5127 2.77474 5.54525 2.69499 5.61035 2.62988C5.67546 2.56478 5.75521 2.53223 5.84961 2.53223C5.94727 2.53223 6.02865 2.56478 6.09375 2.62988C6.15885 2.69499 6.19141 2.77474 6.19141 2.86914V6.16016C6.19141 6.25781 6.15885 6.33919 6.09375 6.4043C6.02865 6.4694 5.94727 6.50195 5.84961 6.50195H3.30078ZM5.85449 10.9551C5.16764 10.9551 4.52311 10.8249 3.9209 10.5645C3.31868 10.3073 2.78971 9.95085 2.33398 9.49512C1.87826 9.03939 1.52018 8.51042 1.25977 7.9082C1.0026 7.30599 0.874023 6.66146 0.874023 5.97461C0.874023 5.28776 1.0026 4.64323 1.25977 4.04102C1.52018 3.4388 1.87826 2.90983 2.33398 2.4541C2.78971 1.99512 3.31868 1.63704 3.9209 1.37988C4.52311 1.12272 5.16764 0.994141 5.85449 0.994141C6.54134 0.994141 7.18587 1.12272 7.78809 1.37988C8.3903 1.63704 8.91927 1.99512 9.375 2.4541C9.83073 2.90983 10.1872 3.4388 10.4443 4.04102C10.7048 4.64323 10.835 5.28776 10.835 5.97461C10.835 6.66146 10.7048 7.30599 10.4443 7.9082C10.1872 8.51042 9.83073 9.03939 9.375 9.49512C8.91927 9.95085 8.3903 10.3073 7.78809 10.5645C7.18587 10.8249 6.54134 10.9551 5.85449 10.9551ZM5.85449 10.125C6.42741 10.125 6.96452 10.0176 7.46582 9.80273C7.96712 9.58789 8.4082 9.29004 8.78906 8.90918C9.16992 8.52832 9.46777 8.08724 9.68262 7.58594C9.89746 7.08464 10.0049 6.54753 10.0049 5.97461C10.0049 5.40169 9.89746 4.86458 9.68262 4.36328C9.46777 3.85872 9.16992 3.41764 8.78906 3.04004C8.4082 2.65918 7.96712 2.36133 7.46582 2.14648C6.96452 1.93164 6.42741 1.82422 5.85449 1.82422C5.28158 1.82422 4.74447 1.93164 4.24316 2.14648C3.74186 2.36133 3.30078 2.65918 2.91992 3.04004C2.53906 3.41764 2.24121 3.85872 2.02637 4.36328C1.81152 4.86458 1.7041 5.40169 1.7041 5.97461C1.7041 6.54753 1.81152 7.08464 2.02637 7.58594C2.24121 8.08724 2.53906 8.52832 2.91992 8.90918C3.30078 9.29004 3.74186 9.58789 4.24316 9.80273C4.74447 10.0176 5.28158 10.125 5.85449 10.125Z\"\n fill=\"#0B54C2\"\n />\n </svg>\n } @else {\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <circle cx=\"6\" cy=\"6\" r=\"5\" stroke=\"#0B54C2\" stroke-width=\"1.2\" />\n <path d=\"M6 5.5v3M6 3.9v.01\" stroke=\"#0B54C2\" stroke-width=\"1.2\" stroke-linecap=\"round\" />\n </svg>\n }\n {{ cardBadgeText() }}\n </span>\n </span>\n <span class=\"sbd-cancel__option-end\">\n @if (isMadaMethod()) {\n <span class=\"sbd-cancel__mada-logo\" aria-hidden=\"true\">\n <ng-container *ngTemplateOutlet=\"madaLogo\"></ng-container>\n </span>\n }\n <span class=\"sbd-cancel__method-radio\"></span>\n </span>\n </button>\n }\n\n <!-- Split Refund -->\n @if (hasSplitRefund()) {\n <button type=\"button\" class=\"sbd-cancel__method-option sbd-cancel__method-option--split\"\n [ngClass]=\"{ 'sbd-cancel__method-option--active': preferredRefundMethod() === 'split' }\"\n (click)=\"preferredRefundMethod.set('split')\">\n <span class=\"sbd-cancel__method-icon\"><i class=\"pi pi-wallet\"></i></span>\n <span class=\"sbd-cancel__method-body-text\">\n <span class=\"sbd-cancel__method-name\">Split Refund</span>\n <span class=\"sbd-cancel__method-desc\">Returned to your original payment method</span>\n </span>\n <strong class=\"sbd-cancel__method-split-total\">{{ fmt(adjustedData()?.totalRefundAmount) }}</strong>\n <span class=\"sbd-cancel__method-radio\"></span>\n\n <span class=\"sbd-cancel__method-split-breakdown\">\n @for (row of splitRefundRows(); track row.label) {\n <span class=\"sbd-cancel__method-split-row\">\n <span class=\"sbd-cancel__method-split-label\">\n {{ row.label }}\n @if (row.isMada) {\n <span class=\"sbd-cancel__mada-logo\" aria-hidden=\"true\">\n <ng-container *ngTemplateOutlet=\"madaLogo\"></ng-container>\n </span>\n }\n </span>\n <strong>{{ fmt(row.value) }}</strong>\n </span>\n <span class=\"sbd-cancel__method-badge\"\n [ngClass]=\"row.instant ? 'sbd-cancel__method-badge--instant' : (isLoyaltyMethod() ? 'sbd-cancel__method-badge--loyalty' : 'sbd-cancel__method-badge--days')\">\n {{ row.badgeText }}\n </span>\n @if (row.isLoyalty) {\n <span class=\"sbd-cancel__notice sbd-cancel__notice--info\">\n <i class=\"pi pi-info-circle\"></i>\n <span>External loyalty points are refunded to your almatar Wallet as permanent points and ready to use in your next booking.</span>\n </span>\n }\n }\n </span>\n </button>\n }\n }\n </div>\n\n <div class=\"sbd-cancel__footer\">\n <button pButton\n styleClass=\"sbd-cancel__btn-danger-pill\"\n [label]=\"cancelling() ? 'Cancelling...' : 'Confirm Cancellation'\"\n [disabled]=\"cancelling()\" (click)=\"confirm()\"></button>\n </div>\n }\n\n <!-- Main card -->\n @else if (refundData() && step() === 'details') {\n <!-- Card header -->\n <div class=\"sbd-cancel__header sbd-cancel__header--stack\">\n <div class=\"sbd-cancel__header-titles\">\n <span class=\"sbd-cancel__header-title\">Cancel Booking Request</span>\n <span class=\"sbd-cancel__header-sub\">#{{ refundData()?.bookingId ?? bookingId() }}</span>\n </div>\n <button type=\"button\" class=\"sbd-cancel__close-btn\" (click)=\"closed.emit()\" aria-label=\"Close\">\n <i class=\"pi pi-times\"></i>\n </button>\n </div>\n\n <div class=\"sbd-cancel__body\">\n\n <!-- Non-refundable -->\n @if (!refundData()?.isRefundable) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-exclamation-triangle\"></i>\n <div>\n <strong>This booking is not eligible for a refund.</strong>\n @if (refundData()?.cause) {\n <p>{{ refundData()?.cause }}</p>\n } @else {\n <p>Please escalate to the relevant team for manual handling.</p>\n }\n </div>\n </div>\n }\n\n <!-- Refundable flow -->\n @else {\n\n <!-- Top summary: paid / get back -->\n <div class=\"sbd-cancel__summary\">\n <div class=\"sbd-cancel__summary-row\"\n [ngClass]=\"{ 'sbd-cancel__summary-row--toggle': hasPaidBreakdown() && !!adjustedData()?.totalWalletAmount }\"\n (click)=\"hasPaidBreakdown() && !!adjustedData()?.totalWalletAmount && togglePaidSummary()\">\n <span class=\"sbd-cancel__summary-label\"><i class=\"pi pi-wallet\"></i> Total paid</span>\n <span class=\"sbd-cancel__summary-value\">\n <strong>{{ fmt(adjustedData()?.originalAmount) }}</strong>\n @if (hasPaidBreakdown() && !!adjustedData()?.totalWalletAmount) {\n <button type=\"button\" class=\"sbd-cancel__leg-chevron\"\n [ngClass]=\"{ 'sbd-cancel__leg-chevron--collapsed': !paidSummaryExpanded() }\"\n aria-label=\"Toggle paid breakdown\">\n <i class=\"pi pi-chevron-up\"></i>\n </button>\n }\n </span>\n </div>\n\n @if (hasPaidBreakdown() && paidSummaryExpanded() && !!adjustedData()?.totalWalletAmount) {\n <div class=\"sbd-cancel__summary-breakdown\">\n @for (row of paidBreakdownRows(); track row.label) {\n @if (row.label === 'Almatar Wallet') {\n <div class=\"sbd-cancel__summary-breakdown-row\">\n <span>{{ row.label }}</span>\n <span>{{ fmt(row.value) }}</span>\n </div>\n }\n }\n </div>\n }\n\n <div class=\"sbd-cancel__summary-row sbd-cancel__summary-row--refund\">\n <span class=\"sbd-cancel__summary-label\"><i class=\"pi pi-dollar\"></i> You will get back</span>\n <strong>{{ fmt(adjustedData()?.totalRefundAmount) }}</strong>\n </div>\n </div>\n\n <!-- Golden Guarantee gift \u2014 overrides credit-shell messaging below -->\n @if (hasGoldenGuaranteeGift()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--success\">\n <i class=\"pi pi-shield\"></i>\n <span>You're covered by Cancel for any reason service, you'll get 100% refund, minus any applicable fees. See the breakdown below.</span>\n </div>\n }\n\n <!-- One-way (or a round-trip restricted to a single leg): single card -->\n @if (!showRoundTripLegCards()) {\n <!-- Source selector (multiple suppliers) -->\n @if (availableSources().length > 1) {\n <div class=\"sbd-cancel__source-selector\">\n <span class=\"sbd-cancel__source-label\">Cancel via</span>\n @for (src of availableSources(); track src) {\n <button class=\"sbd-cancel__source-chip\"\n [ngClass]=\"{\n 'sbd-cancel__source-chip--active': selectedSource() === src,\n 'sbd-cancel__source-chip--no-refund': activeSourceBreakdown()?.[src]?.isRefundable === false\n }\"\n (click)=\"selectedSource.set(src)\">\n {{ src }}\n @if (activeSourceBreakdown()?.[src]?.isRefundable === false) {\n <i class=\"pi pi-ban\" style=\"font-size:10px; margin-left:4px;\"></i>\n }\n </button>\n }\n </div>\n @if (sourceNonRefundableHint()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-ban\"></i>\n <span>{{ sourceNonRefundableHint() }}</span>\n </div>\n }\n }\n\n <!-- Not refundable -->\n @if (!currentData()?.isRefundable) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-ban\"></i>\n <div>\n <strong>This leg is not eligible for a refund.</strong>\n <p>Please escalate to the relevant team for manual handling.</p>\n </div>\n </div>\n } @else {\n <div class=\"sbd-cancel__section-title\">Refund Details</div>\n\n <div class=\"sbd-cancel__refund-card\">\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline sbd-cancel__badge--green\">Refundable</span>\n\n @if (refundableBreakdownRows(adjustedData()).length) {\n @for (r of refundableBreakdownRows(adjustedData()); track r.label) {\n <div class=\"sbd-cancel__refund-card-row\">\n <span>{{ r.label }}</span>\n <strong>{{ fmt(r.value) }}</strong>\n </div>\n }\n } @else {\n <div class=\"sbd-cancel__refund-card-row\">\n <span>Fare</span>\n <strong>{{ fmt(adjustedData()?.originalAmount) }}</strong>\n </div>\n }\n\n <!-- hide addon note new CR from CX -->\n <!-- @if (hasRefundableAddonNote(adjustedData())) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--neutral\">\n <i class=\"pi pi-info-circle\"></i>\n <span>Your add-on service/s refund will be processed separately by our Customer Service team shortly.</span>\n </div>\n } -->\n\n <!-- Deductions -->\n @if (deductions().length) {\n <div class=\"sbd-cancel__refund-card-divider\"></div>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline sbd-cancel__badge--muted\">Not Refunded - Deductions</span>\n @for (d of deductions(); track d.label) {\n <div class=\"sbd-cancel__refund-card-line\">\n <span>\n {{ d.label }}\n @if (d.badge) { <span class=\"sbd-cancel__tag\">{{ d.badge }}</span> }\n </span>\n <span class=\"sbd-cancel__refund-card-line--neg\">{{ fmtNeg(d.value) }}</span>\n </div>\n }\n }\n\n <div class=\"sbd-cancel__refund-card-divider sbd-cancel__refund-card-divider--dashed\"></div>\n <div class=\"sbd-cancel__refund-card-total\">\n <span>Total Refund</span>\n <strong>{{ fmt(adjustedData()?.totalRefundAmount) }}</strong>\n </div>\n </div>\n\n <!-- Credit shell -->\n @if (creditShellNote()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--credit\">\n <i class=\"pi pi-wallet\"></i>\n <span>\u2139\uFE0F {{ creditShellNote() }}</span>\n </div>\n }\n }\n }\n\n <!-- Round trip: one collapsible card per leg (only when both legs are cancellable) -->\n @if (showRoundTripLegCards()) {\n <!-- Gift hint -->\n @if (giftHint()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--tip\">\n <i class=\"pi pi-info-circle\"></i>\n <span>{{ giftHint() }}</span>\n </div>\n }\n\n <!-- Mixed hint -->\n @if (mixedHint()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-exclamation-triangle\"></i>\n <span>{{ mixedHint() }}</span>\n </div>\n }\n\n <!-- Source selector (multiple suppliers) -->\n @if (availableSources().length > 1) {\n <div class=\"sbd-cancel__source-selector\">\n <span class=\"sbd-cancel__source-label\">Cancel via</span>\n @for (src of availableSources(); track src) {\n <button class=\"sbd-cancel__source-chip\"\n [ngClass]=\"{\n 'sbd-cancel__source-chip--active': selectedSource() === src,\n 'sbd-cancel__source-chip--no-refund': activeSourceBreakdown()?.[src]?.isRefundable === false\n }\"\n (click)=\"selectedSource.set(src)\">\n {{ src }}\n @if (activeSourceBreakdown()?.[src]?.isRefundable === false) {\n <i class=\"pi pi-ban\" style=\"font-size:10px; margin-left:4px;\"></i>\n }\n </button>\n }\n </div>\n @if (sourceNonRefundableHint()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-ban\"></i>\n <span>{{ sourceNonRefundableHint() }}</span>\n </div>\n }\n }\n\n <!-- Onward leg card -->\n @if (onwardChecked()) {\n <div class=\"sbd-cancel__leg-card\">\n <div class=\"sbd-cancel__leg-card-header\" (click)=\"toggleLegExpanded('onward')\">\n <span class=\"sbd-cancel__leg-checkbox\">Onward</span>\n <button type=\"button\" class=\"sbd-cancel__leg-chevron\"\n [ngClass]=\"{ 'sbd-cancel__leg-chevron--collapsed': !onwardExpanded() }\"\n aria-label=\"Toggle\">\n <i class=\"pi pi-chevron-up\"></i>\n </button>\n </div>\n\n @if (onwardExpanded()) {\n <div class=\"sbd-cancel__leg-card-body\">\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline\"\n [ngClass]=\"legAdjustedData('onward')?.isRefundable ? 'sbd-cancel__badge--green' : 'sbd-cancel__badge--red'\">\n {{ legAdjustedData('onward')?.isRefundable ? 'Refundable' : 'Non-Refundable' }}\n </span>\n\n @if (legAdjustedData('onward')?.isRefundable) {\n @if (refundableBreakdownRows(legAdjustedData('onward')).length) {\n @for (r of refundableBreakdownRows(legAdjustedData('onward')); track r.label) {\n <div class=\"sbd-cancel__refund-card-row\">\n <span>{{ r.label }}</span>\n <strong>{{ fmt(r.value) }}</strong>\n </div>\n }\n } @else {\n <div class=\"sbd-cancel__refund-card-row\">\n <span>Fare</span>\n <strong>{{ fmt(legRefundPool('onward')) }}</strong>\n </div>\n }\n\n <!-- hide addon note new CR from CX -->\n <!-- @if (hasRefundableAddonNote(legAdjustedData('onward'))) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--neutral\">\n <i class=\"pi pi-info-circle\"></i>\n <span>Your add-on service/s refund will be processed separately by our Customer Service team shortly.</span>\n </div>\n } -->\n\n @if (legDeductions('onward').length && !isCreditShell()) {\n <div class=\"sbd-cancel__refund-card-divider\"></div>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline sbd-cancel__badge--muted\">Not Refunded - Deductions</span>\n @for (d of legDeductions('onward'); track d.label) {\n <div class=\"sbd-cancel__refund-card-line\">\n <span>\n {{ d.label }}\n @if (d.badge) { <span class=\"sbd-cancel__tag\">{{ d.badge }}</span> }\n </span>\n <span class=\"sbd-cancel__refund-card-line--neg\">{{ fmtNeg(d.value) }}</span>\n </div>\n }\n }\n\n <div class=\"sbd-cancel__refund-card-divider sbd-cancel__refund-card-divider--dashed\"></div>\n <div class=\"sbd-cancel__refund-card-total\">\n <span>Departure Total Refund</span>\n <strong>{{ fmt(legAdjustedData('onward')?.totalRefundAmount) }}</strong>\n </div>\n\n @if (refundData()?.legInfo?.onward?.isCreditShell && !hasGoldenGuaranteeGift()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--credit\">\n <i class=\"pi pi-wallet\"></i>\n <span>\u2139\uFE0F The refund for the onward flight has been issued as an Airline Credit Shell.</span>\n </div>\n }\n } @else {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-ban\"></i>\n <span>This leg is not eligible for a refund.</span>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Return leg card -->\n @if (returnChecked()) {\n <div class=\"sbd-cancel__leg-card\">\n <div class=\"sbd-cancel__leg-card-header\" (click)=\"toggleLegExpanded('return')\">\n <span class=\"sbd-cancel__leg-checkbox\">Return</span>\n <button type=\"button\" class=\"sbd-cancel__leg-chevron\"\n [ngClass]=\"{ 'sbd-cancel__leg-chevron--collapsed': !returnExpanded() }\"\n aria-label=\"Toggle\">\n <i class=\"pi pi-chevron-up\"></i>\n </button>\n </div>\n\n @if (returnExpanded()) {\n <div class=\"sbd-cancel__leg-card-body\">\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline\"\n [ngClass]=\"legAdjustedData('return')?.isRefundable ? 'sbd-cancel__badge--green' : 'sbd-cancel__badge--red'\">\n {{ legAdjustedData('return')?.isRefundable ? 'Refundable' : 'Non-Refundable' }}\n </span>\n\n @if (legAdjustedData('return')?.isRefundable) {\n @if (refundableBreakdownRows(legAdjustedData('return')).length) {\n @for (r of refundableBreakdownRows(legAdjustedData('return')); track r.label) {\n <div class=\"sbd-cancel__refund-card-row\">\n <span>{{ r.label }}</span>\n <strong>{{ fmt(r.value) }}</strong>\n </div>\n }\n } @else {\n <div class=\"sbd-cancel__refund-card-row\">\n <span>Fare</span>\n <strong>{{ fmt(legRefundPool('return')) }}</strong>\n </div>\n }\n\n <!-- hide addon note new CR from CX -->\n <!-- @if (hasRefundableAddonNote(legAdjustedData('return'))) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--neutral\">\n <i class=\"pi pi-info-circle\"></i>\n <span>Your add-on service/s refund will be processed separately by our Customer Service team shortly.</span>\n </div>\n } -->\n\n @if (legDeductions('return').length && !isCreditShell()) {\n <div class=\"sbd-cancel__refund-card-divider\"></div>\n <span class=\"sbd-cancel__badge sbd-cancel__badge--outline sbd-cancel__badge--muted\">Not Refunded - Deductions</span>\n @for (d of legDeductions('return'); track d.label) {\n <div class=\"sbd-cancel__refund-card-line\">\n <span>\n {{ d.label }}\n @if (d.badge) { <span class=\"sbd-cancel__tag\">{{ d.badge }}</span> }\n </span>\n <span class=\"sbd-cancel__refund-card-line--neg\">{{ fmtNeg(d.value) }}</span>\n </div>\n }\n }\n\n <div class=\"sbd-cancel__refund-card-divider sbd-cancel__refund-card-divider--dashed\"></div>\n <div class=\"sbd-cancel__refund-card-total\">\n <span>Return Total Refund</span>\n <strong>{{ fmt(legAdjustedData('return')?.totalRefundAmount) }}</strong>\n </div>\n\n @if (refundData()?.legInfo?.return?.isCreditShell && !hasGoldenGuaranteeGift()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--credit\">\n <i class=\"pi pi-wallet\"></i>\n <span>\u2139\uFE0F The refund for the return flight has been issued as an Airline Credit Shell.</span>\n </div>\n }\n } @else {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--warning\">\n <i class=\"pi pi-ban\"></i>\n <span>This leg is not eligible for a refund.</span>\n </div>\n }\n </div>\n }\n </div>\n }\n }\n\n <!-- Instant refund notice -->\n @if (currentData()?.isRefundable) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--shield\">\n <i class=\"pi pi-shield\"></i>\n <span>\n @if (isNilRefund()) {\n We're sorry, as per airline policy, no refund applies because the cancellation fees equal or exceed your refundable fare amount.\n } @else {\n @if (isWalletOnlyRefund() || isLoyaltyOnlyRefund()) {\n Your refund is instant to the customer's almatar Wallet.\n } @else {\n Your refund is instant from our side, your bank may take up to 15 business days to reflect it.\n }\n }\n @if (!isNilRefund() && !isCreditShell()) {\n <strong>You can select refund option in the next step</strong>\n }\n </span>\n </div>\n }\n }\n\n </div>\n\n <!-- Footer -->\n <div class=\"sbd-cancel__footer\">\n @if (refundData()?.isRefundable) {\n <button pButton label=\"Keep booking\" styleClass=\"p-button-outlined p-button-secondary sbd-cancel__btn-keep\" (click)=\"closed.emit()\"></button>\n @if (currentData()?.isRefundable) {\n <button pButton\n styleClass=\"sbd-cancel__btn-primary\"\n [label]=\"isCreditShell() ? 'Continue Cancellation' : 'Select refund option'\"\n [disabled]=\"!canConfirm()\" (click)=\"requestCancel()\"></button>\n }\n } @else {\n <button pButton label=\"Close\" styleClass=\"p-button-outlined p-button-secondary\" (click)=\"closed.emit()\"></button>\n }\n </div>\n }\n\n <!-- \u2500\u2500 Result overlay: floating modal over the dimmed refund-method screen (scoped to this dialog, not the whole viewport) \u2500\u2500 -->\n @if (cancelResult()) {\n <div class=\"sbd-cancel__result-overlay\">\n <div class=\"sbd-cancel__result-backdrop\"></div>\n <div class=\"sbd-cancel__result-modal\">\n <div class=\"sbd-cancel__result-hero\">\n <span class=\"sbd-cancel__result-icon sbd-cancel__result-icon--green\">\n <i class=\"pi pi-check\"></i>\n </span>\n <h3 class=\"sbd-cancel__result-title\">{{ confirmIsNilRefund() ? 'Cancel Confirmed' : 'Refund Confirmed' }}</h3>\n </div>\n\n @for (r of confirmRefunds(); track r.label) {\n <div class=\"sbd-cancel__result-row\">\n <span class=\"sbd-cancel__method-icon\">\n <i class=\"pi\" [ngClass]=\"isWalletDestinationRow(r) ? 'pi-wallet' : 'pi-credit-card'\"></i>\n </span>\n <span class=\"sbd-cancel__result-body-text\">\n <span class=\"sbd-cancel__method-name\">{{ r.label }}</span>\n @if (r.subRows?.length) {\n <span class=\"sbd-cancel__result-sub-rows\">\n @for (sub of r.subRows; track sub.label) {\n <span class=\"sbd-cancel__result-sub-row\">\n <span>{{ sub.label }}</span>\n <strong>{{ fmtResult(sub.value) }}</strong>\n </span>\n }\n </span>\n }\n <span class=\"sbd-cancel__method-badge\"\n [ngClass]=\"isWalletDestinationRow(r) ? 'sbd-cancel__method-badge--instant' : 'sbd-cancel__method-badge--days'\">\n {{ isWalletDestinationRow(r) ? 'Ready Now' : (r.key === 'totalCardRefundAmount' ? confirmCardBadgeText() : 'Up to 15 business days, bank policy') }}\n </span>\n @if (r.hasLoyaltyNote) {\n <span class=\"sbd-cancel__notice sbd-cancel__notice--info\">\n <i class=\"pi pi-info-circle\"></i>\n <span>External loyalty points are refunded to your almatar Wallet as permanent points and ready to use in your next booking.</span>\n </span>\n }\n </span>\n <strong class=\"sbd-cancel__result-amount\">{{ fmtResult(r.value) }}</strong>\n </div>\n }\n\n @if (confirmCreditShellNote()) {\n <div class=\"sbd-cancel__notice sbd-cancel__notice--credit\">\n <i class=\"pi pi-wallet\"></i>\n <span>\u2139\uFE0F {{ confirmCreditShellNote() }}</span>\n </div>\n }\n\n <button pButton styleClass=\"sbd-cancel__btn-primary sbd-cancel__result-done\" label=\"Close\" (click)=\"closed.emit()\"></button>\n </div>\n </div>\n }\n\n </div>\n\n</p-dialog>\n\n<ng-template #madaLogo>\n <svg width=\"43\" height=\"15\" viewBox=\"0 0 43 15\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <g clip-path=\"url(#clip0_sbd_mada)\">\n <path d=\"M18.1894 8.6543H0V14.9933H18.1894V8.6543Z\" fill=\"#84B740\" />\n <path d=\"M18.1894 0H0V6.34463H18.1894V0Z\" fill=\"#259BD6\" />\n <path\n d=\"M36.3779 13.7034L36.2969 13.7203C36.016 13.7768 35.9134 13.7994 35.7082 13.7994C35.233 13.7994 34.6713 13.5452 34.6713 12.3475C34.6713 11.7316 34.7685 10.9124 35.6542 10.9124H35.6596C35.8108 10.9237 35.9836 10.9407 36.3077 11.0424L36.3779 11.065V13.7034ZM36.5237 7.73164L36.3779 7.75989V9.97458L36.2483 9.93503L36.2105 9.92373C36.0647 9.87853 35.7298 9.77684 35.4058 9.77684C33.6344 9.77684 33.2617 11.178 33.2617 12.3531C33.2617 13.9633 34.1258 14.8898 35.6326 14.8898C36.2699 14.8898 36.7397 14.822 37.215 14.6582C37.6524 14.5113 37.8091 14.3023 37.8091 13.8559V7.5C37.3878 7.5791 36.9504 7.65819 36.5237 7.73164Z\"\n fill=\"#747583\"\n />\n <path\n d=\"M41.651 13.7385L41.5754 13.7611L41.3053 13.8345C41.0515 13.9023 40.8247 13.9419 40.6519 13.9419C40.236 13.9419 39.9876 13.7272 39.9876 13.36C39.9876 13.1227 40.0902 12.7215 40.7707 12.7215H41.651V13.7385ZM41.0299 9.74414C40.4844 9.74414 39.9228 9.84584 39.2261 10.0718L38.7724 10.2131L38.9236 11.2865L39.3665 11.134C39.831 10.9758 40.4088 10.8741 40.8409 10.8741C41.0353 10.8741 41.6294 10.8741 41.6294 11.5464V11.8402H40.8031C39.2963 11.8402 38.5996 12.343 38.5996 13.4221C38.5996 14.343 39.2423 14.8967 40.3224 14.8967C40.6573 14.8967 41.1217 14.8289 41.5214 14.7272L41.543 14.7215L41.5646 14.7272L41.6996 14.7498C42.1208 14.8289 42.5583 14.908 42.9849 14.9927V11.4673C42.9849 10.3261 42.3261 9.74414 41.0299 9.74414Z\"\n fill=\"#747583\"\n />\n <path\n d=\"M31.1527 13.7385L31.0771 13.7611L30.807 13.8345C30.5532 13.9023 30.3318 13.9419 30.1536 13.9419C29.7377 13.9419 29.4893 13.7272 29.4893 13.36C29.4893 13.1227 29.5919 12.7215 30.267 12.7215H31.1473L31.1527 13.7385ZM30.537 9.74414C29.9861 9.74414 29.4299 9.84584 28.7332 10.0718L28.2795 10.2131L28.4308 11.2865L28.8736 11.134C29.3381 10.9758 29.9159 10.8741 30.348 10.8741C30.5424 10.8741 31.1365 10.8741 31.1365 11.5464V11.8402H30.3102C28.8034 11.8402 28.1013 12.343 28.1013 13.4221C28.1013 14.343 28.744 14.8967 29.8295 14.8967C30.1644 14.8967 30.6288 14.8289 31.0285 14.7272L31.0501 14.7215L31.0717 14.7272L31.2013 14.7498C31.6279 14.8289 32.06 14.908 32.4866 14.9984V11.473C32.492 10.3204 31.8332 9.74414 30.537 9.74414Z\"\n fill=\"#747583\"\n />\n <path\n d=\"M25.4701 9.7576C24.7842 9.7576 24.2172 9.99489 24.0065 10.0966L23.9525 10.1248L23.9039 10.0853C23.6123 9.86495 23.1856 9.75195 22.5916 9.75195C22.0677 9.75195 21.5762 9.83105 21.0416 9.99489C20.5825 10.1418 20.4043 10.3734 20.4043 10.8085V14.8367H21.8409V11.1135L21.9111 11.0909C22.2027 10.9892 22.3755 10.9723 22.543 10.9723C22.9588 10.9723 23.1694 11.2039 23.1694 11.6559V14.8423H24.5844V11.5938C24.5844 11.4017 24.5466 11.2887 24.5412 11.2661L24.4926 11.17L24.5898 11.1248C24.8058 11.0231 25.0435 10.9723 25.2919 10.9723C25.5781 10.9723 25.9184 11.0909 25.9184 11.6559V14.8423H27.3279V11.509C27.3279 10.3282 26.7231 9.7576 25.4701 9.7576Z\"\n fill=\"#747583\"\n />\n <path\n d=\"M40.5866 4.13559C40.3759 4.13559 40.0249 4.113 39.7495 4.0565L39.6684 4.03955V1.86441C39.6684 1.68362 39.636 1.57062 39.6306 1.55367L39.5874 1.46328L39.6793 1.42373C39.7009 1.41243 39.7225 1.40678 39.7495 1.39548L39.7657 1.38418C39.7981 1.37288 39.8305 1.36158 39.8629 1.35028C39.8791 1.34463 39.8899 1.33898 39.9007 1.33898C40.2193 1.24859 40.511 1.25989 40.6406 1.24859H40.646C41.5263 1.24859 41.6289 2.0678 41.6289 2.68362C41.6235 3.88136 41.0564 4.13559 40.5866 4.13559ZM40.5812 0C40.5704 0 40.5542 0 40.5434 0C39.7171 0 38.8692 0.237288 38.5667 0.700565C38.4047 0.926554 38.3129 1.20904 38.3075 1.54237V3.78531C38.3075 3.9774 38.2697 4.05085 38.2643 4.0678L38.2157 4.16384H35.6072V2.60452H35.6018C35.5694 0.960452 34.6404 0.0564972 33.2957 0.0564972H33.1391H31.9833C31.9293 0.457627 31.8861 0.740113 31.8321 1.14124H33.1391C33.8249 1.14124 34.1868 1.75141 34.1868 2.68927V4.25989L34.095 4.20904C34.0788 4.20339 33.9654 4.16384 33.7871 4.16384H31.5297C31.4865 4.46328 31.4325 4.85311 31.373 5.24294H38.3129C38.5505 5.19209 38.8259 5.14689 39.0636 5.10735C39.4146 5.28814 40.0681 5.38418 40.5164 5.38418C42.0231 5.38418 43.0007 4.32768 43.0007 2.70057C42.9953 1.0904 42.0501 0.0338983 40.5812 0Z\"\n fill=\"#747583\"\n />\n <path\n d=\"M28.4189 5.90821H28.4837C29.9905 5.90821 30.6925 5.38844 30.6925 4.10595C30.6925 3.18505 30.0499 2.45059 28.9697 2.45059H27.5818C27.1659 2.45059 26.9175 2.202 26.9175 1.78392C26.9175 1.50143 27.0201 1.15115 27.7006 1.15115H30.7357C30.8006 0.738723 30.833 0.478836 30.8924 0.0664062H27.7384C26.2694 0.0664062 25.5295 0.710474 25.5295 1.78392C25.5295 2.84607 26.1722 3.39974 27.2523 3.39974H28.6403C29.0561 3.39974 29.3046 3.74437 29.3046 4.10595C29.3046 4.34324 29.202 4.83477 28.5269 4.83477H28.2947L23.8499 4.82347H23.0398C22.3539 4.82347 21.8733 4.41669 21.8733 3.47319V2.82347C21.8733 1.84042 22.2459 1.23025 23.0398 1.23025H24.3576C24.417 0.812169 24.4548 0.546632 24.5088 0.151152H22.867H22.7104C21.3656 0.151152 20.4367 1.09466 20.4043 2.73872V2.80087V3.47319C20.4367 5.11725 21.3656 5.90256 22.7104 5.90256H22.867H24.0227L26.4314 5.90821H27.868H28.4189Z\"\n fill=\"#747583\"\n />\n </g>\n <defs>\n <clipPath id=\"clip0_sbd_mada\">\n <rect width=\"43\" height=\"15\" fill=\"white\" />\n </clipPath>\n </defs>\n </svg>\n</ng-template>\n", styles: [":host ::ng-deep .sbd-cancel-dialog-wrapper{border-radius:16px;overflow:hidden;box-shadow:0 24px 60px #0f172a38}:host ::ng-deep .sbd-cancel-dialog-wrapper .p-dialog-content{padding:0;border-radius:16px;overflow-y:auto;overflow-x:hidden}.sbd-cancel{font-family:inherit;border-radius:16px;overflow:visible;position:relative}.sbd-cancel__loading{padding:24px}.sbd-cancel__loading-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.sbd-cancel__loading-rows{display:flex;flex-direction:column;gap:10px;margin-top:20px}.sbd-cancel__loading-row{display:flex;justify-content:space-between}.sbd-cancel__are-you-sure{padding:36px 24px 28px;display:flex;flex-direction:column;align-items:center;gap:14px;text-align:center;border-top:1px solid #e2e8f0}.sbd-cancel__ays-icon{width:56px;height:56px;border-radius:50%;background:#fff7ed;border:2px solid #fed7aa;display:flex;align-items:center;justify-content:center}.sbd-cancel__ays-icon .pi{font-size:24px;color:#f97316}.sbd-cancel__ays-icon--warn{background:#fef2f2;border-color:#fecaca}.sbd-cancel__ays-icon--warn .pi{color:#ef4444}.sbd-cancel__ays-hint{font-size:13px;color:#64748b;min-height:20px;animation:sbd-hint-fade .35s ease-out}.sbd-cancel__ays-dots{display:flex;gap:6px}.sbd-cancel__ays-dots span{width:7px;height:7px;border-radius:50%;background:#f97316;opacity:.3;animation:sbd-dot-pulse 1.2s ease-in-out infinite}.sbd-cancel__ays-dots span:nth-child(2){animation-delay:.2s}.sbd-cancel__ays-dots span:nth-child(3){animation-delay:.4s}@keyframes sbd-hint-fade{0%{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes sbd-dot-pulse{0%,to{opacity:.3;transform:scale(1)}50%{opacity:1;transform:scale(1.4)}}.sbd-cancel__header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #e2e8f0;background:#f8fafc}.sbd-cancel__header-left{display:flex;align-items:center;gap:10px}.sbd-cancel__header-icon{font-size:18px;color:#475569}.sbd-cancel__header-title{font-size:15px;font-weight:700;color:#1e2e4a}.sbd-cancel__header--stack{align-items:flex-start}.sbd-cancel__header-titles{display:flex;flex-direction:column;gap:2px}.sbd-cancel__header-sub{font-size:12.5px;font-weight:600;color:#94a3b8;font-variant-numeric:tabular-nums}.sbd-cancel__badge{font-size:11.5px;font-weight:700;border-radius:20px;padding:3px 12px;letter-spacing:.03em;width:max-content;text-transform:uppercase}.sbd-cancel__badge--green{background:#dcfce7;color:#166534}.sbd-cancel__badge--red{background:#fee2e2;color:#991b1b}.sbd-cancel__badge--orange{background:#fff7ed;color:#9a3412}.sbd-cancel__badge--outline{background:transparent;box-shadow:inset 0 0 0 1.5px currentColor;padding:4px 14px}.sbd-cancel__badge--outline.sbd-cancel__badge--green{color:#16a34a}.sbd-cancel__badge--outline.sbd-cancel__badge--red{color:#dc2626}.sbd-cancel__badge--muted{background:#f8fafc;color:#64748b;box-shadow:inset 0 0 0 1px #e2e8f0}.sbd-cancel__close-btn{width:30px;height:30px;border-radius:8px;border:none;background:transparent;color:#64748b;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s,color .15s}.sbd-cancel__close-btn .pi{font-size:14px}.sbd-cancel__close-btn:hover{background:#e2e8f0;color:#1e2e4a}.sbd-cancel__close-btn--floating{position:absolute;top:12px;inset-inline-end:12px}.sbd-cancel__summary{background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.sbd-cancel__summary-row{display:flex;align-items:center;justify-content:space-between;font-size:13.5px}.sbd-cancel__summary-row strong{font-size:15px;font-weight:800;color:#1e2e4a;font-variant-numeric:tabular-nums}.sbd-cancel__summary-row--refund .sbd-cancel__summary-label,.sbd-cancel__summary-row--refund strong{color:#16a34a}.sbd-cancel__summary-row--toggle{cursor:pointer}.sbd-cancel__summary-label{display:inline-flex;align-items:center;gap:8px;font-weight:600;color:#475569}.sbd-cancel__summary-label .pi{font-size:13px}.sbd-cancel__summary-value{display:flex;align-items:center;gap:4px}.sbd-cancel__summary-breakdown{display:flex;flex-direction:column;gap:6px;padding:2px 0 2px 22px}.sbd-cancel__summary-breakdown-row{display:flex;align-items:center;justify-content:space-between;font-size:12.5px;color:#64748b}.sbd-cancel__summary-breakdown-row span:last-child{font-variant-numeric:tabular-nums;font-weight:600;color:#475569}.sbd-cancel__section-title{font-size:14px;font-weight:700;color:#1e2e4a}.sbd-cancel__refund-card{border:1px solid #e2e8f0;border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:4px}.sbd-cancel__refund-card-row{display:flex;align-items:center;justify-content:space-between;margin-top:10px;font-size:14px}.sbd-cancel__refund-card-row span{color:#334155;font-weight:600}.sbd-cancel__refund-card-row strong{color:#1e2e4a;font-weight:700;font-variant-numeric:tabular-nums}.sbd-cancel__refund-card-divider{border-top:1px solid #e2e8f0;margin:10px 0}.sbd-cancel__refund-card-divider--dashed{border-top-style:dashed}.sbd-cancel__refund-card-line{display:flex;align-items:center;justify-content:space-between;padding:5px 0;font-size:13px}.sbd-cancel__refund-card-line span:first-child{color:#64748b;display:inline-flex;align-items:center}.sbd-cancel__refund-card-line--neg{color:#64748b;font-weight:600;font-variant-numeric:tabular-nums}.sbd-cancel__refund-card-total{display:flex;align-items:center;justify-content:space-between}.sbd-cancel__refund-card-total span{font-size:14.5px;font-weight:700;color:#1e2e4a}.sbd-cancel__refund-card-total strong{font-size:17px;font-weight:800;color:#1e2e4a;font-variant-numeric:tabular-nums}.sbd-cancel__btn-keep{flex:1 1 0}.sbd-cancel__btn-primary{flex:1 1 0;background:#6d4aff!important;border-color:#6d4aff!important;color:#fff!important}.sbd-cancel__btn-primary:hover:not(:disabled){background:#5c3ce6!important;border-color:#5c3ce6!important}.sbd-cancel__btn-primary:disabled{opacity:.5}.sbd-cancel__paid{display:flex;align-items:center;justify-content:space-between;margin:0 -20px;padding:14px 20px;border-radius:0;background:#f1f0fb;border-top:1px solid #e7e5f7;border-bottom:1px solid #e7e5f7}.sbd-cancel__paid span{font-size:13.5px;font-weight:600;color:#4b4670}.sbd-cancel__paid strong{font-size:18px;font-weight:800;color:#2b2747;font-variant-numeric:tabular-nums;letter-spacing:.01em}.sbd-cancel__group{display:flex;flex-direction:column}.sbd-cancel__group-label{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.07em;margin-bottom:2px}.sbd-cancel__group-label--deduct{color:#e05656}.sbd-cancel__group-label--refund{color:#16a34a}.sbd-cancel__line{display:flex;justify-content:space-between;align-items:center;padding:11px 0;font-size:13.5px;border-bottom:1px dashed #e5e7eb}.sbd-cancel__line:last-child{border-bottom:none}.sbd-cancel__line-label{display:inline-flex;align-items:center;color:#334155}.sbd-cancel__line-val{font-weight:700;font-variant-numeric:tabular-nums}.sbd-cancel__line-val--neg{color:#dc2626}.sbd-cancel__line-val--pos{color:#16a34a}.sbd-cancel__tag{margin-left:8px;font-size:10px;font-weight:700;line-height:1;padding:3px 8px;border-radius:6px;background:#ede9fe;color:#6d28d9;letter-spacing:.02em}.sbd-cancel__total-deduct{display:flex;align-items:center;justify-content:space-between;padding:13px 16px;border-radius:10px;background:#fdf2f2;border:1px solid #f6c9c9}.sbd-cancel__total-deduct span{font-size:14px;font-weight:700;color:#b3344a}.sbd-cancel__total-deduct strong{font-size:15px;font-weight:800;color:#dc2626;font-variant-numeric:tabular-nums}.sbd-cancel__calc{display:flex;align-items:center;gap:12px;margin:2px 0;color:#94a3b8;font-size:13px;font-variant-numeric:tabular-nums}.sbd-cancel__calc:before,.sbd-cancel__calc:after{content:\"\";flex:1;height:1px;background:#e5e7eb}.sbd-cancel__refund-total{display:flex;align-items:center;justify-content:space-between}.sbd-cancel__refund-total span{font-size:16px;font-weight:800;color:#1e2e4a}.sbd-cancel__refund-total strong{font-size:26px;font-weight:800;color:#16a34a;font-variant-numeric:tabular-nums;letter-spacing:.01em}.sbd-cancel__formula{text-align:center;padding:14px 16px;border-radius:10px;background:#1f2438;color:#f1f5f9;font-family:SFMono-Regular,ui-monospace,Menlo,Consolas,monospace;font-size:14px;font-weight:600;letter-spacing:.04em;font-variant-numeric:tabular-nums}.sbd-cancel__discrepancy{display:flex;align-items:flex-start;gap:10px;padding:11px 13px;border-radius:8px;font-size:12.5px;line-height:1.45;background:#fffbeb;border:1px solid #fde68a;color:#92400e}.sbd-cancel__discrepancy i{flex-shrink:0;margin-top:1px;font-size:14px;color:#d97706}.sbd-cancel__body{padding:16px 20px;display:flex;flex-direction:column;gap:12px}.sbd-cancel__leg-card{width:100%;border-radius:12px;border:1px solid #e2e8f0;background:#fff;overflow:hidden}.sbd-cancel__leg-card-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:14px 16px;cursor:pointer}.sbd-cancel__leg-checkbox{display:flex;align-items:center;gap:10px;font-size:13.5px;font-weight:700;color:#1e2e4a;cursor:pointer}.sbd-cancel__leg-checkbox input[type=checkbox]{width:17px;height:17px;accent-color:#6d4aff;cursor:pointer}.sbd-cancel__leg-chevron{flex-shrink:0;width:26px;height:26px;display:flex;align-items:center;justify-content:center;border:none;background:transparent;color:#64748b;cursor:pointer;transition:transform .15s}.sbd-cancel__leg-chevron i{font-size:13px}.sbd-cancel__leg-chevron--collapsed{transform:rotate(180deg)}.sbd-cancel__leg-card-body{display:flex;flex-direction:column;gap:4px;padding:0 16px 16px}.sbd-cancel__leg-card-body--excluded{opacity:.5}.sbd-cancel__notice{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-radius:8px;font-size:12.5px}.sbd-cancel__notice i{flex-shrink:0;margin-top:1px;font-size:14px}.sbd-cancel__notice p{margin:2px 0 0;font-size:12px;opacity:.8}.sbd-cancel__notice strong{display:block;font-weight:700}.sbd-cancel__notice--warning{background:#fff7ed;border:1px solid #fed7aa;color:#9a3412}.sbd-cancel__notice--warning i{color:#f97316}.sbd-cancel__notice--info{background:#eff6ff;border:1px solid #bfdbfe;color:#1e40af}.sbd-cancel__notice--info i{color:#3b82f6}.sbd-cancel__notice--neutral{background:#f7f7f8;border:1px solid #e5e7eb;color:#111827}.sbd-cancel__notice--neutral i{color:#111827}.sbd-cancel__notice--tip{background:#f0fdf4;border:1px solid #bbf7d0;color:#14532d}.sbd-cancel__notice--tip i{color:#22c55e}.sbd-cancel__notice--success{background:#f0fdf4;border:1px solid #bbf7d0;color:#15803c}.sbd-cancel__notice--success i{color:#16a34a}.sbd-cancel__notice--credit{background:#faf5ff;border:1px solid #e9d5ff;color:#5b21b6;font-size:12px}.sbd-cancel__notice--credit i{color:#a855f7}.sbd-cancel__notice--shield{background:#eff6ff;border:1px solid #bfdbfe;color:#1e40af}.sbd-cancel__notice--shield strong{display:inline;font-weight:700}.sbd-cancel__notice--shield i{color:#3b82f6}.sbd-cancel__source-selector{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:4px 0}.sbd-cancel__source-label{font-size:11px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;margin-right:2px}.sbd-cancel__source-chip{font-size:12px;font-weight:600;padding:4px 14px;border-radius:20px;border:1.5px solid #e2e8f0;background:#f8fafc;color:#475569;cursor:pointer;transition:border-color .15s,color .15s,background .15s;display:inline-flex;align-items:center}.sbd-cancel__source-chip:hover{border-color:#94a3b8;color:#475569;background:#f1f5f9}.sbd-cancel__source-chip--active{background:#eff6ff;color:#1d4ed8;border-color:#93c5fd}.sbd-cancel__source-chip--active:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}.sbd-cancel__source-chip--no-refund{border-color:#fecaca;color:#b91c1c;background:#fff5f5}.sbd-cancel__source-chip--no-refund:hover{border-color:#fca5a5;background:#fee2e2;color:#b91c1c}.sbd-cancel__source-chip--active.sbd-cancel__source-chip--no-refund{background:#fee2e2;color:#991b1b;border-color:#fca5a5}.sbd-cancel__source-chip--active.sbd-cancel__source-chip--no-refund:hover{background:#fecaca}.sbd-cancel__footer{display:flex;justify-content:flex-end;gap:10px;align-items:stretch;padding:14px 20px;border-top:1px solid #e2e8f0;background:#fff}.sbd-cancel__btn-danger-pill{flex:1 1 0;border-radius:999px!important;background:#fee2e2!important;border-color:#fee2e2!important;color:#dc2626!important;font-weight:700!important}.sbd-cancel__btn-danger-pill:hover:not(:disabled){background:#fecaca!important;border-color:#fecaca!important}.sbd-cancel__btn-danger-pill:disabled{opacity:.5}.sbd-cancel__method-header{display:flex;align-items:flex-start;gap:12px;padding:20px 20px 0}.sbd-cancel__method-close{flex-shrink:0;width:32px;height:32px;border-radius:50%;background:#f8fafc;border:none;color:#1e2e4a;display:flex;align-items:center;justify-content:center;cursor:pointer}.sbd-cancel__method-close .pi{font-size:13px}.sbd-cancel__method-close:hover{background:#e2e8f0}.sbd-cancel__method-header-text{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;text-align:center;margin-right:32px}.sbd-cancel__method-title{font-size:19px;font-weight:800;color:#1e2e4a;margin:0}.sbd-cancel__method-sub{font-size:12.5px;color:#94a3b8}.sbd-cancel__method-body{padding:16px 20px 20px;display:flex;flex-direction:column;gap:14px}.sbd-cancel__select-leg-body{padding:16px 20px 20px;display:flex;flex-direction:column;gap:12px}.sbd-cancel__leg-select-card{position:relative;border:1.5px solid #e2e8f0;border-radius:16px;padding:16px 16px 16px 48px;transition:border-color .15s}.sbd-cancel__leg-select-card--selected:not(.sbd-cancel__leg-select-card--frozen){border-color:#6d4aff}.sbd-cancel__leg-select-card--disabled{opacity:.7}.sbd-cancel__leg-select-card--frozen{opacity:.6;border-color:#e2e8f0}.sbd-cancel__leg-select-lock{position:absolute;top:16px;left:16px;width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:14px;color:#94a3b8}.sbd-cancel__single-leg-note{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:12px;background:#eaf2ff;border:1px solid #b9d6ff;color:#1a5fd6;font-size:13px;line-height:1.5}.sbd-cancel__single-leg-note i{flex-shrink:0;margin-top:1px}.sbd-cancel__all-non-refundable-note{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:12px;background:#fdf3e3;border:1px solid #f3d9a4;color:#8a5a1e;font-size:13px;line-height:1.5}.sbd-cancel__all-non-refundable-note i{flex-shrink:0;margin-top:1px;color:#b98a3e}.sbd-cancel__leg-select-checkbox{position:absolute;top:16px;left:16px;width:22px;height:22px;cursor:pointer}.sbd-cancel__leg-select-checkbox input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}.sbd-cancel__leg-select-checkbox input:disabled{cursor:not-allowed}.sbd-cancel__leg-select-icon{display:flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;border:1.5px solid #cbd5e1;background:#fff;color:#fff;font-size:12px}input:checked+.sbd-cancel__leg-select-icon{background:#6d4aff;border-color:#6d4aff}input:disabled+.sbd-cancel__leg-select-icon{background:#f1f5f9;border-color:#e2e8f0}.sbd-cancel__leg-select-content{display:flex;flex-direction:column;gap:8px}.sbd-cancel__leg-select-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.sbd-cancel__leg-select-name{font-size:15px;font-weight:700;color:#1e2e4a}.sbd-cancel__leg-select-date{font-size:13px;color:#64748b;margin-top:2px}.sbd-cancel__leg-select-times{display:flex;align-items:center;gap:8px}.sbd-cancel__leg-select-time{font-size:18px;font-weight:700;color:#1e2e4a;font-variant-numeric:tabular-nums}.sbd-cancel__leg-select-line{flex:1;position:relative;display:flex;flex-direction:column;align-items:center;gap:2px;padding:0 16px 0 8px}.sbd-cancel__leg-select-line:before{content:\"\";position:absolute;left:0;right:14px;top:50%;height:2px;background:#cbd5e1;z-index:1}.sbd-cancel__leg-select-line-stops{font-size:12px;color:#64748b}.sbd-cancel__leg-select-line-dur{font-size:11px;color:#94a3b8}.sbd-cancel__leg-select-line-arrow{position:absolute;right:4px;top:51%;transform:translateY(-50%);font-size:11px;color:#64748b}.sbd-cancel__leg-select-footer{display:flex;align-items:center;gap:8px}.sbd-cancel__leg-select-airline{font-size:12.5px;color:#64748b}.sbd-cancel__leg-select-operated-by{font-size:12px;color:#94a3b8}.sbd-cancel__leg-select-chevron{font-size:12px;color:#94a3b8;margin-inline-start:auto}.sbd-cancel__leg-select-hint{text-align:center;padding:8px 20px 0;font-size:13px;color:#94a3b8}.sbd-cancel__leg-select-notice{display:flex;align-items:flex-start;gap:8px;padding:10px 12px;border-radius:10px;background:#fdf1e0;color:#6b4a16;font-size:12.5px;line-height:1.4}.sbd-cancel__leg-select-notice i{flex-shrink:0;margin-top:1px;color:#b98a3e}.sbd-cancel__method-amount{display:flex;align-items:center;justify-content:space-between}.sbd-cancel__method-amount span{font-size:14px;color:#1e2e4a}.sbd-cancel__method-amount strong{font-size:22px;font-weight:800;color:#1e2e4a;font-variant-numeric:tabular-nums}.sbd-cancel__method-option{display:flex;align-items:flex-start;gap:12px;width:100%;text-align:left;padding:16px;border-radius:16px;border:1.5px solid #e2e8f0;background:#fff;cursor:pointer;transition:border-color .15s,box-shadow .15s}.sbd-cancel__method-option--active{border-color:#6d4aff;box-shadow:0 0 0 1px #6d4aff}.sbd-cancel__method-option:disabled{opacity:.5;cursor:not-allowed}.sbd-cancel__method-icon{flex-shrink:0;width:44px;height:44px;border-radius:10px;display:flex;align-items:center;justify-content:center;border:1px solid #e2e8f0;color:#1e2e4a}.sbd-cancel__method-icon .pi{font-size:16px}.sbd-cancel__method-body-text{flex:1;display:flex;flex-direction:column;align-items:flex-start;gap:4px}.sbd-cancel__method-name{font-size:16px;font-weight:700;color:#1e2e4a;display:inline-flex;align-items:center;gap:6px}.sbd-cancel__method-desc{font-size:13px;color:#64748b}.sbd-cancel__method-badge{display:inline-flex;align-items:center;gap:5px;margin-top:4px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;padding:4px 10px;border-radius:20px}.sbd-cancel__method-badge svg{flex-shrink:0}.sbd-cancel__method-badge--instant{background:#dcfce7;color:#166534}.sbd-cancel__method-badge--days,.sbd-cancel__method-badge--loyalty{background:#eff6ff;color:#1e40af}.sbd-cancel__option-end{flex-shrink:0;display:flex;align-items:center;gap:8px}.sbd-cancel__method-radio{flex-shrink:0;width:18px;height:18px;margin-top:2px;border-radius:50%;border:2px solid #7c3aed;position:relative}.sbd-cancel__method-option--active .sbd-cancel__method-radio{border-color:#6d4aff}.sbd-cancel__method-option--active .sbd-cancel__method-radio:after{content:\"\";position:absolute;inset:3px;border-radius:50%;background:#6d4aff}.sbd-cancel__method-option--split{flex-wrap:wrap}.sbd-cancel__method-split-total{flex-shrink:0;font-size:16px;font-weight:700;color:#1e2e4a;font-variant-numeric:tabular-nums}.sbd-cancel__method-split-breakdown{flex-basis:100%;display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px;margin-top:12px;padding-top:12px;border-top:1px solid #e2e8f0}.sbd-cancel__method-split-row{flex:1 1 auto;display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;font-size:14px;color:#1e2e4a}.sbd-cancel__method-split-row strong{font-weight:700;font-variant-numeric:tabular-nums}.sbd-cancel__method-split-label{color:#64748b;display:inline-flex;align-items:center;gap:6px}.sbd-cancel__mada-logo{display:inline-flex;align-items:center;flex-shrink:0}.sbd-cancel__mada-logo svg{display:block}.sbd-cancel__result-hero{display:flex;flex-direction:column;align-items:center;gap:18px;padding:28px 0 8px}.sbd-cancel__result-icon{width:84px;height:84px;border-radius:50%;display:flex;align-items:center;justify-content:center}.sbd-cancel__result-icon .pi{font-size:30px;color:#fff}.sbd-cancel__result-icon--green{background:#4ade80}.sbd-cancel__result-icon--orange{background:#fb923c}.sbd-cancel__result-title{font-size:19px;font-weight:800;color:#1e2e4a;margin:0;text-align:center}.sbd-cancel__result-row{display:flex;align-items:flex-start;gap:12px;width:100%;padding:12px 14px;border-radius:14px;background:#f8fafc}.sbd-cancel__result-body-text{flex:1;min-width:0;display:flex;flex-direction:column;align-items:flex-start;gap:5px}.sbd-cancel__result-body-text .sbd-cancel__method-badge{text-transform:none;letter-spacing:normal;line-height:1.35;white-space:normal}.sbd-cancel__result-sub-rows{display:flex;flex-direction:column;gap:2px;width:100%}.sbd-cancel__result-sub-row{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;font-size:12.5px;color:#64748b}.sbd-cancel__result-sub-row strong{font-weight:700;color:#64748b;font-variant-numeric:tabular-nums}.sbd-cancel__result-amount{font-size:16px;font-weight:800;color:#1e2e4a;font-variant-numeric:tabular-nums;white-space:nowrap}.sbd-cancel__result-overlay{position:absolute;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:24px}.sbd-cancel__result-backdrop{position:absolute;inset:0;background:#0000008c}.sbd-cancel__result-modal{position:relative;width:100%;max-width:400px;max-height:90%;overflow-y:auto;display:flex;flex-direction:column;gap:16px;padding:24px 20px;border-radius:20px;background:#fff;box-shadow:0 24px 60px #00000059}.sbd-cancel__result-done{width:100%}\n"] }]
|
|
2583
|
+
}], ctorParameters: () => [] });
|
|
2584
|
+
|
|
2585
|
+
class SbdFlightItineraryCardComponent {
|
|
2586
|
+
constructor() {
|
|
2587
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2588
|
+
this.data = input(null);
|
|
2589
|
+
this.product = input.required();
|
|
2590
|
+
this.verified = input(false);
|
|
2591
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2592
|
+
this.bi = computed(() => this.data()?.data?.booking_info);
|
|
2593
|
+
this.legs = computed(() => {
|
|
2594
|
+
const d = this.bi();
|
|
2595
|
+
if (!d)
|
|
2596
|
+
return [];
|
|
2597
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2598
|
+
return (d.leg_info ?? []).map((leg, i) => ({
|
|
2599
|
+
direction: leg.LegDirectionality,
|
|
2600
|
+
airlineName: (leg.LegAirlinesNames ?? []).join(', '),
|
|
2601
|
+
cabin: leg.LegCabins?.[0]?.Name ?? '',
|
|
2602
|
+
fareBrand: leg.fareBrand?.name ?? '',
|
|
2603
|
+
from: leg.LegDepartureAirportCode,
|
|
2604
|
+
to: leg.LegArrivalAirportCode,
|
|
2605
|
+
depTime: leg.LegDepartureDateTime,
|
|
2606
|
+
arrTime: leg.LegArrivalDateTime,
|
|
2607
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2608
|
+
stops: (leg.Stops ?? []),
|
|
2609
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2610
|
+
segments: (d.airline_info?.[i]?.Segments ?? []),
|
|
2611
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2612
|
+
durationSegments: (d.duration?.legs?.[i]?.Segments ?? []),
|
|
2613
|
+
}));
|
|
2614
|
+
});
|
|
2615
|
+
this.passengers = computed(() =>
|
|
2616
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2617
|
+
(this.bi()?.passenger_info?.passengers ?? []));
|
|
2618
|
+
this.payment = computed(() => this.bi()?.payment_status);
|
|
2619
|
+
this.typeLabel = { ADT: 'Adult', CNN: 'Child', INF: 'Infant' };
|
|
2620
|
+
}
|
|
2621
|
+
fmtTime(dt) {
|
|
2622
|
+
if (!dt)
|
|
2623
|
+
return '—';
|
|
2624
|
+
try {
|
|
2625
|
+
return new Date(dt).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
2626
|
+
}
|
|
2627
|
+
catch {
|
|
2628
|
+
return dt;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
fmtShortDate(dt) {
|
|
2632
|
+
if (!dt)
|
|
2633
|
+
return '—';
|
|
2634
|
+
try {
|
|
2635
|
+
return new Date(dt).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
|
|
2636
|
+
}
|
|
2637
|
+
catch {
|
|
2638
|
+
return dt;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
fmtMins(mins) {
|
|
2642
|
+
if (mins == null)
|
|
2643
|
+
return '—';
|
|
2644
|
+
return `${Math.floor(mins / 60)}h ${mins % 60}m`;
|
|
2645
|
+
}
|
|
2646
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdFlightItineraryCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2647
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SbdFlightItineraryCardComponent, isStandalone: true, selector: "app-sbd-flight-itinerary-card", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, product: { classPropertyName: "product", publicName: "product", isSignal: true, isRequired: true, transformFunction: null }, verified: { classPropertyName: "verified", publicName: "verified", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"fic\">\n @if (product() === 'flight') {\n\n <!-- \u2500\u2500 Verified pill \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (verified()) {\n <div class=\"fic__verified-bar\">\n <span class=\"fic__verified-pill\">\n <i class=\"pi pi-verified\"></i> Verified with Airlines\n </span>\n </div>\n }\n\n <!-- \u2500\u2500 Legs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (leg of legs(); track leg.direction + $index; let li = $index) {\n <div class=\"fic__leg\" [class.fic__leg--return]=\"leg.direction === 'Return'\">\n\n <!-- Leg header -->\n <div class=\"fic__leg-header\">\n <div class=\"fic__leg-direction\">\n <i class=\"pi\" [class.pi-arrow-right]=\"leg.direction !== 'Return'\" [class.pi-arrow-left]=\"leg.direction === 'Return'\"></i>\n <span>{{ leg.direction }}</span>\n </div>\n <div class=\"fic__leg-chips\">\n @if (leg.cabin) {\n <span class=\"fic__chip fic__chip--cabin\">{{ leg.cabin }}</span>\n }\n @if (leg.fareBrand) {\n <span class=\"fic__chip fic__chip--fare\">{{ leg.fareBrand }}</span>\n }\n </div>\n </div>\n\n <!-- Airline row -->\n @if (leg.airlineName) {\n <div class=\"fic__airline\">\n <i class=\"pi pi-send\"></i>\n <span>{{ leg.airlineName }}</span>\n </div>\n }\n\n <!-- Route timeline -->\n <div class=\"fic__route\">\n\n <!-- Origin -->\n <div class=\"fic__airport\">\n <div class=\"fic__airport-code\">{{ leg.from }}</div>\n <div class=\"fic__airport-time\">{{ fmtTime(leg.depTime) }}</div>\n <div class=\"fic__airport-date\">{{ fmtShortDate(leg.depTime) }}</div>\n </div>\n\n <!-- Segments + stops -->\n <div class=\"fic__timeline\">\n\n <!-- Top row: all labels -->\n <div class=\"fic__tl-top\">\n @for (seg of leg.durationSegments; track seg.DepartureAirportCode; let si = $index) {\n <div class=\"fic__tl-seg\">\n <span class=\"fic__tl-flt\">{{ leg.segments[si]?.AirlineCode }}{{ leg.segments[si]?.FlightNumber }}</span>\n <span class=\"fic__tl-dur\">{{ fmtMins(seg.SegmentElapsedTimeMinutes) }}</span>\n </div>\n @if (si < leg.stops.length) {\n <div class=\"fic__tl-stop\">\n <span class=\"fic__stop-dot\"></span>\n <span class=\"fic__stop-code\">{{ leg.stops[si].StopAirport }}</span>\n <span class=\"fic__stop-sep\">\u00B7</span>\n <span class=\"fic__stop-dur\">{{ fmtMins(leg.stops[si].LayoverDuration) }}</span>\n </div>\n }\n }\n </div>\n\n <!-- Bottom row: single continuous line + one arrow -->\n <div class=\"fic__tl-track\">\n <div class=\"fic__tl-line\"></div>\n <i class=\"pi pi-send fic__tl-plane\"></i>\n </div>\n\n </div>\n\n <!-- Destination -->\n <div class=\"fic__airport fic__airport--right\">\n <div class=\"fic__airport-code\">{{ leg.to }}</div>\n <div class=\"fic__airport-time\">{{ fmtTime(leg.arrTime) }}</div>\n <div class=\"fic__airport-date\">{{ fmtShortDate(leg.arrTime) }}</div>\n </div>\n\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Passengers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (passengers().length > 0) {\n <div class=\"fic__section\">\n <div class=\"fic__section-label\">\n <i class=\"pi pi-users\"></i> Passengers\n <span class=\"fic__section-count\">{{ passengers().length }}</span>\n </div>\n <div class=\"fic__pax-list\">\n @for (p of passengers(); track p.passengerId) {\n <div class=\"fic__pax\">\n <div class=\"fic__pax-top\">\n <span class=\"fic__pax-badge fic__pax-badge--{{ p.type | lowercase }}\">{{ typeLabel[p.type] ?? p.type }}</span>\n <span class=\"fic__pax-name\">{{ p.name?.prefix }} {{ p.name?.givenName }} {{ p.name?.surName }}</span>\n </div>\n <div class=\"fic__pax-bottom\">\n @if (p.identity?.number) {\n <span class=\"fic__pax-doc\"><i class=\"pi pi-id-card\"></i> {{ p.identity?.number }}</span>\n }\n @if (p.email) {\n <span class=\"fic__pax-email\">{{ p.email }}</span>\n }\n </div>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Payment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (payment()) {\n <div class=\"fic__payment\">\n <div class=\"fic__payment-main\">\n <i class=\"pi pi-credit-card fic__payment-icon\"></i>\n <span class=\"fic__payment-amount\">{{ payment()?.amount }} {{ payment()?.currency }}</span>\n <span class=\"fic__payment-status\" [class.fic__payment-status--ok]=\"payment()?.status?.key === 'SUCCESS'\">\n <i class=\"pi\" [class.pi-check-circle]=\"payment()?.status?.key === 'SUCCESS'\" [class.pi-clock]=\"payment()?.status?.key !== 'SUCCESS'\"></i>\n {{ payment()?.status?.name }}\n </span>\n </div>\n @if (payment()?.method || payment()?.date) {\n <div class=\"fic__payment-sub\">\n @if (payment()?.method) { <span>{{ payment()?.method }}</span> }\n @if (payment()?.method && payment()?.date) { <span class=\"fic__dot\">\u00B7</span> }\n @if (payment()?.date) { <span>{{ payment()?.date }}</span> }\n </div>\n }\n </div>\n }\n\n\n }\n</div>\n", styles: [".fic{background:#fff;border:1px solid #e2e8f0;border-radius:12px;overflow:hidden;font-family:inherit}.fic__leg{padding:16px 16px 18px;border-bottom:1px solid #e2e8f0}.fic__leg--return{background:#fafbff}.fic__leg-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.fic__leg-direction{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#1e2e4a}.fic__leg-direction .pi{font-size:10px;color:#475569}.fic__leg-chips{display:flex;gap:5px}.fic__chip{font-size:10.5px;font-weight:600;padding:2px 8px;border-radius:12px;letter-spacing:.02em}.fic__chip--cabin{background:#eff6ff;color:#1d4ed8}.fic__chip--fare{background:#fef9c3;color:#854d0e}.fic__airline{display:flex;align-items:center;gap:5px;font-size:12px;color:#64748b;margin-bottom:14px}.fic__airline .pi{font-size:11px;color:#94a3b8;transform:rotate(45deg)}.fic__route{display:flex;align-items:flex-start;gap:0}.fic__airport{display:flex;flex-direction:column;align-items:flex-start;flex-shrink:0}.fic__airport--right{align-items:flex-end}.fic__airport-code{font-size:26px;font-weight:800;color:#1e2e4a;line-height:1}.fic__airport-time{font-size:13px;font-weight:600;color:#334155;margin-top:3px}.fic__airport-date{font-size:10.5px;color:#94a3b8;margin-top:1px}.fic__timeline{flex:1;display:flex;flex-direction:column;gap:4px;margin:0 4px}.fic__tl-top{display:flex;align-items:flex-end;gap:0}.fic__tl-seg{flex:1;display:flex;flex-direction:column;align-items:center;gap:1px}.fic__tl-flt{font-size:10px;font-weight:700;color:#1e2e4a;background:#f1f5f9;padding:1px 5px;border-radius:4px;white-space:nowrap}.fic__tl-dur{font-size:9.5px;color:#94a3b8;white-space:nowrap}.fic__tl-stop{flex-shrink:0;display:inline-flex;align-items:center;gap:4px;padding:0 8px 1px}.fic__tl-track{display:flex;align-items:center;height:22px}.fic__tl-line{flex:1;height:2px;background:#cbd5e1}.fic__tl-plane{font-size:13px;color:#475569;transform:rotate(45deg);flex-shrink:0;margin-left:-2px}.fic__stop-dot{width:7px;height:7px;border-radius:50%;background:#94a3b8;flex-shrink:0}.fic__stop-code{font-size:11px;font-weight:700;color:#334155}.fic__stop-sep{font-size:11px;color:#cbd5e1}.fic__stop-dur{font-size:11px;font-weight:600;color:#f59e0b}.fic__section{padding:14px 16px;border-bottom:1px solid #e2e8f0}.fic__section-label{display:flex;align-items:center;gap:5px;font-size:10.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:#64748b;margin-bottom:10px}.fic__section-label .pi{font-size:11px}.fic__section-count{margin-left:auto;background:#f1f5f9;color:#475569;font-size:10px;font-weight:700;padding:1px 6px;border-radius:10px}.fic__pax-list{display:flex;flex-direction:column;gap:8px}.fic__pax{background:#f8fafc;border-radius:8px;padding:8px 10px;display:flex;flex-direction:column;gap:5px}.fic__pax-top{display:flex;align-items:center;gap:7px}.fic__pax-badge{font-size:9.5px;font-weight:700;letter-spacing:.04em;padding:2px 7px;border-radius:10px;flex-shrink:0;text-transform:uppercase}.fic__pax-badge--adt{background:#dbeafe;color:#1d4ed8}.fic__pax-badge--cnn{background:#dcfce7;color:#15803d}.fic__pax-badge--inf{background:#fce7f3;color:#9d174d}.fic__pax-name{font-size:12.5px;font-weight:600;color:#1e2e4a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fic__pax-bottom{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.fic__pax-doc{display:inline-flex;align-items:center;gap:3px;font-size:11px;font-family:monospace;color:#475569;background:#f1f5f9;padding:1px 6px;border-radius:4px}.fic__pax-doc .pi{font-size:10px}.fic__pax-email{font-size:11px;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px}.fic__payment{padding:12px 16px;background:#f8fafc;display:flex;flex-direction:column;gap:4px}.fic__payment-main{display:flex;align-items:center;gap:8px}.fic__payment-icon{font-size:14px;color:#64748b}.fic__payment-amount{font-size:14px;font-weight:700;color:#1e2e4a}.fic__payment-status{display:flex;align-items:center;gap:4px;font-size:12px;font-weight:600;color:#64748b;margin-left:auto}.fic__payment-status .pi{font-size:12px}.fic__payment-status--ok{color:#16a34a}.fic__payment-sub{display:flex;align-items:center;gap:6px;font-size:11.5px;color:#94a3b8;padding-left:22px}.fic__dot{color:#cbd5e1}.fic__verified-bar{display:flex;align-items:center;padding:8px 12px;background:#f0fdf4;border-bottom:1px solid #bbf7d0}.fic__verified-pill{display:inline-flex;align-items:center;gap:5px;padding:3px 10px 3px 7px;background:#16a34a;color:#fff;font-size:11px;font-weight:700;border-radius:20px;letter-spacing:.02em}.fic__verified-pill .pi{font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: TagModule }, { kind: "pipe", type: LowerCasePipe, name: "lowercase" }] }); }
|
|
2648
|
+
}
|
|
2649
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdFlightItineraryCardComponent, decorators: [{
|
|
2650
|
+
type: Component,
|
|
2651
|
+
args: [{ selector: 'app-sbd-flight-itinerary-card', standalone: true, imports: [TagModule, UpperCasePipe, LowerCasePipe], template: "<div class=\"fic\">\n @if (product() === 'flight') {\n\n <!-- \u2500\u2500 Verified pill \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (verified()) {\n <div class=\"fic__verified-bar\">\n <span class=\"fic__verified-pill\">\n <i class=\"pi pi-verified\"></i> Verified with Airlines\n </span>\n </div>\n }\n\n <!-- \u2500\u2500 Legs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (leg of legs(); track leg.direction + $index; let li = $index) {\n <div class=\"fic__leg\" [class.fic__leg--return]=\"leg.direction === 'Return'\">\n\n <!-- Leg header -->\n <div class=\"fic__leg-header\">\n <div class=\"fic__leg-direction\">\n <i class=\"pi\" [class.pi-arrow-right]=\"leg.direction !== 'Return'\" [class.pi-arrow-left]=\"leg.direction === 'Return'\"></i>\n <span>{{ leg.direction }}</span>\n </div>\n <div class=\"fic__leg-chips\">\n @if (leg.cabin) {\n <span class=\"fic__chip fic__chip--cabin\">{{ leg.cabin }}</span>\n }\n @if (leg.fareBrand) {\n <span class=\"fic__chip fic__chip--fare\">{{ leg.fareBrand }}</span>\n }\n </div>\n </div>\n\n <!-- Airline row -->\n @if (leg.airlineName) {\n <div class=\"fic__airline\">\n <i class=\"pi pi-send\"></i>\n <span>{{ leg.airlineName }}</span>\n </div>\n }\n\n <!-- Route timeline -->\n <div class=\"fic__route\">\n\n <!-- Origin -->\n <div class=\"fic__airport\">\n <div class=\"fic__airport-code\">{{ leg.from }}</div>\n <div class=\"fic__airport-time\">{{ fmtTime(leg.depTime) }}</div>\n <div class=\"fic__airport-date\">{{ fmtShortDate(leg.depTime) }}</div>\n </div>\n\n <!-- Segments + stops -->\n <div class=\"fic__timeline\">\n\n <!-- Top row: all labels -->\n <div class=\"fic__tl-top\">\n @for (seg of leg.durationSegments; track seg.DepartureAirportCode; let si = $index) {\n <div class=\"fic__tl-seg\">\n <span class=\"fic__tl-flt\">{{ leg.segments[si]?.AirlineCode }}{{ leg.segments[si]?.FlightNumber }}</span>\n <span class=\"fic__tl-dur\">{{ fmtMins(seg.SegmentElapsedTimeMinutes) }}</span>\n </div>\n @if (si < leg.stops.length) {\n <div class=\"fic__tl-stop\">\n <span class=\"fic__stop-dot\"></span>\n <span class=\"fic__stop-code\">{{ leg.stops[si].StopAirport }}</span>\n <span class=\"fic__stop-sep\">\u00B7</span>\n <span class=\"fic__stop-dur\">{{ fmtMins(leg.stops[si].LayoverDuration) }}</span>\n </div>\n }\n }\n </div>\n\n <!-- Bottom row: single continuous line + one arrow -->\n <div class=\"fic__tl-track\">\n <div class=\"fic__tl-line\"></div>\n <i class=\"pi pi-send fic__tl-plane\"></i>\n </div>\n\n </div>\n\n <!-- Destination -->\n <div class=\"fic__airport fic__airport--right\">\n <div class=\"fic__airport-code\">{{ leg.to }}</div>\n <div class=\"fic__airport-time\">{{ fmtTime(leg.arrTime) }}</div>\n <div class=\"fic__airport-date\">{{ fmtShortDate(leg.arrTime) }}</div>\n </div>\n\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Passengers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (passengers().length > 0) {\n <div class=\"fic__section\">\n <div class=\"fic__section-label\">\n <i class=\"pi pi-users\"></i> Passengers\n <span class=\"fic__section-count\">{{ passengers().length }}</span>\n </div>\n <div class=\"fic__pax-list\">\n @for (p of passengers(); track p.passengerId) {\n <div class=\"fic__pax\">\n <div class=\"fic__pax-top\">\n <span class=\"fic__pax-badge fic__pax-badge--{{ p.type | lowercase }}\">{{ typeLabel[p.type] ?? p.type }}</span>\n <span class=\"fic__pax-name\">{{ p.name?.prefix }} {{ p.name?.givenName }} {{ p.name?.surName }}</span>\n </div>\n <div class=\"fic__pax-bottom\">\n @if (p.identity?.number) {\n <span class=\"fic__pax-doc\"><i class=\"pi pi-id-card\"></i> {{ p.identity?.number }}</span>\n }\n @if (p.email) {\n <span class=\"fic__pax-email\">{{ p.email }}</span>\n }\n </div>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Payment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (payment()) {\n <div class=\"fic__payment\">\n <div class=\"fic__payment-main\">\n <i class=\"pi pi-credit-card fic__payment-icon\"></i>\n <span class=\"fic__payment-amount\">{{ payment()?.amount }} {{ payment()?.currency }}</span>\n <span class=\"fic__payment-status\" [class.fic__payment-status--ok]=\"payment()?.status?.key === 'SUCCESS'\">\n <i class=\"pi\" [class.pi-check-circle]=\"payment()?.status?.key === 'SUCCESS'\" [class.pi-clock]=\"payment()?.status?.key !== 'SUCCESS'\"></i>\n {{ payment()?.status?.name }}\n </span>\n </div>\n @if (payment()?.method || payment()?.date) {\n <div class=\"fic__payment-sub\">\n @if (payment()?.method) { <span>{{ payment()?.method }}</span> }\n @if (payment()?.method && payment()?.date) { <span class=\"fic__dot\">\u00B7</span> }\n @if (payment()?.date) { <span>{{ payment()?.date }}</span> }\n </div>\n }\n </div>\n }\n\n\n }\n</div>\n", styles: [".fic{background:#fff;border:1px solid #e2e8f0;border-radius:12px;overflow:hidden;font-family:inherit}.fic__leg{padding:16px 16px 18px;border-bottom:1px solid #e2e8f0}.fic__leg--return{background:#fafbff}.fic__leg-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.fic__leg-direction{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#1e2e4a}.fic__leg-direction .pi{font-size:10px;color:#475569}.fic__leg-chips{display:flex;gap:5px}.fic__chip{font-size:10.5px;font-weight:600;padding:2px 8px;border-radius:12px;letter-spacing:.02em}.fic__chip--cabin{background:#eff6ff;color:#1d4ed8}.fic__chip--fare{background:#fef9c3;color:#854d0e}.fic__airline{display:flex;align-items:center;gap:5px;font-size:12px;color:#64748b;margin-bottom:14px}.fic__airline .pi{font-size:11px;color:#94a3b8;transform:rotate(45deg)}.fic__route{display:flex;align-items:flex-start;gap:0}.fic__airport{display:flex;flex-direction:column;align-items:flex-start;flex-shrink:0}.fic__airport--right{align-items:flex-end}.fic__airport-code{font-size:26px;font-weight:800;color:#1e2e4a;line-height:1}.fic__airport-time{font-size:13px;font-weight:600;color:#334155;margin-top:3px}.fic__airport-date{font-size:10.5px;color:#94a3b8;margin-top:1px}.fic__timeline{flex:1;display:flex;flex-direction:column;gap:4px;margin:0 4px}.fic__tl-top{display:flex;align-items:flex-end;gap:0}.fic__tl-seg{flex:1;display:flex;flex-direction:column;align-items:center;gap:1px}.fic__tl-flt{font-size:10px;font-weight:700;color:#1e2e4a;background:#f1f5f9;padding:1px 5px;border-radius:4px;white-space:nowrap}.fic__tl-dur{font-size:9.5px;color:#94a3b8;white-space:nowrap}.fic__tl-stop{flex-shrink:0;display:inline-flex;align-items:center;gap:4px;padding:0 8px 1px}.fic__tl-track{display:flex;align-items:center;height:22px}.fic__tl-line{flex:1;height:2px;background:#cbd5e1}.fic__tl-plane{font-size:13px;color:#475569;transform:rotate(45deg);flex-shrink:0;margin-left:-2px}.fic__stop-dot{width:7px;height:7px;border-radius:50%;background:#94a3b8;flex-shrink:0}.fic__stop-code{font-size:11px;font-weight:700;color:#334155}.fic__stop-sep{font-size:11px;color:#cbd5e1}.fic__stop-dur{font-size:11px;font-weight:600;color:#f59e0b}.fic__section{padding:14px 16px;border-bottom:1px solid #e2e8f0}.fic__section-label{display:flex;align-items:center;gap:5px;font-size:10.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:#64748b;margin-bottom:10px}.fic__section-label .pi{font-size:11px}.fic__section-count{margin-left:auto;background:#f1f5f9;color:#475569;font-size:10px;font-weight:700;padding:1px 6px;border-radius:10px}.fic__pax-list{display:flex;flex-direction:column;gap:8px}.fic__pax{background:#f8fafc;border-radius:8px;padding:8px 10px;display:flex;flex-direction:column;gap:5px}.fic__pax-top{display:flex;align-items:center;gap:7px}.fic__pax-badge{font-size:9.5px;font-weight:700;letter-spacing:.04em;padding:2px 7px;border-radius:10px;flex-shrink:0;text-transform:uppercase}.fic__pax-badge--adt{background:#dbeafe;color:#1d4ed8}.fic__pax-badge--cnn{background:#dcfce7;color:#15803d}.fic__pax-badge--inf{background:#fce7f3;color:#9d174d}.fic__pax-name{font-size:12.5px;font-weight:600;color:#1e2e4a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fic__pax-bottom{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.fic__pax-doc{display:inline-flex;align-items:center;gap:3px;font-size:11px;font-family:monospace;color:#475569;background:#f1f5f9;padding:1px 6px;border-radius:4px}.fic__pax-doc .pi{font-size:10px}.fic__pax-email{font-size:11px;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px}.fic__payment{padding:12px 16px;background:#f8fafc;display:flex;flex-direction:column;gap:4px}.fic__payment-main{display:flex;align-items:center;gap:8px}.fic__payment-icon{font-size:14px;color:#64748b}.fic__payment-amount{font-size:14px;font-weight:700;color:#1e2e4a}.fic__payment-status{display:flex;align-items:center;gap:4px;font-size:12px;font-weight:600;color:#64748b;margin-left:auto}.fic__payment-status .pi{font-size:12px}.fic__payment-status--ok{color:#16a34a}.fic__payment-sub{display:flex;align-items:center;gap:6px;font-size:11.5px;color:#94a3b8;padding-left:22px}.fic__dot{color:#cbd5e1}.fic__verified-bar{display:flex;align-items:center;padding:8px 12px;background:#f0fdf4;border-bottom:1px solid #bbf7d0}.fic__verified-pill{display:inline-flex;align-items:center;gap:5px;padding:3px 10px 3px 7px;background:#16a34a;color:#fff;font-size:11px;font-weight:700;border-radius:20px;letter-spacing:.02em}.fic__verified-pill .pi{font-size:12px}\n"] }]
|
|
2652
|
+
}] });
|
|
2653
|
+
|
|
2654
|
+
class SbdDataSearchComponent {
|
|
2655
|
+
constructor() {
|
|
2656
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2657
|
+
this.data = input(null);
|
|
2658
|
+
this.query = signal('');
|
|
2659
|
+
this.allEntries = computed(() => {
|
|
2660
|
+
const d = this.data()?.data?.booking_info;
|
|
2661
|
+
if (!d)
|
|
2662
|
+
return [];
|
|
2663
|
+
return this.flatten(d);
|
|
2664
|
+
});
|
|
2665
|
+
this.results = computed(() => {
|
|
2666
|
+
const q = this.query().trim().toLowerCase();
|
|
2667
|
+
if (q.length < 2)
|
|
2668
|
+
return [];
|
|
2669
|
+
return this.allEntries()
|
|
2670
|
+
.filter(e => e.key.toLowerCase().includes(q) ||
|
|
2671
|
+
e.rawValue.toLowerCase().includes(q) ||
|
|
2672
|
+
e.displayValue.toLowerCase().includes(q))
|
|
2673
|
+
.slice(0, 50);
|
|
2674
|
+
});
|
|
2675
|
+
this.totalCount = computed(() => {
|
|
2676
|
+
const q = this.query().trim().toLowerCase();
|
|
2677
|
+
if (q.length < 2)
|
|
2678
|
+
return 0;
|
|
2679
|
+
return this.allEntries().filter(e => e.key.toLowerCase().includes(q) ||
|
|
2680
|
+
e.rawValue.toLowerCase().includes(q) ||
|
|
2681
|
+
e.displayValue.toLowerCase().includes(q)).length;
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2685
|
+
flatten(obj, pathParts = []) {
|
|
2686
|
+
if (obj === null || obj === undefined)
|
|
2687
|
+
return [];
|
|
2688
|
+
if (typeof obj !== 'object') {
|
|
2689
|
+
const raw = String(obj);
|
|
2690
|
+
const key = pathParts[pathParts.length - 1] ?? '';
|
|
2691
|
+
const { display, type } = this.formatEntry(key, raw);
|
|
2692
|
+
return [{ path: pathParts.slice(0, -1).join(' › '), key, rawValue: raw, displayValue: display, type }];
|
|
2693
|
+
}
|
|
2694
|
+
if (Array.isArray(obj)) {
|
|
2695
|
+
return obj.flatMap((item, i) => this.flatten(item, [...pathParts, `[${i}]`]));
|
|
2696
|
+
}
|
|
2697
|
+
return Object.entries(obj).flatMap(([k, v]) => this.flatten(v, [...pathParts, k]));
|
|
2698
|
+
}
|
|
2699
|
+
formatEntry(key, raw) {
|
|
2700
|
+
if (/minutes|duration/i.test(key)) {
|
|
2701
|
+
const n = Number(raw);
|
|
2702
|
+
if (!isNaN(n) && raw !== '') {
|
|
2703
|
+
return { display: `${Math.floor(n / 60)}h ${n % 60}m`, type: 'duration' };
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(raw)) {
|
|
2707
|
+
try {
|
|
2708
|
+
return { display: new Date(raw).toLocaleString('en-GB', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }), type: 'date' };
|
|
2709
|
+
}
|
|
2710
|
+
catch { /* fall through */ }
|
|
2711
|
+
}
|
|
2712
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
|
2713
|
+
try {
|
|
2714
|
+
return { display: new Date(raw + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }), type: 'date' };
|
|
2715
|
+
}
|
|
2716
|
+
catch { /* fall through */ }
|
|
2717
|
+
}
|
|
2718
|
+
if (raw === 'true' || raw === 'false')
|
|
2719
|
+
return { display: raw, type: 'bool' };
|
|
2720
|
+
if (raw !== '' && !isNaN(Number(raw)))
|
|
2721
|
+
return { display: raw, type: 'number' };
|
|
2722
|
+
return { display: raw, type: 'text' };
|
|
2723
|
+
}
|
|
2724
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdDataSearchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2725
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SbdDataSearchComponent, isStandalone: true, selector: "app-sbd-data-search", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"sbd-data-search\">\n <div class=\"sbd-data-search__input-wrap\">\n <i class=\"pi pi-search sbd-data-search__icon\"></i>\n <input pInputText class=\"sbd-data-search__input\"\n placeholder=\"Search any field or value from this booking\u2026\"\n [ngModel]=\"query()\" (ngModelChange)=\"query.set($event)\" />\n @if (query().length >= 2) {\n <span class=\"sbd-data-search__count\">{{ totalCount() }} match{{ totalCount() !== 1 ? 'es' : '' }}</span>\n }\n </div>\n\n @if (query().length >= 2 && results().length > 0) {\n <div class=\"sbd-data-search__results\">\n @for (entry of results(); track entry.path + entry.key) {\n <div class=\"sbd-data-search__result\">\n <div class=\"sbd-data-search__result-left\">\n <div class=\"sbd-data-search__result-path\">\n @if (entry.path) { <span class=\"sbd-data-search__result-pathtext\">{{ entry.path }} \u203A</span> }\n <span class=\"sbd-data-search__result-key\">{{ entry.key }}</span>\n </div>\n </div>\n <div class=\"sbd-data-search__result-right\">\n <span class=\"sbd-data-search__type-badge sbd-data-search__type-badge--{{ entry.type }}\">{{ entry.type }}</span>\n <span class=\"sbd-data-search__result-value\">{{ entry.displayValue }}</span>\n </div>\n </div>\n }\n @if (totalCount() > 50) {\n <div class=\"sbd-data-search__more\">+{{ totalCount() - 50 }} more \u2014 refine your search</div>\n }\n </div>\n }\n\n @if (query().length >= 2 && results().length === 0) {\n <div class=\"sbd-data-search__empty\">\n <i class=\"pi pi-inbox\"></i> No fields or values match \"{{ query() }}\"\n </div>\n }\n</div>\n", styles: [".sbd-data-search{background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}.sbd-data-search__input-wrap{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid transparent;transition:border-color .15s}.sbd-data-search__input-wrap:focus-within{border-bottom-color:#e2e8f0}.sbd-data-search__icon{color:#94a3b8;font-size:15px;flex-shrink:0}.sbd-data-search__input{flex:1;border:none!important;outline:none!important;box-shadow:none!important;padding:0!important;font-size:14px;background:transparent!important;color:#1e2e4a}.sbd-data-search__input::placeholder{color:#94a3b8}.sbd-data-search__count{font-size:12px;font-weight:600;color:#6366f1;background:#eef2ff;padding:2px 8px;border-radius:10px;white-space:nowrap}.sbd-data-search__results{max-height:320px;overflow-y:auto}.sbd-data-search__result{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 16px;border-bottom:1px solid #f1f5f9;cursor:default;transition:background .1s}.sbd-data-search__result:last-child{border-bottom:none}.sbd-data-search__result:hover{background:#f8fafc}.sbd-data-search__result-left{min-width:0;flex:1;overflow:hidden}.sbd-data-search__result-right{display:flex;align-items:center;gap:8px;flex-shrink:0;max-width:55%}.sbd-data-search__result-path{font-size:11px;color:#94a3b8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sbd-data-search__result-pathtext{color:#cbd5e1}.sbd-data-search__result-key{font-weight:600;color:#475569}.sbd-data-search__result-value{font-size:13px;font-weight:600;color:#1e2e4a;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:monospace}.sbd-data-search__type-badge{font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:2px 6px;border-radius:4px;white-space:nowrap;flex-shrink:0}.sbd-data-search__type-badge--date{background:#eff6ff;color:#3b82f6}.sbd-data-search__type-badge--duration{background:#fff7ed;color:#f97316}.sbd-data-search__type-badge--bool{background:#f0fdf4;color:#22c55e}.sbd-data-search__type-badge--number{background:#faf5ff;color:#a855f7}.sbd-data-search__type-badge--text{background:#f8fafc;color:#64748b}.sbd-data-search__more{padding:10px 16px;font-size:12px;color:#94a3b8;text-align:center;font-style:italic}.sbd-data-search__empty{display:flex;align-items:center;gap:8px;padding:16px;font-size:13px;color:#94a3b8}.sbd-data-search__empty .pi{font-size:16px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i3.InputText, selector: "[pInputText]" }] }); }
|
|
2726
|
+
}
|
|
2727
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdDataSearchComponent, decorators: [{
|
|
2728
|
+
type: Component,
|
|
2729
|
+
args: [{ selector: 'app-sbd-data-search', standalone: true, imports: [FormsModule, InputTextModule], template: "<div class=\"sbd-data-search\">\n <div class=\"sbd-data-search__input-wrap\">\n <i class=\"pi pi-search sbd-data-search__icon\"></i>\n <input pInputText class=\"sbd-data-search__input\"\n placeholder=\"Search any field or value from this booking\u2026\"\n [ngModel]=\"query()\" (ngModelChange)=\"query.set($event)\" />\n @if (query().length >= 2) {\n <span class=\"sbd-data-search__count\">{{ totalCount() }} match{{ totalCount() !== 1 ? 'es' : '' }}</span>\n }\n </div>\n\n @if (query().length >= 2 && results().length > 0) {\n <div class=\"sbd-data-search__results\">\n @for (entry of results(); track entry.path + entry.key) {\n <div class=\"sbd-data-search__result\">\n <div class=\"sbd-data-search__result-left\">\n <div class=\"sbd-data-search__result-path\">\n @if (entry.path) { <span class=\"sbd-data-search__result-pathtext\">{{ entry.path }} \u203A</span> }\n <span class=\"sbd-data-search__result-key\">{{ entry.key }}</span>\n </div>\n </div>\n <div class=\"sbd-data-search__result-right\">\n <span class=\"sbd-data-search__type-badge sbd-data-search__type-badge--{{ entry.type }}\">{{ entry.type }}</span>\n <span class=\"sbd-data-search__result-value\">{{ entry.displayValue }}</span>\n </div>\n </div>\n }\n @if (totalCount() > 50) {\n <div class=\"sbd-data-search__more\">+{{ totalCount() - 50 }} more \u2014 refine your search</div>\n }\n </div>\n }\n\n @if (query().length >= 2 && results().length === 0) {\n <div class=\"sbd-data-search__empty\">\n <i class=\"pi pi-inbox\"></i> No fields or values match \"{{ query() }}\"\n </div>\n }\n</div>\n", styles: [".sbd-data-search{background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}.sbd-data-search__input-wrap{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid transparent;transition:border-color .15s}.sbd-data-search__input-wrap:focus-within{border-bottom-color:#e2e8f0}.sbd-data-search__icon{color:#94a3b8;font-size:15px;flex-shrink:0}.sbd-data-search__input{flex:1;border:none!important;outline:none!important;box-shadow:none!important;padding:0!important;font-size:14px;background:transparent!important;color:#1e2e4a}.sbd-data-search__input::placeholder{color:#94a3b8}.sbd-data-search__count{font-size:12px;font-weight:600;color:#6366f1;background:#eef2ff;padding:2px 8px;border-radius:10px;white-space:nowrap}.sbd-data-search__results{max-height:320px;overflow-y:auto}.sbd-data-search__result{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 16px;border-bottom:1px solid #f1f5f9;cursor:default;transition:background .1s}.sbd-data-search__result:last-child{border-bottom:none}.sbd-data-search__result:hover{background:#f8fafc}.sbd-data-search__result-left{min-width:0;flex:1;overflow:hidden}.sbd-data-search__result-right{display:flex;align-items:center;gap:8px;flex-shrink:0;max-width:55%}.sbd-data-search__result-path{font-size:11px;color:#94a3b8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sbd-data-search__result-pathtext{color:#cbd5e1}.sbd-data-search__result-key{font-weight:600;color:#475569}.sbd-data-search__result-value{font-size:13px;font-weight:600;color:#1e2e4a;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:monospace}.sbd-data-search__type-badge{font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:2px 6px;border-radius:4px;white-space:nowrap;flex-shrink:0}.sbd-data-search__type-badge--date{background:#eff6ff;color:#3b82f6}.sbd-data-search__type-badge--duration{background:#fff7ed;color:#f97316}.sbd-data-search__type-badge--bool{background:#f0fdf4;color:#22c55e}.sbd-data-search__type-badge--number{background:#faf5ff;color:#a855f7}.sbd-data-search__type-badge--text{background:#f8fafc;color:#64748b}.sbd-data-search__more{padding:10px 16px;font-size:12px;color:#94a3b8;text-align:center;font-style:italic}.sbd-data-search__empty{display:flex;align-items:center;gap:8px;padding:16px;font-size:13px;color:#94a3b8}.sbd-data-search__empty .pi{font-size:16px}\n"] }]
|
|
2730
|
+
}] });
|
|
2731
|
+
|
|
2732
|
+
const AR_LABELS = {
|
|
2733
|
+
'PNR': 'رقم الحجز',
|
|
2734
|
+
'Booking ID': 'معرف الحجز',
|
|
2735
|
+
'Supplier ref': 'مرجع المورد',
|
|
2736
|
+
'Supplier': 'المورد',
|
|
2737
|
+
'Cross-seller': 'حجز متقاطع',
|
|
2738
|
+
'Departure': 'المغادرة',
|
|
2739
|
+
'Arrival': 'الوصول',
|
|
2740
|
+
'Duration': 'المدة',
|
|
2741
|
+
'Direction': 'الاتجاه',
|
|
2742
|
+
'Departure time': 'وقت المغادرة',
|
|
2743
|
+
'Arrival time': 'وقت الوصول',
|
|
2744
|
+
'Stops': 'التوقفات',
|
|
2745
|
+
'Journey duration': 'مدة الرحلة',
|
|
2746
|
+
'Airline(s)': 'شركة الطيران',
|
|
2747
|
+
'Flight no.': 'رقم الرحلة',
|
|
2748
|
+
'Flight': 'الرحلة',
|
|
2749
|
+
'IATA code': 'رمز IATA',
|
|
2750
|
+
'Aircraft': 'نوع الطائرة',
|
|
2751
|
+
'Operating code': 'رمز التشغيل',
|
|
2752
|
+
'Route': 'المسار',
|
|
2753
|
+
'Cabin': 'درجة المقصورة',
|
|
2754
|
+
'Fare brand': 'نوع التعرفة',
|
|
2755
|
+
'Fare desc.': 'وصف التعرفة',
|
|
2756
|
+
'Departure airport': 'مطار المغادرة',
|
|
2757
|
+
'Dep. terminal': 'صالة المغادرة',
|
|
2758
|
+
'Arrival airport': 'مطار الوصول',
|
|
2759
|
+
'Arr. terminal': 'صالة الوصول',
|
|
2760
|
+
'Type': 'النوع',
|
|
2761
|
+
'Gender': 'الجنس',
|
|
2762
|
+
'Date of birth': 'تاريخ الميلاد',
|
|
2763
|
+
'Passport': 'جواز السفر',
|
|
2764
|
+
'Nationality': 'الجنسية',
|
|
2765
|
+
'Passport exp.': 'انتهاء الجواز',
|
|
2766
|
+
'Issue country': 'بلد الإصدار',
|
|
2767
|
+
'Doc type': 'نوع الوثيقة',
|
|
2768
|
+
'Email': 'البريد الإلكتروني',
|
|
2769
|
+
'Phone': 'الهاتف',
|
|
2770
|
+
'Total': 'الإجمالي',
|
|
2771
|
+
'Adults (ADT)': 'بالغون',
|
|
2772
|
+
'Children (CNN)': 'أطفال',
|
|
2773
|
+
'Infants (INF)': 'رضع',
|
|
2774
|
+
'Status': 'الحالة',
|
|
2775
|
+
'Amount': 'المبلغ',
|
|
2776
|
+
'Paid amount': 'المبلغ المدفوع',
|
|
2777
|
+
'Payment method': 'طريقة الدفع',
|
|
2778
|
+
'Date': 'التاريخ',
|
|
2779
|
+
'Currency': 'العملة',
|
|
2780
|
+
'Transaction ref': 'مرجع المعاملة',
|
|
2781
|
+
'Amount paid': 'المبلغ المدفوع',
|
|
2782
|
+
'Source': 'المصدر',
|
|
2783
|
+
'Total paid': 'إجمالي المدفوع',
|
|
2784
|
+
'Adult · Cabin': 'بالغ · حقيبة مقصورة',
|
|
2785
|
+
'Adult · Checked': 'بالغ · حقيبة مسجلة',
|
|
2786
|
+
'Child · Cabin': 'طفل · حقيبة مقصورة',
|
|
2787
|
+
'Child · Checked': 'طفل · حقيبة مسجلة',
|
|
2788
|
+
'Infant · Cabin': 'رضيع · حقيبة مقصورة',
|
|
2789
|
+
'Infant · Checked': 'رضيع · حقيبة مسجلة',
|
|
2790
|
+
'Checked': 'حقيبة مسجلة',
|
|
2791
|
+
'Original amount': 'المبلغ الأصلي',
|
|
2792
|
+
'Cancellation fee': 'رسوم الإلغاء',
|
|
2793
|
+
'Total refund': 'إجمالي الاسترداد',
|
|
2794
|
+
'Refundable': 'قابل للاسترداد',
|
|
2795
|
+
'Cancel fee': 'رسوم الإلغاء',
|
|
2796
|
+
'Deadline': 'الموعد النهائي',
|
|
2797
|
+
'Policy': 'السياسة',
|
|
2798
|
+
'Penalty': 'الغرامة',
|
|
2799
|
+
'Non-refundable charge': 'رسوم غير قابلة للاسترداد',
|
|
2800
|
+
'Non-refundable ancillary': 'خدمات إضافية غير قابلة للاسترداد',
|
|
2801
|
+
'Gift deduction': 'خصم الهدية',
|
|
2802
|
+
'Coupon deduction': 'خصم القسيمة',
|
|
2803
|
+
'Loyalty deduction': 'خصم نقاط الولاء',
|
|
2804
|
+
'Card refund': 'استرداد البطاقة',
|
|
2805
|
+
'Credit refund': 'استرداد الرصيد',
|
|
2806
|
+
'Wallet refund': 'استرداد المحفظة',
|
|
2807
|
+
'Loyalty points refund': 'استرداد نقاط الولاء',
|
|
2808
|
+
'Leg total refund': 'إجمالي استرداد هذا الاتجاه',
|
|
2809
|
+
'Room type': 'نوع الغرفة',
|
|
2810
|
+
'Beds': 'الأسرّة',
|
|
2811
|
+
'View': 'الإطلالة',
|
|
2812
|
+
'Floor': 'الطابق',
|
|
2813
|
+
'Size': 'المساحة',
|
|
2814
|
+
'Check-in date': 'تاريخ الوصول',
|
|
2815
|
+
'Check-in time': 'وقت الوصول',
|
|
2816
|
+
'Check-out date': 'تاريخ المغادرة',
|
|
2817
|
+
'Check-out time': 'وقت المغادرة',
|
|
2818
|
+
'Nights': 'عدد الليالي',
|
|
2819
|
+
'Hotel': 'الفندق',
|
|
2820
|
+
'Stars': 'النجوم',
|
|
2821
|
+
'Address': 'العنوان',
|
|
2822
|
+
'City': 'المدينة',
|
|
2823
|
+
'Country': 'الدولة',
|
|
2824
|
+
'Zip code': 'الرمز البريدي',
|
|
2825
|
+
'Landmark': 'معلم قريب',
|
|
2826
|
+
'Balance': 'الرصيد',
|
|
2827
|
+
'Cashback': 'كاش باك',
|
|
2828
|
+
'Promo credit': 'رصيد ترويجي',
|
|
2829
|
+
'Last updated': 'آخر تحديث',
|
|
2830
|
+
'Board basis': 'أساس الإقامة',
|
|
2831
|
+
'Breakfast': 'الإفطار',
|
|
2832
|
+
'Lunch': 'الغداء',
|
|
2833
|
+
'Dinner': 'العشاء',
|
|
2834
|
+
};
|
|
2835
|
+
const AR_SECTION_TITLES = {
|
|
2836
|
+
'Booking Reference': 'مرجع الحجز',
|
|
2837
|
+
'Payment': 'الدفع',
|
|
2838
|
+
'Summary': 'ملخص',
|
|
2839
|
+
'Transaction': 'المعاملة',
|
|
2840
|
+
'References': 'المراجع',
|
|
2841
|
+
'Total': 'الإجمالي',
|
|
2842
|
+
'Info': 'معلومات',
|
|
2843
|
+
'Status': 'الحالة',
|
|
2844
|
+
'Add-on Baggage': 'أمتعة إضافية',
|
|
2845
|
+
'Change History': 'سجل التغييرات',
|
|
2846
|
+
};
|
|
2847
|
+
|
|
2848
|
+
class SbdResultPanelComponent {
|
|
2849
|
+
constructor() {
|
|
2850
|
+
this.toast = inject(SbdToastService);
|
|
2851
|
+
this.actionLabel = input('');
|
|
2852
|
+
this.elapsedMs = input(0);
|
|
2853
|
+
this.status = input('idle');
|
|
2854
|
+
this.result = input(null);
|
|
2855
|
+
this.feedbackGiven = output();
|
|
2856
|
+
}
|
|
2857
|
+
get elapsedSeconds() { return (this.elapsedMs() / 1000).toFixed(2); }
|
|
2858
|
+
copy() {
|
|
2859
|
+
const r = this.result();
|
|
2860
|
+
if (!r)
|
|
2861
|
+
return;
|
|
2862
|
+
const lines = [r.title, r.subtitle ?? '', '', ...r.points.map(p => `${p.label}: ${p.value}`), r.callout ? `\nNote: ${r.callout.message}` : ''].filter(Boolean);
|
|
2863
|
+
navigator.clipboard.writeText(lines.join('\n')).then(() => this.toast.success('Copied to clipboard'));
|
|
2864
|
+
}
|
|
2865
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdResultPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2866
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SbdResultPanelComponent, isStandalone: true, selector: "app-sbd-result-panel", inputs: { actionLabel: { classPropertyName: "actionLabel", publicName: "actionLabel", isSignal: true, isRequired: false, transformFunction: null }, elapsedMs: { classPropertyName: "elapsedMs", publicName: "elapsedMs", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, result: { classPropertyName: "result", publicName: "result", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { feedbackGiven: "feedbackGiven" }, ngImport: i0, template: "<div class=\"sbd-result-panel\">\n @if (status() === 'loading') {\n <div class=\"sbd-result-panel__loading\">\n <div class=\"sbd-result-panel__loading-header\">\n <p-skeleton width=\"200px\" height=\"18px\" /><p-skeleton width=\"80px\" height=\"18px\" />\n </div>\n <p-skeleton width=\"60%\" height=\"28px\" styleClass=\"mt-3 mb-2\" />\n <p-skeleton width=\"40%\" height=\"16px\" styleClass=\"mb-4\" />\n <div class=\"sbd-result-panel__loading-points\">\n @for (i of [1,2,3,4]; track i) {\n <div class=\"sbd-result-panel__loading-row\">\n <p-skeleton width=\"100px\" height=\"13px\" /><p-skeleton width=\"160px\" height=\"13px\" />\n </div>\n }\n </div>\n </div>\n }\n @if (status() === 'success' && result()) {\n <div @fadeInUp>\n <div class=\"sbd-result-panel__header\">\n <div class=\"sbd-result-panel__header-left\">\n <i class=\"pi pi-check-circle sbd-result-panel__check-icon\"></i>\n <span class=\"sbd-result-panel__header-label\">Result \u00B7 {{ actionLabel() }}</span>\n </div>\n <span class=\"sbd-result-panel__elapsed\">Found in {{ elapsedSeconds }}s</span>\n </div>\n <div class=\"sbd-result-panel__body\">\n <h2 class=\"sbd-result-panel__title\">{{ result()!.title }}</h2>\n @if (result()!.subtitle) { <p class=\"sbd-result-panel__subtitle\">{{ result()!.subtitle }}</p> }\n\n <!-- Sectioned layout (refund breakdown etc.) -->\n @if (result()!.sections?.length) {\n <div class=\"sbd-result-panel__sections\">\n @for (section of result()!.sections!; track section.title) {\n <div class=\"sbd-result-panel__section\">\n <div class=\"sbd-result-panel__section-title\">{{ section.title }}</div>\n @for (row of section.rows; track row.label) {\n @if (row.value) {\n <div class=\"sbd-result-panel__section-row\">\n <span class=\"sbd-result-panel__section-row-label\">{{ row.label }}</span>\n <span class=\"sbd-result-panel__section-row-value sbd-result-panel__section-row-value--{{ row.valueClass ?? 'neutral' }}\">{{ row.value }}</span>\n </div>\n } @else {\n <div class=\"sbd-result-panel__section-divider\">{{ row.label }}</div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Flat points layout (all other actions) -->\n @else if (result()!.points?.length) {\n <ul class=\"sbd-result-panel__points\">\n @for (point of result()!.points!; track point.label) {\n <li class=\"sbd-result-panel__point\">\n <span class=\"sbd-result-panel__point-label\">{{ point.label }}</span>\n <span class=\"sbd-result-panel__point-value\">{{ point.value }}</span>\n </li>\n }\n </ul>\n }\n\n @if (result()!.callout) {\n <div class=\"sbd-result-panel__callout sbd-result-panel__callout--{{ result()!.callout!.type }}\">\n <i class=\"pi pi-info-circle\"></i><span>{{ result()!.callout!.message }}</span>\n </div>\n }\n </div>\n <div class=\"sbd-result-panel__footer\">\n <div class=\"sbd-result-panel__feedback\">\n <span class=\"sbd-result-panel__feedback-label\">Helpful?</span>\n <button class=\"sbd-result-panel__feedback-btn sbd-result-panel__feedback-btn--up\" (click)=\"feedbackGiven.emit('up')\">\n <i class=\"pi pi-thumbs-up\"></i>\n </button>\n <button class=\"sbd-result-panel__feedback-btn sbd-result-panel__feedback-btn--down\" (click)=\"feedbackGiven.emit('down')\">\n <i class=\"pi pi-thumbs-down\"></i>\n </button>\n </div>\n </div>\n </div>\n }\n @if (status() === 'error') {\n <div class=\"sbd-result-panel__error\" @fadeInUp>\n <i class=\"pi pi-exclamation-triangle\"></i><span>Could not load this information. Please try again.</span>\n </div>\n }\n @if (status() === 'idle') {\n <div class=\"sbd-result-panel__idle\">\n <i class=\"pi pi-arrow-up-left\"></i><span>Select an action above to see results here</span>\n </div>\n }\n</div>\n", styles: [".sbd-result-panel{background:#fff;border-radius:12px;border:1px solid #e2e8f0;overflow:hidden}.sbd-result-panel__loading{padding:20px}.sbd-result-panel__loading-header{display:flex;justify-content:space-between;margin-bottom:14px}.sbd-result-panel__loading-points{display:flex;flex-direction:column;gap:10px}.sbd-result-panel__loading-row{display:flex;gap:24px}.sbd-result-panel__header{display:flex;align-items:center;justify-content:space-between;padding:12px 20px;background:#00b67a12;border-bottom:1px solid rgba(0,182,122,.15)}.sbd-result-panel__header-left{display:flex;align-items:center;gap:8px}.sbd-result-panel__check-icon{color:#00b67a;font-size:15px}.sbd-result-panel__header-label{font-size:13px;font-weight:600;color:#1e2e4a}.sbd-result-panel__elapsed{font-size:11.5px;font-weight:600;color:#00b67a;background:#00b67a1a;border-radius:20px;padding:2px 10px}.sbd-result-panel__body{padding:20px}.sbd-result-panel__title{font-size:20px;font-weight:800;color:#1e2e4a;margin:0 0 4px}.sbd-result-panel__subtitle{font-size:13px;color:#94a3b8;margin:0 0 14px}.sbd-result-panel__points{list-style:none;margin:0 0 14px;padding:0;display:flex;flex-direction:column}.sbd-result-panel__point{display:flex;align-items:baseline;gap:14px;padding:9px 0;border-bottom:1px solid #f1f5f9}.sbd-result-panel__point:last-child{border-bottom:none}.sbd-result-panel__point:before{content:\"\";width:6px;height:6px;border-radius:50%;background:#3b82f6;flex-shrink:0;margin-top:5px;align-self:flex-start}.sbd-result-panel__point-label{font-size:12px;color:#94a3b8;font-weight:500;width:140px;flex-shrink:0}.sbd-result-panel__point-value{font-size:13px;font-weight:600;color:#1e2e4a;flex:1}.sbd-result-panel__sections{display:flex;flex-direction:column;gap:14px;margin-bottom:14px}.sbd-result-panel__section{border:1px solid #e2e8f0;border-radius:8px;overflow:hidden}.sbd-result-panel__section-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#64748b;background:#f8fafc;padding:7px 12px;border-bottom:1px solid #e2e8f0}.sbd-result-panel__section-row{display:flex;justify-content:space-between;align-items:center;padding:7px 12px;border-bottom:1px solid #f1f5f9;font-size:13px}.sbd-result-panel__section-row:last-child{border-bottom:none}.sbd-result-panel__section-row-label{color:#64748b;font-size:12.5px}.sbd-result-panel__section-row-value{font-weight:600;font-size:13px}.sbd-result-panel__section-row-value--positive{color:#16a34a}.sbd-result-panel__section-row-value--negative{color:#ef4444}.sbd-result-panel__section-row-value--neutral{color:#1e2e4a}.sbd-result-panel__section-divider{padding:5px 12px;font-size:10.5px;font-weight:700;letter-spacing:.05em;color:#94a3b8;background:#f8fafc;border-bottom:1px solid #f1f5f9;text-align:center}.sbd-result-panel__callout{display:flex;align-items:flex-start;gap:8px;padding:12px;border-radius:8px;font-size:12.5px;font-weight:500;border-left:3px solid #6366f1;background:#f8f9ff;color:#1e2e4a}.sbd-result-panel__callout--warning{border-left-color:#f97316;background:#f973160f}.sbd-result-panel__callout--tip{border-left-color:#0ea5e9;background:#0ea5e912}.sbd-result-panel__callout i{font-size:14px;margin-top:1px}.sbd-result-panel__footer{display:flex;align-items:center;justify-content:flex-end;padding:10px 20px;border-top:1px solid #f1f5f9;background:#f8fafc;gap:10px}.sbd-result-panel__feedback{display:flex;align-items:center;gap:6px}.sbd-result-panel__feedback-label{font-size:12px;color:#94a3b8;margin-right:2px}.sbd-result-panel__feedback-btn{background:none;border:1px solid #e2e8f0;border-radius:8px;width:34px;height:34px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .15s}.sbd-result-panel__feedback-btn .pi{font-size:14px;color:#94a3b8}.sbd-result-panel__feedback-btn:hover{border-color:#cbd5e1;background:#f1f5f9}.sbd-result-panel__feedback-btn:hover .pi{color:#475569}.sbd-result-panel__feedback-btn--up:hover{border-color:#86efac;background:#f0fdf4}.sbd-result-panel__feedback-btn--up:hover .pi{color:#16a34a}.sbd-result-panel__feedback-btn--down:hover{border-color:#fca5a5;background:#fef2f2}.sbd-result-panel__feedback-btn--down:hover .pi{color:#ef4444}.sbd-result-panel__error{display:flex;align-items:center;gap:8px;padding:24px;color:#ef4444;font-size:13.5px;font-weight:500}.sbd-result-panel__error i{font-size:18px}.sbd-result-panel__idle{display:flex;align-items:center;gap:8px;padding:24px;color:#94a3b8;font-size:13px}.sbd-result-panel__idle i{font-size:16px}\n"], dependencies: [{ kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: i1$2.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "style", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "ngmodule", type: ButtonModule }], animations: [
|
|
2867
|
+
trigger('fadeInUp', [
|
|
2868
|
+
transition(':enter', [
|
|
2869
|
+
style({ opacity: 0, transform: 'translateY(8px)' }),
|
|
2870
|
+
animate('200ms ease-out', style({ opacity: 1, transform: 'translateY(0)' })),
|
|
2871
|
+
]),
|
|
2872
|
+
]),
|
|
2873
|
+
] }); }
|
|
2874
|
+
}
|
|
2875
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdResultPanelComponent, decorators: [{
|
|
2876
|
+
type: Component,
|
|
2877
|
+
args: [{ selector: 'app-sbd-result-panel', standalone: true, imports: [SkeletonModule, ButtonModule], animations: [
|
|
2878
|
+
trigger('fadeInUp', [
|
|
2879
|
+
transition(':enter', [
|
|
2880
|
+
style({ opacity: 0, transform: 'translateY(8px)' }),
|
|
2881
|
+
animate('200ms ease-out', style({ opacity: 1, transform: 'translateY(0)' })),
|
|
2882
|
+
]),
|
|
2883
|
+
]),
|
|
2884
|
+
], template: "<div class=\"sbd-result-panel\">\n @if (status() === 'loading') {\n <div class=\"sbd-result-panel__loading\">\n <div class=\"sbd-result-panel__loading-header\">\n <p-skeleton width=\"200px\" height=\"18px\" /><p-skeleton width=\"80px\" height=\"18px\" />\n </div>\n <p-skeleton width=\"60%\" height=\"28px\" styleClass=\"mt-3 mb-2\" />\n <p-skeleton width=\"40%\" height=\"16px\" styleClass=\"mb-4\" />\n <div class=\"sbd-result-panel__loading-points\">\n @for (i of [1,2,3,4]; track i) {\n <div class=\"sbd-result-panel__loading-row\">\n <p-skeleton width=\"100px\" height=\"13px\" /><p-skeleton width=\"160px\" height=\"13px\" />\n </div>\n }\n </div>\n </div>\n }\n @if (status() === 'success' && result()) {\n <div @fadeInUp>\n <div class=\"sbd-result-panel__header\">\n <div class=\"sbd-result-panel__header-left\">\n <i class=\"pi pi-check-circle sbd-result-panel__check-icon\"></i>\n <span class=\"sbd-result-panel__header-label\">Result \u00B7 {{ actionLabel() }}</span>\n </div>\n <span class=\"sbd-result-panel__elapsed\">Found in {{ elapsedSeconds }}s</span>\n </div>\n <div class=\"sbd-result-panel__body\">\n <h2 class=\"sbd-result-panel__title\">{{ result()!.title }}</h2>\n @if (result()!.subtitle) { <p class=\"sbd-result-panel__subtitle\">{{ result()!.subtitle }}</p> }\n\n <!-- Sectioned layout (refund breakdown etc.) -->\n @if (result()!.sections?.length) {\n <div class=\"sbd-result-panel__sections\">\n @for (section of result()!.sections!; track section.title) {\n <div class=\"sbd-result-panel__section\">\n <div class=\"sbd-result-panel__section-title\">{{ section.title }}</div>\n @for (row of section.rows; track row.label) {\n @if (row.value) {\n <div class=\"sbd-result-panel__section-row\">\n <span class=\"sbd-result-panel__section-row-label\">{{ row.label }}</span>\n <span class=\"sbd-result-panel__section-row-value sbd-result-panel__section-row-value--{{ row.valueClass ?? 'neutral' }}\">{{ row.value }}</span>\n </div>\n } @else {\n <div class=\"sbd-result-panel__section-divider\">{{ row.label }}</div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Flat points layout (all other actions) -->\n @else if (result()!.points?.length) {\n <ul class=\"sbd-result-panel__points\">\n @for (point of result()!.points!; track point.label) {\n <li class=\"sbd-result-panel__point\">\n <span class=\"sbd-result-panel__point-label\">{{ point.label }}</span>\n <span class=\"sbd-result-panel__point-value\">{{ point.value }}</span>\n </li>\n }\n </ul>\n }\n\n @if (result()!.callout) {\n <div class=\"sbd-result-panel__callout sbd-result-panel__callout--{{ result()!.callout!.type }}\">\n <i class=\"pi pi-info-circle\"></i><span>{{ result()!.callout!.message }}</span>\n </div>\n }\n </div>\n <div class=\"sbd-result-panel__footer\">\n <div class=\"sbd-result-panel__feedback\">\n <span class=\"sbd-result-panel__feedback-label\">Helpful?</span>\n <button class=\"sbd-result-panel__feedback-btn sbd-result-panel__feedback-btn--up\" (click)=\"feedbackGiven.emit('up')\">\n <i class=\"pi pi-thumbs-up\"></i>\n </button>\n <button class=\"sbd-result-panel__feedback-btn sbd-result-panel__feedback-btn--down\" (click)=\"feedbackGiven.emit('down')\">\n <i class=\"pi pi-thumbs-down\"></i>\n </button>\n </div>\n </div>\n </div>\n }\n @if (status() === 'error') {\n <div class=\"sbd-result-panel__error\" @fadeInUp>\n <i class=\"pi pi-exclamation-triangle\"></i><span>Could not load this information. Please try again.</span>\n </div>\n }\n @if (status() === 'idle') {\n <div class=\"sbd-result-panel__idle\">\n <i class=\"pi pi-arrow-up-left\"></i><span>Select an action above to see results here</span>\n </div>\n }\n</div>\n", styles: [".sbd-result-panel{background:#fff;border-radius:12px;border:1px solid #e2e8f0;overflow:hidden}.sbd-result-panel__loading{padding:20px}.sbd-result-panel__loading-header{display:flex;justify-content:space-between;margin-bottom:14px}.sbd-result-panel__loading-points{display:flex;flex-direction:column;gap:10px}.sbd-result-panel__loading-row{display:flex;gap:24px}.sbd-result-panel__header{display:flex;align-items:center;justify-content:space-between;padding:12px 20px;background:#00b67a12;border-bottom:1px solid rgba(0,182,122,.15)}.sbd-result-panel__header-left{display:flex;align-items:center;gap:8px}.sbd-result-panel__check-icon{color:#00b67a;font-size:15px}.sbd-result-panel__header-label{font-size:13px;font-weight:600;color:#1e2e4a}.sbd-result-panel__elapsed{font-size:11.5px;font-weight:600;color:#00b67a;background:#00b67a1a;border-radius:20px;padding:2px 10px}.sbd-result-panel__body{padding:20px}.sbd-result-panel__title{font-size:20px;font-weight:800;color:#1e2e4a;margin:0 0 4px}.sbd-result-panel__subtitle{font-size:13px;color:#94a3b8;margin:0 0 14px}.sbd-result-panel__points{list-style:none;margin:0 0 14px;padding:0;display:flex;flex-direction:column}.sbd-result-panel__point{display:flex;align-items:baseline;gap:14px;padding:9px 0;border-bottom:1px solid #f1f5f9}.sbd-result-panel__point:last-child{border-bottom:none}.sbd-result-panel__point:before{content:\"\";width:6px;height:6px;border-radius:50%;background:#3b82f6;flex-shrink:0;margin-top:5px;align-self:flex-start}.sbd-result-panel__point-label{font-size:12px;color:#94a3b8;font-weight:500;width:140px;flex-shrink:0}.sbd-result-panel__point-value{font-size:13px;font-weight:600;color:#1e2e4a;flex:1}.sbd-result-panel__sections{display:flex;flex-direction:column;gap:14px;margin-bottom:14px}.sbd-result-panel__section{border:1px solid #e2e8f0;border-radius:8px;overflow:hidden}.sbd-result-panel__section-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#64748b;background:#f8fafc;padding:7px 12px;border-bottom:1px solid #e2e8f0}.sbd-result-panel__section-row{display:flex;justify-content:space-between;align-items:center;padding:7px 12px;border-bottom:1px solid #f1f5f9;font-size:13px}.sbd-result-panel__section-row:last-child{border-bottom:none}.sbd-result-panel__section-row-label{color:#64748b;font-size:12.5px}.sbd-result-panel__section-row-value{font-weight:600;font-size:13px}.sbd-result-panel__section-row-value--positive{color:#16a34a}.sbd-result-panel__section-row-value--negative{color:#ef4444}.sbd-result-panel__section-row-value--neutral{color:#1e2e4a}.sbd-result-panel__section-divider{padding:5px 12px;font-size:10.5px;font-weight:700;letter-spacing:.05em;color:#94a3b8;background:#f8fafc;border-bottom:1px solid #f1f5f9;text-align:center}.sbd-result-panel__callout{display:flex;align-items:flex-start;gap:8px;padding:12px;border-radius:8px;font-size:12.5px;font-weight:500;border-left:3px solid #6366f1;background:#f8f9ff;color:#1e2e4a}.sbd-result-panel__callout--warning{border-left-color:#f97316;background:#f973160f}.sbd-result-panel__callout--tip{border-left-color:#0ea5e9;background:#0ea5e912}.sbd-result-panel__callout i{font-size:14px;margin-top:1px}.sbd-result-panel__footer{display:flex;align-items:center;justify-content:flex-end;padding:10px 20px;border-top:1px solid #f1f5f9;background:#f8fafc;gap:10px}.sbd-result-panel__feedback{display:flex;align-items:center;gap:6px}.sbd-result-panel__feedback-label{font-size:12px;color:#94a3b8;margin-right:2px}.sbd-result-panel__feedback-btn{background:none;border:1px solid #e2e8f0;border-radius:8px;width:34px;height:34px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .15s}.sbd-result-panel__feedback-btn .pi{font-size:14px;color:#94a3b8}.sbd-result-panel__feedback-btn:hover{border-color:#cbd5e1;background:#f1f5f9}.sbd-result-panel__feedback-btn:hover .pi{color:#475569}.sbd-result-panel__feedback-btn--up:hover{border-color:#86efac;background:#f0fdf4}.sbd-result-panel__feedback-btn--up:hover .pi{color:#16a34a}.sbd-result-panel__feedback-btn--down:hover{border-color:#fca5a5;background:#fef2f2}.sbd-result-panel__feedback-btn--down:hover .pi{color:#ef4444}.sbd-result-panel__error{display:flex;align-items:center;gap:8px;padding:24px;color:#ef4444;font-size:13.5px;font-weight:500}.sbd-result-panel__error i{font-size:18px}.sbd-result-panel__idle{display:flex;align-items:center;gap:8px;padding:24px;color:#94a3b8;font-size:13px}.sbd-result-panel__idle i{font-size:16px}\n"] }]
|
|
2885
|
+
}] });
|
|
2886
|
+
|
|
2887
|
+
class SbdActionDialogComponent {
|
|
2888
|
+
constructor() {
|
|
2889
|
+
this.bookingService = inject(SbdBookingService);
|
|
2890
|
+
this.toast = inject(SbdToastService);
|
|
2891
|
+
this.visible = input(false);
|
|
2892
|
+
this.bookingId = input('');
|
|
2893
|
+
this.product = input('flight');
|
|
2894
|
+
this.actions = input([]);
|
|
2895
|
+
this.initialKey = input(null);
|
|
2896
|
+
// Pre-fetched booking data from the parent — avoids a redundant API call
|
|
2897
|
+
// when the action's apiField is already satisfied by the initial lookup.
|
|
2898
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2899
|
+
this.preloadedData = input(null);
|
|
2900
|
+
this.closed = output();
|
|
2901
|
+
this.selectedAction = signal(null);
|
|
2902
|
+
this.actionStatus = signal('idle');
|
|
2903
|
+
this.actionData = signal(null);
|
|
2904
|
+
this.elapsedMs = signal(0);
|
|
2905
|
+
this.copying = signal(false);
|
|
2906
|
+
this.resultContent = computed(() => {
|
|
2907
|
+
const a = this.selectedAction();
|
|
2908
|
+
const d = this.actionData();
|
|
2909
|
+
return a && d ? a.formatter(d) : null;
|
|
2910
|
+
});
|
|
2911
|
+
effect(() => {
|
|
2912
|
+
if (this.visible()) {
|
|
2913
|
+
const key = this.initialKey();
|
|
2914
|
+
const action = key ? this.actions().find(a => a.key === key) ?? null : null;
|
|
2915
|
+
if (action) {
|
|
2916
|
+
this.onActionSelect(action);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
else {
|
|
2920
|
+
this.selectedAction.set(null);
|
|
2921
|
+
this.actionStatus.set('idle');
|
|
2922
|
+
this.actionData.set(null);
|
|
2923
|
+
this.elapsedMs.set(0);
|
|
2924
|
+
}
|
|
2925
|
+
}, { allowSignalWrites: true });
|
|
2926
|
+
}
|
|
2927
|
+
copyResult(mode) {
|
|
2928
|
+
const enResult = this.resultContent();
|
|
2929
|
+
const action = this.selectedAction();
|
|
2930
|
+
if (!enResult || !action)
|
|
2931
|
+
return;
|
|
2932
|
+
if (mode === 'en') {
|
|
2933
|
+
const text = this.buildBilingualText(enResult, null);
|
|
2934
|
+
navigator.clipboard.writeText(text).then(() => this.toast.success('Copied EN'));
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
this.copying.set(true);
|
|
2938
|
+
const arData$ = action.apiField === '$refund_details'
|
|
2939
|
+
? this.bookingService.getRefundDetails(this.bookingId(), this.product(), 'ar').pipe(map((data) => data), catchError(() => of(null)))
|
|
2940
|
+
: this.bookingService.getBooking(this.bookingId(), this.product(), action.apiField, 'ar').pipe(map(({ data }) => data), catchError(() => of(null)));
|
|
2941
|
+
arData$.subscribe(arRaw => {
|
|
2942
|
+
const arResult = arRaw ? action.formatter(arRaw) : null;
|
|
2943
|
+
const text = mode === 'ar'
|
|
2944
|
+
? this.buildBilingualText(null, arResult)
|
|
2945
|
+
: this.buildBilingualText(enResult, arResult);
|
|
2946
|
+
const msg = mode === 'ar' ? 'Copied AR' : 'Copied EN + AR';
|
|
2947
|
+
navigator.clipboard.writeText(text)
|
|
2948
|
+
.then(() => this.toast.success(msg))
|
|
2949
|
+
.catch(() => this.toast.success('Copied to clipboard'))
|
|
2950
|
+
.finally(() => this.copying.set(false));
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
buildBilingualText(en, ar) {
|
|
2954
|
+
const LTR = '';
|
|
2955
|
+
const RTL = '';
|
|
2956
|
+
const divider = `${LTR}${'─'.repeat(40)}`;
|
|
2957
|
+
const formatEn = (r) => {
|
|
2958
|
+
const lines = [`${LTR}${r.title}`];
|
|
2959
|
+
if (r.subtitle)
|
|
2960
|
+
lines.push(`${LTR}${r.subtitle}`);
|
|
2961
|
+
lines.push('');
|
|
2962
|
+
if (r.sections?.length) {
|
|
2963
|
+
for (const s of r.sections) {
|
|
2964
|
+
lines.push(`${LTR}── ${s.title} ──`);
|
|
2965
|
+
for (const row of s.rows) {
|
|
2966
|
+
lines.push(row.value ? `${LTR}${row.label}: ${row.value}` : `${LTR}· ${row.label}`);
|
|
2967
|
+
}
|
|
2968
|
+
lines.push('');
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
else if (r.points?.length) {
|
|
2972
|
+
for (const p of r.points)
|
|
2973
|
+
lines.push(`${LTR}${p.label}: ${p.value}`);
|
|
2974
|
+
}
|
|
2975
|
+
if (r.callout)
|
|
2976
|
+
lines.push(`${LTR}Note: ${r.callout.message}`);
|
|
2977
|
+
return lines;
|
|
2978
|
+
};
|
|
2979
|
+
const formatAr = (r) => {
|
|
2980
|
+
const tl = (label) => AR_LABELS[label] ?? label;
|
|
2981
|
+
const ts = (title) => AR_SECTION_TITLES[title] ?? title;
|
|
2982
|
+
const lines = [`${RTL}${r.title}`];
|
|
2983
|
+
if (r.subtitle)
|
|
2984
|
+
lines.push(`${RTL}${r.subtitle}`);
|
|
2985
|
+
lines.push('');
|
|
2986
|
+
if (r.sections?.length) {
|
|
2987
|
+
for (const s of r.sections) {
|
|
2988
|
+
lines.push(`${RTL}── ${ts(s.title)} ──`);
|
|
2989
|
+
for (const row of s.rows) {
|
|
2990
|
+
lines.push(row.value ? `${RTL}${tl(row.label)}: ${row.value}` : `${RTL}· ${tl(row.label)}`);
|
|
2991
|
+
}
|
|
2992
|
+
lines.push('');
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
else if (r.points?.length) {
|
|
2996
|
+
for (const p of r.points)
|
|
2997
|
+
lines.push(`${RTL}${tl(p.label)}: ${p.value}`);
|
|
2998
|
+
}
|
|
2999
|
+
if (r.callout)
|
|
3000
|
+
lines.push(`${RTL}ملاحظة: ${r.callout.message}`);
|
|
3001
|
+
return lines;
|
|
3002
|
+
};
|
|
3003
|
+
if (!en && ar)
|
|
3004
|
+
return formatAr(ar).join('\r\n');
|
|
3005
|
+
if (en && !ar)
|
|
3006
|
+
return formatEn(en).join('\r\n');
|
|
3007
|
+
return [...formatEn(en), '', divider, '', ...formatAr(ar)].join('\r\n');
|
|
3008
|
+
}
|
|
3009
|
+
onActionSelect(action) {
|
|
3010
|
+
if (!action)
|
|
3011
|
+
return;
|
|
3012
|
+
this.selectedAction.set(action);
|
|
3013
|
+
this.actionStatus.set('loading');
|
|
3014
|
+
this.actionData.set(null);
|
|
3015
|
+
// Use pre-fetched data when available and the action doesn't need its own
|
|
3016
|
+
// endpoint (i.e. not the special $refund_details POST).
|
|
3017
|
+
const cached = this.preloadedData();
|
|
3018
|
+
if (action.apiField !== '$refund_details' && cached) {
|
|
3019
|
+
this.actionData.set(cached);
|
|
3020
|
+
this.elapsedMs.set(0);
|
|
3021
|
+
this.actionStatus.set('success');
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
const start = performance.now();
|
|
3025
|
+
const source$ = action.apiField === '$refund_details'
|
|
3026
|
+
? this.bookingService.getRefundDetails(this.bookingId(), this.product()).pipe(map((data) => ({ data, elapsedMs: performance.now() - start })))
|
|
3027
|
+
: this.bookingService.getBooking(this.bookingId(), this.product(), action.apiField);
|
|
3028
|
+
source$.subscribe({
|
|
3029
|
+
next: ({ data, elapsedMs }) => {
|
|
3030
|
+
this.actionData.set(data);
|
|
3031
|
+
this.elapsedMs.set(elapsedMs);
|
|
3032
|
+
this.actionStatus.set('success');
|
|
3033
|
+
},
|
|
3034
|
+
error: () => {
|
|
3035
|
+
this.actionStatus.set('error');
|
|
3036
|
+
},
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdActionDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3040
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.14", type: SbdActionDialogComponent, isStandalone: true, selector: "app-sbd-action-dialog", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, bookingId: { classPropertyName: "bookingId", publicName: "bookingId", isSignal: true, isRequired: false, transformFunction: null }, product: { classPropertyName: "product", publicName: "product", isSignal: true, isRequired: false, transformFunction: null }, actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null }, initialKey: { classPropertyName: "initialKey", publicName: "initialKey", isSignal: true, isRequired: false, transformFunction: null }, preloadedData: { classPropertyName: "preloadedData", publicName: "preloadedData", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, ngImport: i0, template: "<p-dialog\n [visible]=\"visible()\"\n (visibleChange)=\"$event ? null : closed.emit()\"\n [modal]=\"true\"\n [blockScroll]=\"true\"\n [draggable]=\"false\"\n [resizable]=\"false\"\n [style]=\"{ width: '720px', maxHeight: '90vh' }\"\n styleClass=\"sbd-action-dialog-wrapper\"\n header=\"Browse Actions\">\n\n <div class=\"sad\">\n\n <!-- ng-select dropdown -->\n <div class=\"sad__picker\">\n <ng-select\n [items]=\"actions()\"\n [ngModel]=\"selectedAction()\"\n (change)=\"onActionSelect($event)\"\n [searchable]=\"true\"\n [clearable]=\"false\"\n bindLabel=\"label\"\n placeholder=\"Search actions...\"\n class=\"sad__select\">\n\n <!-- Selected value display -->\n <ng-template ng-label-tmp let-item=\"item\">\n <div class=\"sad__option\">\n <i class=\"{{ item.icon }} sad__option-icon\"></i>\n <div class=\"sad__option-text\">\n <span class=\"sad__option-label\">{{ item.label }}</span>\n <span class=\"sad__option-sub\">{{ item.subtitle }}</span>\n </div>\n </div>\n </ng-template>\n\n <!-- Dropdown list item -->\n <ng-template ng-option-tmp let-item=\"item\" let-search=\"searchTerm\">\n <div class=\"sad__option\">\n <i class=\"{{ item.icon }} sad__option-icon\"></i>\n <div class=\"sad__option-text\">\n <span class=\"sad__option-label\">{{ item.label }}</span>\n <span class=\"sad__option-sub\">{{ item.subtitle }}</span>\n </div>\n </div>\n </ng-template>\n\n </ng-select>\n </div>\n\n <!-- Result area -->\n <div class=\"sad__result\">\n <app-sbd-result-panel\n [actionLabel]=\"selectedAction()?.label ?? ''\"\n [elapsedMs]=\"elapsedMs()\"\n [status]=\"actionStatus()\"\n [result]=\"resultContent()\"\n />\n </div>\n\n </div>\n\n <ng-template pTemplate=\"footer\">\n <button pButton label=\"Copy EN\" icon=\"pi pi-copy\"\n class=\"p-button-outlined p-button-sm\"\n [disabled]=\"actionStatus() !== 'success' || copying()\"\n (click)=\"copyResult('en')\">\n </button>\n <button pButton label=\"Copy AR\" icon=\"pi pi-copy\"\n class=\"p-button-outlined p-button-sm\"\n [disabled]=\"actionStatus() !== 'success' || copying()\"\n (click)=\"copyResult('ar')\">\n </button>\n <button pButton label=\"Copy EN + AR\"\n [icon]=\"copying() ? 'pi pi-spin pi-spinner' : 'pi pi-copy'\"\n class=\"p-button-outlined p-button-sm\"\n [disabled]=\"actionStatus() !== 'success' || copying()\"\n (click)=\"copyResult('both')\">\n </button>\n <button pButton label=\"Close\" icon=\"pi pi-times\" class=\"p-button-outlined p-button-secondary p-button-sm\" (click)=\"closed.emit()\"></button>\n </ng-template>\n\n</p-dialog>\n", styles: [".sad{display:flex;flex-direction:column;font-family:inherit}.sad__picker{padding:16px 20px;border-bottom:1px solid #e2e8f0}.sad__select{width:100%}.sad__select ::ng-deep .ng-select-container{height:56px}.sad__option{display:flex;align-items:center;gap:10px;padding:2px 0}.sad__option-icon{font-size:14px;color:#64748b;flex-shrink:0;width:20px;text-align:center}.sad__option-text{display:flex;flex-direction:column;gap:2px;min-width:0}.sad__option-label{font-size:13.5px;font-weight:600;color:#1e2e4a}.sad__option-sub{font-size:11.5px;color:#94a3b8}.sad__result{overflow-y:auto;max-height:calc(90vh - 200px)}.sad__result::-webkit-scrollbar{width:4px}.sad__result::-webkit-scrollbar-track{background:transparent}.sad__result::-webkit-scrollbar-thumb{background:#e2e8f0;border-radius:4px}.sad__result ::ng-deep .sbd-result-panel{border:none;border-radius:0;box-shadow:none}.sad__select.ng-select .ng-select-container{border:1px solid #e2e8f0;border-radius:8px;min-height:48px;font-family:inherit;box-shadow:none}.sad__select.ng-select .ng-select-container:hover{border-color:#94a3b8}.sad__select.ng-select .ng-select-container .ng-value-container{padding:0 12px}.sad__select.ng-select .ng-select-container .ng-value-container .ng-placeholder{color:#94a3b8;font-size:13.5px}.sad__select.ng-select .ng-select-container .ng-value-container .ng-value{font-size:13.5px}.sad__select.ng-select .ng-select-container .ng-arrow-wrapper{padding-right:10px}.sad__select.ng-select .ng-select-container .ng-arrow-wrapper .ng-arrow{border-color:#94a3b8 transparent transparent}.sad__select.ng-select.ng-select-focused .ng-select-container{border-color:#94a3b8;box-shadow:0 0 0 3px #94a3b826}.sad__select.ng-select.ng-select-opened .ng-select-container{border-color:#94a3b8;border-radius:8px 8px 0 0}.sad__select.ng-select.ng-select-opened .ng-select-container .ng-arrow{border-color:transparent transparent #94a3b8}.sad__select .ng-dropdown-panel{border:1px solid #e2e8f0;border-top:none;border-radius:0 0 8px 8px;box-shadow:0 8px 24px #00000014}.sad__select .ng-dropdown-panel .ng-dropdown-panel-items{max-height:520px}.sad__select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option{padding:8px 12px;color:#1e2e4a;font-size:13.5px;background:#fff}.sad__select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option:hover,.sad__select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked{background:#f8fafc}.sad__select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected{background:#f1f5f9;font-weight:500}.sad__select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected .sad__option-label{color:#1e2e4a}.sad__select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected .sad__option-icon{color:#475569}.sad__select .ng-dropdown-panel .ng-dropdown-header{border-bottom:1px solid #f1f5f9;padding:8px 12px}\n"], dependencies: [{ kind: "ngmodule", type: DialogModule }, { kind: "component", type: i1$1.Dialog, selector: "p-dialog", inputs: ["header", "draggable", "resizable", "positionLeft", "positionTop", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "responsive", "appendTo", "breakpoints", "styleClass", "maskStyleClass", "showHeader", "breakpoint", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "visible", "style", "position"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "directive", type: i2$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3$1.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: NgSelectModule }, { kind: "component", type: i5$1.NgSelectComponent, selector: "ng-select", inputs: ["bindLabel", "bindValue", "ariaLabel", "markFirst", "placeholder", "fixedPlaceholder", "notFoundText", "typeToSearchText", "preventToggleOnRightClick", "addTagText", "loadingText", "clearAllText", "appearance", "dropdownPosition", "appendTo", "loading", "closeOnSelect", "hideSelected", "selectOnTab", "openOnEnter", "maxSelectedItems", "groupBy", "groupValue", "bufferAmount", "virtualScroll", "selectableGroup", "selectableGroupAsModel", "searchFn", "trackByFn", "clearOnBackspace", "labelForId", "inputAttrs", "tabIndex", "readonly", "searchWhileComposing", "minTermLength", "editableSearchTerm", "ngClass", "typeahead", "multiple", "addTag", "searchable", "clearable", "isOpen", "items", "compareWith", "clearSearchOnAdd", "deselectOnClick", "keyDownFn"], outputs: ["blur", "focus", "change", "open", "close", "search", "clear", "add", "remove", "scroll", "scrollToEnd"] }, { kind: "directive", type: i5$1.NgOptionTemplateDirective, selector: "[ng-option-tmp]" }, { kind: "directive", type: i5$1.NgLabelTemplateDirective, selector: "[ng-label-tmp]" }, { kind: "component", type: SbdResultPanelComponent, selector: "app-sbd-result-panel", inputs: ["actionLabel", "elapsedMs", "status", "result"], outputs: ["feedbackGiven"] }] }); }
|
|
3041
|
+
}
|
|
3042
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdActionDialogComponent, decorators: [{
|
|
3043
|
+
type: Component,
|
|
3044
|
+
args: [{ selector: 'app-sbd-action-dialog', standalone: true, imports: [DialogModule, ButtonModule, FormsModule, NgSelectModule, SbdResultPanelComponent], template: "<p-dialog\n [visible]=\"visible()\"\n (visibleChange)=\"$event ? null : closed.emit()\"\n [modal]=\"true\"\n [blockScroll]=\"true\"\n [draggable]=\"false\"\n [resizable]=\"false\"\n [style]=\"{ width: '720px', maxHeight: '90vh' }\"\n styleClass=\"sbd-action-dialog-wrapper\"\n header=\"Browse Actions\">\n\n <div class=\"sad\">\n\n <!-- ng-select dropdown -->\n <div class=\"sad__picker\">\n <ng-select\n [items]=\"actions()\"\n [ngModel]=\"selectedAction()\"\n (change)=\"onActionSelect($event)\"\n [searchable]=\"true\"\n [clearable]=\"false\"\n bindLabel=\"label\"\n placeholder=\"Search actions...\"\n class=\"sad__select\">\n\n <!-- Selected value display -->\n <ng-template ng-label-tmp let-item=\"item\">\n <div class=\"sad__option\">\n <i class=\"{{ item.icon }} sad__option-icon\"></i>\n <div class=\"sad__option-text\">\n <span class=\"sad__option-label\">{{ item.label }}</span>\n <span class=\"sad__option-sub\">{{ item.subtitle }}</span>\n </div>\n </div>\n </ng-template>\n\n <!-- Dropdown list item -->\n <ng-template ng-option-tmp let-item=\"item\" let-search=\"searchTerm\">\n <div class=\"sad__option\">\n <i class=\"{{ item.icon }} sad__option-icon\"></i>\n <div class=\"sad__option-text\">\n <span class=\"sad__option-label\">{{ item.label }}</span>\n <span class=\"sad__option-sub\">{{ item.subtitle }}</span>\n </div>\n </div>\n </ng-template>\n\n </ng-select>\n </div>\n\n <!-- Result area -->\n <div class=\"sad__result\">\n <app-sbd-result-panel\n [actionLabel]=\"selectedAction()?.label ?? ''\"\n [elapsedMs]=\"elapsedMs()\"\n [status]=\"actionStatus()\"\n [result]=\"resultContent()\"\n />\n </div>\n\n </div>\n\n <ng-template pTemplate=\"footer\">\n <button pButton label=\"Copy EN\" icon=\"pi pi-copy\"\n class=\"p-button-outlined p-button-sm\"\n [disabled]=\"actionStatus() !== 'success' || copying()\"\n (click)=\"copyResult('en')\">\n </button>\n <button pButton label=\"Copy AR\" icon=\"pi pi-copy\"\n class=\"p-button-outlined p-button-sm\"\n [disabled]=\"actionStatus() !== 'success' || copying()\"\n (click)=\"copyResult('ar')\">\n </button>\n <button pButton label=\"Copy EN + AR\"\n [icon]=\"copying() ? 'pi pi-spin pi-spinner' : 'pi pi-copy'\"\n class=\"p-button-outlined p-button-sm\"\n [disabled]=\"actionStatus() !== 'success' || copying()\"\n (click)=\"copyResult('both')\">\n </button>\n <button pButton label=\"Close\" icon=\"pi pi-times\" class=\"p-button-outlined p-button-secondary p-button-sm\" (click)=\"closed.emit()\"></button>\n </ng-template>\n\n</p-dialog>\n", styles: [".sad{display:flex;flex-direction:column;font-family:inherit}.sad__picker{padding:16px 20px;border-bottom:1px solid #e2e8f0}.sad__select{width:100%}.sad__select ::ng-deep .ng-select-container{height:56px}.sad__option{display:flex;align-items:center;gap:10px;padding:2px 0}.sad__option-icon{font-size:14px;color:#64748b;flex-shrink:0;width:20px;text-align:center}.sad__option-text{display:flex;flex-direction:column;gap:2px;min-width:0}.sad__option-label{font-size:13.5px;font-weight:600;color:#1e2e4a}.sad__option-sub{font-size:11.5px;color:#94a3b8}.sad__result{overflow-y:auto;max-height:calc(90vh - 200px)}.sad__result::-webkit-scrollbar{width:4px}.sad__result::-webkit-scrollbar-track{background:transparent}.sad__result::-webkit-scrollbar-thumb{background:#e2e8f0;border-radius:4px}.sad__result ::ng-deep .sbd-result-panel{border:none;border-radius:0;box-shadow:none}.sad__select.ng-select .ng-select-container{border:1px solid #e2e8f0;border-radius:8px;min-height:48px;font-family:inherit;box-shadow:none}.sad__select.ng-select .ng-select-container:hover{border-color:#94a3b8}.sad__select.ng-select .ng-select-container .ng-value-container{padding:0 12px}.sad__select.ng-select .ng-select-container .ng-value-container .ng-placeholder{color:#94a3b8;font-size:13.5px}.sad__select.ng-select .ng-select-container .ng-value-container .ng-value{font-size:13.5px}.sad__select.ng-select .ng-select-container .ng-arrow-wrapper{padding-right:10px}.sad__select.ng-select .ng-select-container .ng-arrow-wrapper .ng-arrow{border-color:#94a3b8 transparent transparent}.sad__select.ng-select.ng-select-focused .ng-select-container{border-color:#94a3b8;box-shadow:0 0 0 3px #94a3b826}.sad__select.ng-select.ng-select-opened .ng-select-container{border-color:#94a3b8;border-radius:8px 8px 0 0}.sad__select.ng-select.ng-select-opened .ng-select-container .ng-arrow{border-color:transparent transparent #94a3b8}.sad__select .ng-dropdown-panel{border:1px solid #e2e8f0;border-top:none;border-radius:0 0 8px 8px;box-shadow:0 8px 24px #00000014}.sad__select .ng-dropdown-panel .ng-dropdown-panel-items{max-height:520px}.sad__select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option{padding:8px 12px;color:#1e2e4a;font-size:13.5px;background:#fff}.sad__select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option:hover,.sad__select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked{background:#f8fafc}.sad__select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected{background:#f1f5f9;font-weight:500}.sad__select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected .sad__option-label{color:#1e2e4a}.sad__select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected .sad__option-icon{color:#475569}.sad__select .ng-dropdown-panel .ng-dropdown-header{border-bottom:1px solid #f1f5f9;padding:8px 12px}\n"] }]
|
|
3045
|
+
}], ctorParameters: () => [] });
|
|
3046
|
+
|
|
3047
|
+
class SbdAddNoteDialogComponent {
|
|
3048
|
+
constructor() {
|
|
3049
|
+
this.bookingService = inject(SbdBookingService);
|
|
3050
|
+
this.toast = inject(SbdToastService);
|
|
3051
|
+
this.visible = input(false);
|
|
3052
|
+
this.bookingId = input('');
|
|
3053
|
+
this.product = input('flight');
|
|
3054
|
+
this.closed = output();
|
|
3055
|
+
this.message = signal('');
|
|
3056
|
+
this.action = signal('GENERAL');
|
|
3057
|
+
this.submitting = signal(false);
|
|
3058
|
+
this.actionOptions = [
|
|
3059
|
+
{ label: 'General', value: 'GENERAL', icon: 'pi pi-comment' },
|
|
3060
|
+
{ label: 'Feedback', value: 'FEEDBACK', icon: 'pi pi-thumbs-up' },
|
|
3061
|
+
{ label: 'Remark', value: 'REMARK', icon: 'pi pi-exclamation-circle' },
|
|
3062
|
+
];
|
|
3063
|
+
effect(() => {
|
|
3064
|
+
if (!this.visible())
|
|
3065
|
+
this.reset();
|
|
3066
|
+
}, { allowSignalWrites: true });
|
|
3067
|
+
}
|
|
3068
|
+
submit() {
|
|
3069
|
+
const msg = this.message().trim();
|
|
3070
|
+
if (!msg)
|
|
3071
|
+
return;
|
|
3072
|
+
this.submitting.set(true);
|
|
3073
|
+
this.bookingService.addNote(this.bookingId(), this.product(), msg, this.action()).subscribe({
|
|
3074
|
+
next: () => {
|
|
3075
|
+
this.toast.success('Note added successfully');
|
|
3076
|
+
this.closed.emit();
|
|
3077
|
+
},
|
|
3078
|
+
error: () => {
|
|
3079
|
+
this.toast.error('Failed to add note. Please try again.');
|
|
3080
|
+
this.submitting.set(false);
|
|
3081
|
+
},
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
reset() {
|
|
3085
|
+
this.message.set('');
|
|
3086
|
+
this.action.set('GENERAL');
|
|
3087
|
+
this.submitting.set(false);
|
|
3088
|
+
}
|
|
3089
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdAddNoteDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3090
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SbdAddNoteDialogComponent, isStandalone: true, selector: "app-sbd-add-note-dialog", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, bookingId: { classPropertyName: "bookingId", publicName: "bookingId", isSignal: true, isRequired: false, transformFunction: null }, product: { classPropertyName: "product", publicName: "product", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, ngImport: i0, template: "<p-dialog\n [visible]=\"visible()\"\n (visibleChange)=\"$event ? null : closed.emit()\"\n [modal]=\"true\"\n [blockScroll]=\"true\"\n [draggable]=\"false\"\n [resizable]=\"false\"\n [style]=\"{ width: '480px' }\"\n styleClass=\"sbd-add-note-dialog-wrapper\"\n header=\"Add Note\">\n\n <div class=\"and\">\n\n <!-- Note type -->\n <div class=\"and__field\">\n <label class=\"and__label\">Note type</label>\n <div class=\"and__type-group\">\n @for (opt of actionOptions; track opt.value) {\n <button\n type=\"button\"\n class=\"and__type-btn\"\n [class.and__type-btn--active]=\"action() === opt.value\"\n (click)=\"action.set(opt.value)\">\n <i class=\"{{ opt.icon }}\"></i>\n {{ opt.label }}\n </button>\n }\n </div>\n </div>\n\n <!-- Message -->\n <div class=\"and__field\">\n <label class=\"and__label\">\n Message\n <span class=\"and__char-count\">{{ message().length }} / 1000</span>\n </label>\n <textarea\n class=\"and__textarea\"\n [ngModel]=\"message()\"\n (ngModelChange)=\"message.set($event)\"\n placeholder=\"Write your note here...\"\n rows=\"6\"\n maxlength=\"1000\"\n [disabled]=\"submitting()\">\n </textarea>\n </div>\n\n </div>\n\n <ng-template pTemplate=\"footer\">\n <button pButton label=\"Cancel\" icon=\"pi pi-times\"\n class=\"p-button-outlined p-button-secondary p-button-sm\"\n [disabled]=\"submitting()\"\n (click)=\"closed.emit()\">\n </button>\n <button pButton label=\"Submit Note\"\n [icon]=\"submitting() ? 'pi pi-spin pi-spinner' : 'pi pi-check'\"\n class=\"p-button-sm\"\n [disabled]=\"!message().trim() || submitting()\"\n (click)=\"submit()\">\n </button>\n </ng-template>\n\n</p-dialog>\n", styles: [".and{display:flex;flex-direction:column;gap:20px;padding:4px 0}.and__field{display:flex;flex-direction:column;gap:8px}.and__label{display:flex;align-items:center;justify-content:space-between;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#64748b}.and__char-count{font-size:11px;font-weight:500;text-transform:none;letter-spacing:0;color:#94a3b8}.and__type-group{display:flex;gap:8px}.and__type-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:9px 12px;border:1.5px solid #e2e8f0;border-radius:8px;background:#f8fafc;color:#64748b;font-size:12.5px;font-weight:600;cursor:pointer;transition:all .15s ease}.and__type-btn .pi{font-size:13px}.and__type-btn:hover{border-color:#94a3b8;background:#f1f5f9;color:#334155}.and__type-btn--active{border-color:#3b82f6;background:#eff6ff;color:#1d4ed8}.and__type-btn--active:hover{border-color:#2563eb;background:#dbeafe}.and__textarea{width:100%;resize:vertical;padding:10px 12px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:13.5px;font-family:inherit;color:#1e2e4a;background:#fff;line-height:1.55;transition:border-color .15s ease;box-sizing:border-box}.and__textarea::placeholder{color:#94a3b8}.and__textarea:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.and__textarea:disabled{background:#f8fafc;color:#94a3b8;cursor:not-allowed}\n"], dependencies: [{ kind: "ngmodule", type: DialogModule }, { kind: "component", type: i1$1.Dialog, selector: "p-dialog", inputs: ["header", "draggable", "resizable", "positionLeft", "positionTop", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "responsive", "appendTo", "breakpoints", "styleClass", "maskStyleClass", "showHeader", "breakpoint", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "visible", "style", "position"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "directive", type: i2$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3$1.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: NgSelectModule }] }); }
|
|
3091
|
+
}
|
|
3092
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SbdAddNoteDialogComponent, decorators: [{
|
|
3093
|
+
type: Component,
|
|
3094
|
+
args: [{ selector: 'app-sbd-add-note-dialog', standalone: true, imports: [DialogModule, ButtonModule, FormsModule, NgSelectModule], template: "<p-dialog\n [visible]=\"visible()\"\n (visibleChange)=\"$event ? null : closed.emit()\"\n [modal]=\"true\"\n [blockScroll]=\"true\"\n [draggable]=\"false\"\n [resizable]=\"false\"\n [style]=\"{ width: '480px' }\"\n styleClass=\"sbd-add-note-dialog-wrapper\"\n header=\"Add Note\">\n\n <div class=\"and\">\n\n <!-- Note type -->\n <div class=\"and__field\">\n <label class=\"and__label\">Note type</label>\n <div class=\"and__type-group\">\n @for (opt of actionOptions; track opt.value) {\n <button\n type=\"button\"\n class=\"and__type-btn\"\n [class.and__type-btn--active]=\"action() === opt.value\"\n (click)=\"action.set(opt.value)\">\n <i class=\"{{ opt.icon }}\"></i>\n {{ opt.label }}\n </button>\n }\n </div>\n </div>\n\n <!-- Message -->\n <div class=\"and__field\">\n <label class=\"and__label\">\n Message\n <span class=\"and__char-count\">{{ message().length }} / 1000</span>\n </label>\n <textarea\n class=\"and__textarea\"\n [ngModel]=\"message()\"\n (ngModelChange)=\"message.set($event)\"\n placeholder=\"Write your note here...\"\n rows=\"6\"\n maxlength=\"1000\"\n [disabled]=\"submitting()\">\n </textarea>\n </div>\n\n </div>\n\n <ng-template pTemplate=\"footer\">\n <button pButton label=\"Cancel\" icon=\"pi pi-times\"\n class=\"p-button-outlined p-button-secondary p-button-sm\"\n [disabled]=\"submitting()\"\n (click)=\"closed.emit()\">\n </button>\n <button pButton label=\"Submit Note\"\n [icon]=\"submitting() ? 'pi pi-spin pi-spinner' : 'pi pi-check'\"\n class=\"p-button-sm\"\n [disabled]=\"!message().trim() || submitting()\"\n (click)=\"submit()\">\n </button>\n </ng-template>\n\n</p-dialog>\n", styles: [".and{display:flex;flex-direction:column;gap:20px;padding:4px 0}.and__field{display:flex;flex-direction:column;gap:8px}.and__label{display:flex;align-items:center;justify-content:space-between;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#64748b}.and__char-count{font-size:11px;font-weight:500;text-transform:none;letter-spacing:0;color:#94a3b8}.and__type-group{display:flex;gap:8px}.and__type-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:9px 12px;border:1.5px solid #e2e8f0;border-radius:8px;background:#f8fafc;color:#64748b;font-size:12.5px;font-weight:600;cursor:pointer;transition:all .15s ease}.and__type-btn .pi{font-size:13px}.and__type-btn:hover{border-color:#94a3b8;background:#f1f5f9;color:#334155}.and__type-btn--active{border-color:#3b82f6;background:#eff6ff;color:#1d4ed8}.and__type-btn--active:hover{border-color:#2563eb;background:#dbeafe}.and__textarea{width:100%;resize:vertical;padding:10px 12px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:13.5px;font-family:inherit;color:#1e2e4a;background:#fff;line-height:1.55;transition:border-color .15s ease;box-sizing:border-box}.and__textarea::placeholder{color:#94a3b8}.and__textarea:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.and__textarea:disabled{background:#f8fafc;color:#94a3b8;cursor:not-allowed}\n"] }]
|
|
3095
|
+
}], ctorParameters: () => [] });
|
|
3096
|
+
|
|
3097
|
+
class SmartBookingDashboardComponent {
|
|
3098
|
+
constructor() {
|
|
3099
|
+
this.bookingService = inject(SbdBookingService);
|
|
3100
|
+
this.toast = inject(SbdToastService);
|
|
3101
|
+
this.product = signal('flight');
|
|
3102
|
+
this.bookingId = signal('');
|
|
3103
|
+
this.bookingStatus = signal('idle');
|
|
3104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3105
|
+
this.bookingData = signal(null);
|
|
3106
|
+
this.cancelDialogOpen = signal(false);
|
|
3107
|
+
this.actionDialogOpen = signal(false);
|
|
3108
|
+
this.addNoteDialogOpen = signal(false);
|
|
3109
|
+
this.selectedActionKey = signal(null);
|
|
3110
|
+
this.activeContextTab = signal('changes');
|
|
3111
|
+
this.availableActions = computed(() => (this.product() === 'flight' ? FLIGHT_ACTIONS : HOTEL_ACTIONS).filter(a => !a.hidden));
|
|
3112
|
+
// Handle both correct spelling (dataVerified) and BE typo (dataVerfied)
|
|
3113
|
+
this.dataVerified = computed(() => {
|
|
3114
|
+
const bi = this.bookingData()?.data?.booking_info;
|
|
3115
|
+
return bi?.dataVerified === true || bi?.dataVerfied === true;
|
|
3116
|
+
});
|
|
3117
|
+
// PNR list for the active booking, normalized to a flat array regardless of BE shape.
|
|
3118
|
+
this.cancelPnrInfo = computed(() => {
|
|
3119
|
+
const raw = this.bookingData()?.data?.booking_info?.pnr_info;
|
|
3120
|
+
if (Array.isArray(raw))
|
|
3121
|
+
return raw;
|
|
3122
|
+
if (raw?.pnr)
|
|
3123
|
+
return [{ pnr: raw.pnr, LegDirectionality: 'Departure' }];
|
|
3124
|
+
return [];
|
|
3125
|
+
});
|
|
3126
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3127
|
+
this.bookingHistory = computed(() => (this.bookingData()?.data?.booking_info?.history ?? []));
|
|
3128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3129
|
+
this.bookingNotes = computed(() => (this.bookingData()?.data?.booking_info?.notes ?? []));
|
|
3130
|
+
this.historyGroups = computed(() => {
|
|
3131
|
+
const groups = new Map();
|
|
3132
|
+
for (const e of this.bookingHistory()) {
|
|
3133
|
+
if (!groups.has(e.changedAt))
|
|
3134
|
+
groups.set(e.changedAt, { changedBy: e.changedBy, changedAt: e.changedAt, entries: [] });
|
|
3135
|
+
const rawField = e.field.split('.').pop() ?? e.field;
|
|
3136
|
+
groups.get(e.changedAt).entries.push({
|
|
3137
|
+
field: rawField.replace(/\[.*?\]/g, ''),
|
|
3138
|
+
from: e.previousValue,
|
|
3139
|
+
to: e.newValue,
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
return [...groups.values()];
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
fmtNoteDate(date) {
|
|
3146
|
+
if (!date?.sec)
|
|
3147
|
+
return '—';
|
|
3148
|
+
try {
|
|
3149
|
+
return new Date(date.sec * 1000).toLocaleString('en-GB', {
|
|
3150
|
+
day: '2-digit', month: 'short', year: 'numeric',
|
|
3151
|
+
hour: '2-digit', minute: '2-digit',
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
catch {
|
|
3155
|
+
return '—';
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
formatField(field) {
|
|
3159
|
+
return field
|
|
3160
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
3161
|
+
.replace(/([a-z\d])([A-Z])/g, '$1 $2')
|
|
3162
|
+
.trim();
|
|
3163
|
+
}
|
|
3164
|
+
fmtDate(dt) {
|
|
3165
|
+
if (!dt)
|
|
3166
|
+
return '—';
|
|
3167
|
+
try {
|
|
3168
|
+
return new Date(dt).toLocaleString('en-GB', {
|
|
3169
|
+
day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit',
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
catch {
|
|
3173
|
+
return dt;
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
openAction(key) {
|
|
3177
|
+
this.selectedActionKey.set(key);
|
|
3178
|
+
this.actionDialogOpen.set(true);
|
|
3179
|
+
}
|
|
3180
|
+
onKeydown(event) {
|
|
3181
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
|
|
3182
|
+
event.preventDefault();
|
|
3183
|
+
document.querySelector('.sbd-booking-input__field')?.focus();
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
lookupBooking() {
|
|
3187
|
+
const id = this.bookingId().trim();
|
|
3188
|
+
if (!id)
|
|
3189
|
+
return;
|
|
3190
|
+
this.bookingStatus.set('loading');
|
|
3191
|
+
this.bookingService.getBooking(id, this.product(), 'booking_info').subscribe({
|
|
3192
|
+
next: ({ data }) => {
|
|
3193
|
+
this.bookingData.set(data);
|
|
3194
|
+
this.bookingStatus.set('success');
|
|
3195
|
+
},
|
|
3196
|
+
error: () => {
|
|
3197
|
+
this.bookingStatus.set('error');
|
|
3198
|
+
this.bookingData.set(null);
|
|
3199
|
+
this.toast.error('Booking not found. Check the ID and try again.');
|
|
3200
|
+
},
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SmartBookingDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3204
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SmartBookingDashboardComponent, isStandalone: true, selector: "app-smart-booking-dashboard", host: { listeners: { "document:keydown": "onKeydown($event)" } }, ngImport: i0, template: "<div class=\"sbd-page\">\n\n <!-- Search bar full-width at top -->\n <div class=\"sbd-page__search\">\n <app-sbd-booking-input\n [(product)]=\"product\"\n [(bookingId)]=\"bookingId\"\n [loading]=\"bookingStatus() === 'loading'\"\n (lookup)=\"lookupBooking()\"\n />\n </div>\n\n <!-- 2-column layout -->\n <div class=\"sbd-page__content\">\n\n <!-- Left: booking context -->\n <aside class=\"sbd-page__aside\">\n\n <!-- Data search at the top of the sidebar -->\n @if (bookingStatus() === 'success') {\n <app-sbd-data-search [data]=\"bookingData()\" />\n <app-sbd-flight-itinerary-card [product]=\"product()\" [data]=\"bookingData()\" [verified]=\"dataVerified()\" />\n }\n\n <!-- Loading skeleton for aside -->\n @if (bookingStatus() === 'loading') {\n <div class=\"sbd-page__skeleton-card\">\n <div class=\"sbd-page__skeleton-row\" style=\"width:60%\"></div>\n <div class=\"sbd-page__skeleton-row\" style=\"width:40%\"></div>\n <div class=\"sbd-page__skeleton-row\" style=\"width:80%\"></div>\n <div class=\"sbd-page__skeleton-row\" style=\"width:50%\"></div>\n </div>\n }\n\n </aside>\n\n <!-- Right: agent workspace -->\n <main class=\"sbd-page__main\">\n\n <!-- Error state -->\n @if (bookingStatus() === 'error') {\n <div class=\"sbd-page__error\">\n <i class=\"pi pi-exclamation-circle\"></i>\n <span>Booking not found. Double-check the ID and product type, then try again.</span>\n </div>\n }\n\n <!-- Content once loaded -->\n @if (bookingStatus() === 'success') {\n\n <!-- Quick actions -->\n <div class=\"sbd-page__quick-actions\">\n <span class=\"sbd-page__quick-label\">Quick actions</span>\n <app-sbd-workflow-actions\n [bookingId]=\"bookingId()\"\n (cancelBooking)=\"cancelDialogOpen.set(true)\"\n />\n </div>\n\n <!-- Action grid -->\n <app-sbd-action-grid\n [actions]=\"availableActions()\"\n [activeKey]=\"selectedActionKey()\"\n (actionSelected)=\"openAction($event)\"\n />\n\n <!-- Context: Notes + Changes tabbed -->\n <div class=\"sbd-page__context\">\n\n <!-- Tab bar -->\n <div class=\"sbd-page__context-tabs\">\n <button class=\"sbd-page__context-tab\"\n [class.sbd-page__context-tab--active]=\"activeContextTab() === 'changes'\"\n (click)=\"activeContextTab.set('changes')\">\n <i class=\"pi pi-history\"></i>\n Changes\n @if (bookingHistory().length > 0) {\n <span class=\"sbd-page__context-pill sbd-page__context-pill--changes\">{{ bookingHistory().length }}</span>\n }\n </button>\n <button class=\"sbd-page__context-tab\"\n [class.sbd-page__context-tab--active]=\"activeContextTab() === 'notes'\"\n (click)=\"activeContextTab.set('notes')\">\n <i class=\"pi pi-comments\"></i>\n Notes\n <span class=\"sbd-page__context-pill sbd-page__context-pill--notes\">{{ bookingNotes().length }}</span>\n </button>\n @if (activeContextTab() === 'notes') {\n <button pButton label=\"Add Note\" icon=\"pi pi-plus\" class=\"p-button-sm p-button-outlined sbd-page__context-add\"\n (click)=\"addNoteDialogOpen.set(true)\">\n </button>\n }\n </div>\n\n <!-- Notes tab -->\n @if (activeContextTab() === 'notes') {\n <div class=\"sbd-page__notes-list\">\n @if (bookingNotes().length === 0) {\n <div class=\"sbd-page__context-empty\">\n <i class=\"pi pi-inbox\"></i> No notes yet\n </div>\n }\n @for (note of bookingNotes(); track $index) {\n <div class=\"sbd-page__note\">\n <div class=\"sbd-page__note-top\">\n <span class=\"sbd-page__note-badge sbd-page__note-badge--{{ note.action | lowercase }}\">\n {{ note.action }}\n </span>\n <span class=\"sbd-page__note-author\">\n {{ note.writtenBy?.firstName }} {{ note.writtenBy?.lastName }}\n </span>\n <span class=\"sbd-page__note-date\">{{ fmtNoteDate(note.date) }}</span>\n </div>\n <p class=\"sbd-page__note-msg\">{{ note.message }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Changes tab -->\n @if (activeContextTab() === 'changes') {\n <div class=\"sbd-page__changes-groups\">\n @if (historyGroups().length === 0) {\n <div class=\"sbd-page__context-empty\">\n <i class=\"pi pi-check\"></i> No changes recorded\n </div>\n }\n @for (group of historyGroups(); track group.changedAt) {\n <div class=\"sbd-page__changes-group\">\n <div class=\"sbd-page__changes-meta\">\n <i class=\"pi pi-user\"></i>\n <strong>{{ group.changedBy }}</strong>\n <span class=\"sbd-page__changes-sep\">\u00B7</span>\n <span>{{ fmtDate(group.changedAt) }}</span>\n </div>\n <table class=\"sbd-page__changes-table\">\n <thead>\n <tr>\n <th>Field</th>\n <th><i class=\"pi pi-times-circle\"></i> Before</th>\n <th><i class=\"pi pi-check-circle\"></i> After</th>\n </tr>\n </thead>\n <tbody>\n @for (entry of group.entries; track entry.field) {\n <tr>\n <td class=\"sbd-page__changes-field\">{{ formatField(entry.field) }}</td>\n <td class=\"sbd-page__changes-old\">{{ entry.from }}</td>\n <td class=\"sbd-page__changes-new\">{{ entry.to }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n </div>\n }\n\n </div>\n\n }\n\n <!-- Loading skeleton for main -->\n @if (bookingStatus() === 'loading') {\n <div class=\"sbd-page__skeleton-card\">\n <div class=\"sbd-page__skeleton-row\" style=\"width:40%\"></div>\n <div class=\"sbd-page__skeleton-row\" style=\"width:25%\"></div>\n </div>\n }\n\n </main>\n\n </div>\n\n</div>\n\n<app-sbd-cancel-dialog\n [visible]=\"cancelDialogOpen()\"\n [bookingId]=\"bookingId()\"\n [product]=\"product()\"\n [pnrInfo]=\"cancelPnrInfo()\"\n [bookingInfo]=\"bookingData()\"\n (closed)=\"cancelDialogOpen.set(false)\"\n (confirmed)=\"bookingStatus.set('idle'); bookingData.set(null)\"\n/>\n\n<app-sbd-action-dialog\n [visible]=\"actionDialogOpen()\"\n [bookingId]=\"bookingId()\"\n [product]=\"product()\"\n [actions]=\"availableActions()\"\n [initialKey]=\"selectedActionKey()\"\n [preloadedData]=\"bookingData()\"\n (closed)=\"actionDialogOpen.set(false)\"\n/>\n\n<app-sbd-add-note-dialog\n [visible]=\"addNoteDialogOpen()\"\n [bookingId]=\"bookingId()\"\n [product]=\"product()\"\n (closed)=\"addNoteDialogOpen.set(false)\"\n/>\n", styles: [".sbd-page{display:flex;flex-direction:column;gap:20px;padding:24px;max-width:1400px;min-height:100%}.sbd-page__search{width:100%}.sbd-page__error{display:flex;align-items:center;gap:10px;padding:16px 20px;background:#fef2f2;border:1px solid rgba(239,68,68,.2);border-radius:10px;color:#ef4444;font-size:13.5px;font-weight:500}.sbd-page__error i{font-size:18px}.sbd-page__content{display:grid;grid-template-columns:320px 1fr;gap:20px;align-items:start}@media (max-width: 960px){.sbd-page__content{grid-template-columns:1fr}}.sbd-page__aside{display:flex;flex-direction:column;gap:16px;position:sticky;top:20px}.sbd-page__main{display:flex;flex-direction:column;gap:16px}.sbd-page__quick-actions{display:flex;align-items:center;gap:12px;padding:12px 16px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;flex-wrap:wrap}.sbd-page__quick-label{font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8;flex-shrink:0}.sbd-page__context{background:#fff;border:1px solid #e2e8f0;border-radius:12px;overflow:hidden}.sbd-page__context-tabs{display:flex;align-items:center;border-bottom:1px solid #e2e8f0;background:#f8fafc}.sbd-page__context-add{margin-left:auto;margin-right:10px}.sbd-page__context-tab{display:flex;align-items:center;gap:7px;padding:11px 20px;font-size:13px;font-weight:600;color:#64748b;background:transparent;border:none;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:color .15s,border-color .15s}.sbd-page__context-tab .pi{font-size:13px}.sbd-page__context-tab:hover{color:#334155}.sbd-page__context-tab--active{color:#1e2e4a;border-bottom-color:#3b82f6;background:#fff}.sbd-page__context-pill{font-size:10.5px;font-weight:700;padding:1px 7px;border-radius:10px}.sbd-page__context-pill--notes{background:#e0e7ff;color:#4338ca}.sbd-page__context-pill--changes{background:#fef3c7;color:#b45309}.sbd-page__context-empty{display:flex;align-items:center;gap:8px;padding:24px 20px;font-size:13px;color:#94a3b8}.sbd-page__context-empty .pi{font-size:16px}.sbd-page__notes-list{display:flex;flex-direction:column}.sbd-page__note{padding:12px 16px;border-bottom:1px solid #f1f5f9;display:flex;flex-direction:column;gap:6px}.sbd-page__note:last-child{border-bottom:none}.sbd-page__note-top{display:flex;align-items:center;gap:8px}.sbd-page__note-badge{font-size:10px;font-weight:800;letter-spacing:.05em;padding:2px 8px;border-radius:10px;text-transform:uppercase;flex-shrink:0}.sbd-page__note-badge--general{background:#dbeafe;color:#1d4ed8}.sbd-page__note-badge--remark{background:#fef3c7;color:#b45309}.sbd-page__note-badge--feedback{background:#f3e8ff;color:#7e22ce}.sbd-page__note-author{font-size:12px;font-weight:700;color:#1e2e4a}.sbd-page__note-date{margin-left:auto;font-size:11px;color:#94a3b8;white-space:nowrap}.sbd-page__note-msg{margin:0;font-size:13px;color:#334155;line-height:1.55}.sbd-page__changes-groups{display:flex;flex-direction:column}.sbd-page__changes-group{border-bottom:1px solid #f1f5f9}.sbd-page__changes-group:last-child{border-bottom:none}.sbd-page__changes-meta{display:flex;align-items:center;gap:6px;padding:8px 16px;background:#fafafa;border-bottom:1px solid #f1f5f9;font-size:11.5px;color:#64748b}.sbd-page__changes-meta strong{font-weight:700;color:#334155}.sbd-page__changes-meta .pi{font-size:11px;color:#94a3b8}.sbd-page__changes-sep{color:#cbd5e1}.sbd-page__changes-table{width:100%;border-collapse:collapse;font-size:12.5px}.sbd-page__changes-table thead tr{background:#f8fafc;border-bottom:1px solid #e2e8f0}.sbd-page__changes-table thead th{padding:7px 14px;text-align:left;font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:#64748b;white-space:nowrap}.sbd-page__changes-table thead th .pi{margin-right:4px;font-size:10px}.sbd-page__changes-table thead th:nth-child(2){color:#dc2626}.sbd-page__changes-table thead th:nth-child(3){color:#16a34a}.sbd-page__changes-table tbody tr{border-bottom:1px solid #f1f5f9}.sbd-page__changes-table tbody tr:last-child{border-bottom:none}.sbd-page__changes-table tbody tr:hover{background:#f8fafc}.sbd-page__changes-table tbody td{padding:8px 14px;vertical-align:top;word-break:break-word}.sbd-page__changes-field{font-size:12px;font-weight:600;color:#475569;width:28%}.sbd-page__changes-old{color:#dc2626;font-weight:500;text-decoration:line-through;opacity:.8;width:36%}.sbd-page__changes-new{color:#16a34a;font-weight:700;width:36%}.sbd-page__content--loading{opacity:.6;pointer-events:none}.sbd-page__skeleton-card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:12px}.sbd-page__skeleton-row{height:13px;background:#f1f5f9;border-radius:6px;animation:sbd-shimmer 1.4s ease-in-out infinite}.sbd-page__skeleton-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:8px}.sbd-page__skeleton-chip{height:56px;background:#f1f5f9;border-radius:8px;animation:sbd-shimmer 1.4s ease-in-out infinite}@keyframes sbd-shimmer{0%,to{opacity:1}50%{opacity:.45}}@keyframes sbd-fadein{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}\n"], dependencies: [{ kind: "component", type: SbdBookingInputComponent, selector: "app-sbd-booking-input", inputs: ["loading", "product", "bookingId"], outputs: ["productChange", "bookingIdChange", "lookup"] }, { kind: "component", type: SbdWorkflowActionsComponent, selector: "app-sbd-workflow-actions", inputs: ["bookingId"], outputs: ["cancelBooking"] }, { kind: "component", type: SbdActionGridComponent, selector: "app-sbd-action-grid", inputs: ["actions", "activeKey", "loadingKey"], outputs: ["actionSelected"] }, { kind: "component", type: SbdCancelDialogComponent, selector: "app-sbd-cancel-dialog", inputs: ["visible", "bookingId", "product", "pnrInfo", "bookingInfo"], outputs: ["closed", "confirmed"] }, { kind: "component", type: SbdFlightItineraryCardComponent, selector: "app-sbd-flight-itinerary-card", inputs: ["data", "product", "verified"] }, { kind: "component", type: SbdDataSearchComponent, selector: "app-sbd-data-search", inputs: ["data"] }, { kind: "component", type: SbdActionDialogComponent, selector: "app-sbd-action-dialog", inputs: ["visible", "bookingId", "product", "actions", "initialKey", "preloadedData"], outputs: ["closed"] }, { kind: "component", type: SbdAddNoteDialogComponent, selector: "app-sbd-add-note-dialog", inputs: ["visible", "bookingId", "product"], outputs: ["closed"] }, { kind: "pipe", type: LowerCasePipe, name: "lowercase" }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3$1.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading"] }] }); }
|
|
3205
|
+
}
|
|
3206
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SmartBookingDashboardComponent, decorators: [{
|
|
3207
|
+
type: Component,
|
|
3208
|
+
args: [{ selector: 'app-smart-booking-dashboard', standalone: true, imports: [
|
|
3209
|
+
SbdBookingInputComponent,
|
|
3210
|
+
SbdWorkflowActionsComponent,
|
|
3211
|
+
SbdActionGridComponent,
|
|
3212
|
+
SbdCancelDialogComponent,
|
|
3213
|
+
SbdFlightItineraryCardComponent,
|
|
3214
|
+
SbdDataSearchComponent,
|
|
3215
|
+
SbdActionDialogComponent,
|
|
3216
|
+
SbdAddNoteDialogComponent,
|
|
3217
|
+
LowerCasePipe,
|
|
3218
|
+
ButtonModule,
|
|
3219
|
+
], template: "<div class=\"sbd-page\">\n\n <!-- Search bar full-width at top -->\n <div class=\"sbd-page__search\">\n <app-sbd-booking-input\n [(product)]=\"product\"\n [(bookingId)]=\"bookingId\"\n [loading]=\"bookingStatus() === 'loading'\"\n (lookup)=\"lookupBooking()\"\n />\n </div>\n\n <!-- 2-column layout -->\n <div class=\"sbd-page__content\">\n\n <!-- Left: booking context -->\n <aside class=\"sbd-page__aside\">\n\n <!-- Data search at the top of the sidebar -->\n @if (bookingStatus() === 'success') {\n <app-sbd-data-search [data]=\"bookingData()\" />\n <app-sbd-flight-itinerary-card [product]=\"product()\" [data]=\"bookingData()\" [verified]=\"dataVerified()\" />\n }\n\n <!-- Loading skeleton for aside -->\n @if (bookingStatus() === 'loading') {\n <div class=\"sbd-page__skeleton-card\">\n <div class=\"sbd-page__skeleton-row\" style=\"width:60%\"></div>\n <div class=\"sbd-page__skeleton-row\" style=\"width:40%\"></div>\n <div class=\"sbd-page__skeleton-row\" style=\"width:80%\"></div>\n <div class=\"sbd-page__skeleton-row\" style=\"width:50%\"></div>\n </div>\n }\n\n </aside>\n\n <!-- Right: agent workspace -->\n <main class=\"sbd-page__main\">\n\n <!-- Error state -->\n @if (bookingStatus() === 'error') {\n <div class=\"sbd-page__error\">\n <i class=\"pi pi-exclamation-circle\"></i>\n <span>Booking not found. Double-check the ID and product type, then try again.</span>\n </div>\n }\n\n <!-- Content once loaded -->\n @if (bookingStatus() === 'success') {\n\n <!-- Quick actions -->\n <div class=\"sbd-page__quick-actions\">\n <span class=\"sbd-page__quick-label\">Quick actions</span>\n <app-sbd-workflow-actions\n [bookingId]=\"bookingId()\"\n (cancelBooking)=\"cancelDialogOpen.set(true)\"\n />\n </div>\n\n <!-- Action grid -->\n <app-sbd-action-grid\n [actions]=\"availableActions()\"\n [activeKey]=\"selectedActionKey()\"\n (actionSelected)=\"openAction($event)\"\n />\n\n <!-- Context: Notes + Changes tabbed -->\n <div class=\"sbd-page__context\">\n\n <!-- Tab bar -->\n <div class=\"sbd-page__context-tabs\">\n <button class=\"sbd-page__context-tab\"\n [class.sbd-page__context-tab--active]=\"activeContextTab() === 'changes'\"\n (click)=\"activeContextTab.set('changes')\">\n <i class=\"pi pi-history\"></i>\n Changes\n @if (bookingHistory().length > 0) {\n <span class=\"sbd-page__context-pill sbd-page__context-pill--changes\">{{ bookingHistory().length }}</span>\n }\n </button>\n <button class=\"sbd-page__context-tab\"\n [class.sbd-page__context-tab--active]=\"activeContextTab() === 'notes'\"\n (click)=\"activeContextTab.set('notes')\">\n <i class=\"pi pi-comments\"></i>\n Notes\n <span class=\"sbd-page__context-pill sbd-page__context-pill--notes\">{{ bookingNotes().length }}</span>\n </button>\n @if (activeContextTab() === 'notes') {\n <button pButton label=\"Add Note\" icon=\"pi pi-plus\" class=\"p-button-sm p-button-outlined sbd-page__context-add\"\n (click)=\"addNoteDialogOpen.set(true)\">\n </button>\n }\n </div>\n\n <!-- Notes tab -->\n @if (activeContextTab() === 'notes') {\n <div class=\"sbd-page__notes-list\">\n @if (bookingNotes().length === 0) {\n <div class=\"sbd-page__context-empty\">\n <i class=\"pi pi-inbox\"></i> No notes yet\n </div>\n }\n @for (note of bookingNotes(); track $index) {\n <div class=\"sbd-page__note\">\n <div class=\"sbd-page__note-top\">\n <span class=\"sbd-page__note-badge sbd-page__note-badge--{{ note.action | lowercase }}\">\n {{ note.action }}\n </span>\n <span class=\"sbd-page__note-author\">\n {{ note.writtenBy?.firstName }} {{ note.writtenBy?.lastName }}\n </span>\n <span class=\"sbd-page__note-date\">{{ fmtNoteDate(note.date) }}</span>\n </div>\n <p class=\"sbd-page__note-msg\">{{ note.message }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Changes tab -->\n @if (activeContextTab() === 'changes') {\n <div class=\"sbd-page__changes-groups\">\n @if (historyGroups().length === 0) {\n <div class=\"sbd-page__context-empty\">\n <i class=\"pi pi-check\"></i> No changes recorded\n </div>\n }\n @for (group of historyGroups(); track group.changedAt) {\n <div class=\"sbd-page__changes-group\">\n <div class=\"sbd-page__changes-meta\">\n <i class=\"pi pi-user\"></i>\n <strong>{{ group.changedBy }}</strong>\n <span class=\"sbd-page__changes-sep\">\u00B7</span>\n <span>{{ fmtDate(group.changedAt) }}</span>\n </div>\n <table class=\"sbd-page__changes-table\">\n <thead>\n <tr>\n <th>Field</th>\n <th><i class=\"pi pi-times-circle\"></i> Before</th>\n <th><i class=\"pi pi-check-circle\"></i> After</th>\n </tr>\n </thead>\n <tbody>\n @for (entry of group.entries; track entry.field) {\n <tr>\n <td class=\"sbd-page__changes-field\">{{ formatField(entry.field) }}</td>\n <td class=\"sbd-page__changes-old\">{{ entry.from }}</td>\n <td class=\"sbd-page__changes-new\">{{ entry.to }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n </div>\n }\n\n </div>\n\n }\n\n <!-- Loading skeleton for main -->\n @if (bookingStatus() === 'loading') {\n <div class=\"sbd-page__skeleton-card\">\n <div class=\"sbd-page__skeleton-row\" style=\"width:40%\"></div>\n <div class=\"sbd-page__skeleton-row\" style=\"width:25%\"></div>\n </div>\n }\n\n </main>\n\n </div>\n\n</div>\n\n<app-sbd-cancel-dialog\n [visible]=\"cancelDialogOpen()\"\n [bookingId]=\"bookingId()\"\n [product]=\"product()\"\n [pnrInfo]=\"cancelPnrInfo()\"\n [bookingInfo]=\"bookingData()\"\n (closed)=\"cancelDialogOpen.set(false)\"\n (confirmed)=\"bookingStatus.set('idle'); bookingData.set(null)\"\n/>\n\n<app-sbd-action-dialog\n [visible]=\"actionDialogOpen()\"\n [bookingId]=\"bookingId()\"\n [product]=\"product()\"\n [actions]=\"availableActions()\"\n [initialKey]=\"selectedActionKey()\"\n [preloadedData]=\"bookingData()\"\n (closed)=\"actionDialogOpen.set(false)\"\n/>\n\n<app-sbd-add-note-dialog\n [visible]=\"addNoteDialogOpen()\"\n [bookingId]=\"bookingId()\"\n [product]=\"product()\"\n (closed)=\"addNoteDialogOpen.set(false)\"\n/>\n", styles: [".sbd-page{display:flex;flex-direction:column;gap:20px;padding:24px;max-width:1400px;min-height:100%}.sbd-page__search{width:100%}.sbd-page__error{display:flex;align-items:center;gap:10px;padding:16px 20px;background:#fef2f2;border:1px solid rgba(239,68,68,.2);border-radius:10px;color:#ef4444;font-size:13.5px;font-weight:500}.sbd-page__error i{font-size:18px}.sbd-page__content{display:grid;grid-template-columns:320px 1fr;gap:20px;align-items:start}@media (max-width: 960px){.sbd-page__content{grid-template-columns:1fr}}.sbd-page__aside{display:flex;flex-direction:column;gap:16px;position:sticky;top:20px}.sbd-page__main{display:flex;flex-direction:column;gap:16px}.sbd-page__quick-actions{display:flex;align-items:center;gap:12px;padding:12px 16px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;flex-wrap:wrap}.sbd-page__quick-label{font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8;flex-shrink:0}.sbd-page__context{background:#fff;border:1px solid #e2e8f0;border-radius:12px;overflow:hidden}.sbd-page__context-tabs{display:flex;align-items:center;border-bottom:1px solid #e2e8f0;background:#f8fafc}.sbd-page__context-add{margin-left:auto;margin-right:10px}.sbd-page__context-tab{display:flex;align-items:center;gap:7px;padding:11px 20px;font-size:13px;font-weight:600;color:#64748b;background:transparent;border:none;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:color .15s,border-color .15s}.sbd-page__context-tab .pi{font-size:13px}.sbd-page__context-tab:hover{color:#334155}.sbd-page__context-tab--active{color:#1e2e4a;border-bottom-color:#3b82f6;background:#fff}.sbd-page__context-pill{font-size:10.5px;font-weight:700;padding:1px 7px;border-radius:10px}.sbd-page__context-pill--notes{background:#e0e7ff;color:#4338ca}.sbd-page__context-pill--changes{background:#fef3c7;color:#b45309}.sbd-page__context-empty{display:flex;align-items:center;gap:8px;padding:24px 20px;font-size:13px;color:#94a3b8}.sbd-page__context-empty .pi{font-size:16px}.sbd-page__notes-list{display:flex;flex-direction:column}.sbd-page__note{padding:12px 16px;border-bottom:1px solid #f1f5f9;display:flex;flex-direction:column;gap:6px}.sbd-page__note:last-child{border-bottom:none}.sbd-page__note-top{display:flex;align-items:center;gap:8px}.sbd-page__note-badge{font-size:10px;font-weight:800;letter-spacing:.05em;padding:2px 8px;border-radius:10px;text-transform:uppercase;flex-shrink:0}.sbd-page__note-badge--general{background:#dbeafe;color:#1d4ed8}.sbd-page__note-badge--remark{background:#fef3c7;color:#b45309}.sbd-page__note-badge--feedback{background:#f3e8ff;color:#7e22ce}.sbd-page__note-author{font-size:12px;font-weight:700;color:#1e2e4a}.sbd-page__note-date{margin-left:auto;font-size:11px;color:#94a3b8;white-space:nowrap}.sbd-page__note-msg{margin:0;font-size:13px;color:#334155;line-height:1.55}.sbd-page__changes-groups{display:flex;flex-direction:column}.sbd-page__changes-group{border-bottom:1px solid #f1f5f9}.sbd-page__changes-group:last-child{border-bottom:none}.sbd-page__changes-meta{display:flex;align-items:center;gap:6px;padding:8px 16px;background:#fafafa;border-bottom:1px solid #f1f5f9;font-size:11.5px;color:#64748b}.sbd-page__changes-meta strong{font-weight:700;color:#334155}.sbd-page__changes-meta .pi{font-size:11px;color:#94a3b8}.sbd-page__changes-sep{color:#cbd5e1}.sbd-page__changes-table{width:100%;border-collapse:collapse;font-size:12.5px}.sbd-page__changes-table thead tr{background:#f8fafc;border-bottom:1px solid #e2e8f0}.sbd-page__changes-table thead th{padding:7px 14px;text-align:left;font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:#64748b;white-space:nowrap}.sbd-page__changes-table thead th .pi{margin-right:4px;font-size:10px}.sbd-page__changes-table thead th:nth-child(2){color:#dc2626}.sbd-page__changes-table thead th:nth-child(3){color:#16a34a}.sbd-page__changes-table tbody tr{border-bottom:1px solid #f1f5f9}.sbd-page__changes-table tbody tr:last-child{border-bottom:none}.sbd-page__changes-table tbody tr:hover{background:#f8fafc}.sbd-page__changes-table tbody td{padding:8px 14px;vertical-align:top;word-break:break-word}.sbd-page__changes-field{font-size:12px;font-weight:600;color:#475569;width:28%}.sbd-page__changes-old{color:#dc2626;font-weight:500;text-decoration:line-through;opacity:.8;width:36%}.sbd-page__changes-new{color:#16a34a;font-weight:700;width:36%}.sbd-page__content--loading{opacity:.6;pointer-events:none}.sbd-page__skeleton-card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:12px}.sbd-page__skeleton-row{height:13px;background:#f1f5f9;border-radius:6px;animation:sbd-shimmer 1.4s ease-in-out infinite}.sbd-page__skeleton-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:8px}.sbd-page__skeleton-chip{height:56px;background:#f1f5f9;border-radius:8px;animation:sbd-shimmer 1.4s ease-in-out infinite}@keyframes sbd-shimmer{0%,to{opacity:1}50%{opacity:.45}}@keyframes sbd-fadein{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}\n"] }]
|
|
3220
|
+
}], propDecorators: { onKeydown: [{
|
|
3221
|
+
type: HostListener,
|
|
3222
|
+
args: ['document:keydown', ['$event']]
|
|
3223
|
+
}] } });
|
|
3224
|
+
|
|
3225
|
+
var smartBookingDashboard_component = /*#__PURE__*/Object.freeze({
|
|
3226
|
+
__proto__: null,
|
|
3227
|
+
SmartBookingDashboardComponent: SmartBookingDashboardComponent
|
|
3228
|
+
});
|
|
3229
|
+
|
|
3230
|
+
const smartBookingDashboardRoutes = [
|
|
3231
|
+
{
|
|
3232
|
+
path: '',
|
|
3233
|
+
loadComponent: () => Promise.resolve().then(function () { return smartBookingDashboard_component; }).then(c => c.SmartBookingDashboardComponent),
|
|
3234
|
+
},
|
|
3235
|
+
];
|
|
3236
|
+
|
|
3237
|
+
class SmartBookingDashboardModule {
|
|
3238
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SmartBookingDashboardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
3239
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.14", ngImport: i0, type: SmartBookingDashboardModule, imports: [i1$3.RouterModule] }); }
|
|
3240
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SmartBookingDashboardModule, imports: [RouterModule.forChild(smartBookingDashboardRoutes)] }); }
|
|
3241
|
+
}
|
|
3242
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SmartBookingDashboardModule, decorators: [{
|
|
3243
|
+
type: NgModule,
|
|
3244
|
+
args: [{
|
|
3245
|
+
imports: [RouterModule.forChild(smartBookingDashboardRoutes)],
|
|
3246
|
+
}]
|
|
3247
|
+
}] });
|
|
3248
|
+
|
|
3249
|
+
/*
|
|
3250
|
+
* Public API of the Smart Booking Dashboard.
|
|
3251
|
+
* Host apps should import only from this file.
|
|
3252
|
+
*/
|
|
3253
|
+
|
|
3254
|
+
/**
|
|
3255
|
+
* Generated bundle index. Do not edit.
|
|
3256
|
+
*/
|
|
3257
|
+
|
|
3258
|
+
export { SBD_CONFIG, SBD_DEFAULT_CONFIG, SmartBookingDashboardComponent, SmartBookingDashboardModule, provideSmartBookingDashboard, smartBookingDashboardRoutes };
|
|
3259
|
+
//# sourceMappingURL=almatar-smart-booking-dashboard.mjs.map
|