🔄 cleanup: useless domain logic on admin
This commit is contained in:
@@ -1,54 +0,0 @@
|
|||||||
import { TicketRepository } from "$lib/domains/ticket/data/repository";
|
|
||||||
import { db } from "@pkg/db";
|
|
||||||
import { Logger } from "@pkg/logger";
|
|
||||||
import type { FlightTicket } from "./data/entities";
|
|
||||||
import { ScrapedTicketsDataSource } from "./data/scrape.data.source";
|
|
||||||
|
|
||||||
// if the discount is lower than 1 or more than 100, then we don't apply the discount
|
|
||||||
function applyDiscount(price: number, discountPercent: number) {
|
|
||||||
if (discountPercent < 1 || discountPercent > 100) {
|
|
||||||
return price;
|
|
||||||
}
|
|
||||||
const discount = price * (discountPercent / 100);
|
|
||||||
// Logger.info(`Discounted: ${price} -> ${discount}`);
|
|
||||||
return price - discount;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TicketController {
|
|
||||||
private repo: TicketRepository;
|
|
||||||
|
|
||||||
constructor(repo: TicketRepository) {
|
|
||||||
this.repo = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createTicket(payload: FlightTicket) {
|
|
||||||
const checkRes = await this.repo.getTicketIdByInfo({
|
|
||||||
ticketId: "",
|
|
||||||
departureDate: payload.departureDate,
|
|
||||||
returnDate: payload.returnDate,
|
|
||||||
cabinClass: payload.cabinClass,
|
|
||||||
departure: payload.departure,
|
|
||||||
arrival: payload.arrival,
|
|
||||||
});
|
|
||||||
if (checkRes.error) {
|
|
||||||
return { error: checkRes.error };
|
|
||||||
}
|
|
||||||
if (!checkRes.data) {
|
|
||||||
Logger.info("Ticket not already found, so making a new one");
|
|
||||||
return this.repo.createTicket(payload);
|
|
||||||
}
|
|
||||||
Logger.info(
|
|
||||||
"Ticket already exists with similar features, so just returning that",
|
|
||||||
);
|
|
||||||
return { data: checkRes.data };
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTicketById(id: number) {
|
|
||||||
return this.repo.getTicketById(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTC() {
|
|
||||||
const ds = new ScrapedTicketsDataSource("", "");
|
|
||||||
return new TicketController(new TicketRepository(db, ds));
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "@pkg/logic/domains/passengerinfo/data/entities";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "@pkg/logic/domains/ticket/data/entities/enums";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "@pkg/logic/domains/ticket/data/entities/index";
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import { and, eq, or, type Database } from "@pkg/db";
|
|
||||||
import {
|
|
||||||
flightTicketModel,
|
|
||||||
TicketType,
|
|
||||||
type FlightTicket,
|
|
||||||
type TicketSearchDTO,
|
|
||||||
} from "./entities";
|
|
||||||
import { type Result, ERROR_CODES } from "@pkg/result";
|
|
||||||
import { getError, Logger } from "@pkg/logger";
|
|
||||||
import type { ScrapedTicketsDataSource } from "./scrape.data.source";
|
|
||||||
import { flightTicketInfo } from "@pkg/db/schema";
|
|
||||||
|
|
||||||
export class TicketRepository {
|
|
||||||
private db: Database;
|
|
||||||
private scraper: ScrapedTicketsDataSource;
|
|
||||||
|
|
||||||
constructor(db: Database, scraper: ScrapedTicketsDataSource) {
|
|
||||||
this.db = db;
|
|
||||||
this.scraper = scraper;
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchForTickets(
|
|
||||||
payload: TicketSearchDTO,
|
|
||||||
): Promise<Result<FlightTicket[]>> {
|
|
||||||
try {
|
|
||||||
return this.scraper.searchForTickets(payload);
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.NETWORK_ERROR,
|
|
||||||
message: "Failed to search for tickets",
|
|
||||||
detail: "Could not fetch tickets for the given payload",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
error: err,
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTicketById(id: number): Promise<Result<FlightTicket>> {
|
|
||||||
const out = await this.db.query.flightTicketInfo.findFirst({
|
|
||||||
where: eq(flightTicketInfo.id, id),
|
|
||||||
});
|
|
||||||
if (!out) {
|
|
||||||
return {
|
|
||||||
error: getError({
|
|
||||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
|
||||||
message: "Ticket not found",
|
|
||||||
userHint:
|
|
||||||
"Please check if the selected ticket is correct, or try again",
|
|
||||||
detail: "The ticket is not found by the id provided",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const parsed = flightTicketModel.safeParse(out);
|
|
||||||
if (!parsed.success) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "Failed to parse ticket",
|
|
||||||
userHint: "Please try again",
|
|
||||||
detail: "Failed to parse ticket",
|
|
||||||
},
|
|
||||||
JSON.stringify(parsed.error.errors),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { data: parsed.data };
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTicketIdByInfo(info: {
|
|
||||||
ticketId: string;
|
|
||||||
arrival: string;
|
|
||||||
departure: string;
|
|
||||||
cabinClass: string;
|
|
||||||
departureDate: string;
|
|
||||||
returnDate: string;
|
|
||||||
}): Promise<Result<number>> {
|
|
||||||
try {
|
|
||||||
const out = await this.db.query.flightTicketInfo.findFirst({
|
|
||||||
where: or(
|
|
||||||
eq(flightTicketInfo.ticketId, info.ticketId),
|
|
||||||
and(
|
|
||||||
eq(flightTicketInfo.arrival, info.arrival),
|
|
||||||
eq(flightTicketInfo.departure, info.departure),
|
|
||||||
eq(flightTicketInfo.cabinClass, info.cabinClass),
|
|
||||||
eq(
|
|
||||||
flightTicketInfo.departureDate,
|
|
||||||
new Date(info.departureDate),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
columns: { id: true },
|
|
||||||
});
|
|
||||||
if (out && out.id) {
|
|
||||||
return { data: out?.id };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
} catch (e) {
|
|
||||||
Logger.debug(info);
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.DATABASE_ERROR,
|
|
||||||
message: "Failed to lookup ticket",
|
|
||||||
detail: "A database error occured while getting ticket by id",
|
|
||||||
userHint: "Contact us to resolve this issue",
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createTicket(payload: FlightTicket): Promise<Result<number>> {
|
|
||||||
try {
|
|
||||||
let rd = new Date();
|
|
||||||
if (
|
|
||||||
payload.returnDate &&
|
|
||||||
payload.returnDate.length > 0 &&
|
|
||||||
payload.flightType !== TicketType.OneWay
|
|
||||||
) {
|
|
||||||
rd = new Date(payload.returnDate);
|
|
||||||
}
|
|
||||||
const out = await this.db
|
|
||||||
.insert(flightTicketInfo)
|
|
||||||
.values({
|
|
||||||
ticketId: payload.ticketId,
|
|
||||||
flightType: payload.flightType,
|
|
||||||
|
|
||||||
arrival: payload.arrival,
|
|
||||||
departure: payload.departure,
|
|
||||||
cabinClass: payload.cabinClass,
|
|
||||||
departureDate: new Date(payload.departureDate),
|
|
||||||
returnDate: rd,
|
|
||||||
|
|
||||||
priceDetails: payload.priceDetails,
|
|
||||||
passengerCounts: payload.passengerCounts,
|
|
||||||
bagsInfo: payload.bagsInfo,
|
|
||||||
|
|
||||||
lastAvailable: payload.lastAvailable,
|
|
||||||
|
|
||||||
flightIteneraries: payload.flightIteneraries,
|
|
||||||
checkoutUrl: payload.checkoutUrl ?? "",
|
|
||||||
|
|
||||||
dates: payload.dates,
|
|
||||||
|
|
||||||
refundable: payload.refundable ?? false,
|
|
||||||
isCache: payload.isCache ?? false,
|
|
||||||
|
|
||||||
shareId: payload.shareId,
|
|
||||||
})
|
|
||||||
.returning({ id: flightTicketInfo.id })
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (!out || out.length === 0) {
|
|
||||||
Logger.error("Failed to create ticket");
|
|
||||||
Logger.debug(out);
|
|
||||||
Logger.debug(payload);
|
|
||||||
return {
|
|
||||||
error: getError({
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "Failed to create ticket",
|
|
||||||
userHint: "Please try again",
|
|
||||||
detail: "Failed to create ticket",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { data: out[0].id };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
||||||
message: "An error occured while creating ticket",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
actionable: false,
|
|
||||||
detail: "An error occured while creating ticket",
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
|
||||||
import type { FlightTicket, TicketSearchDTO } from "./entities";
|
|
||||||
import { betterFetch } from "@better-fetch/fetch";
|
|
||||||
import { getError, Logger } from "@pkg/logger";
|
|
||||||
|
|
||||||
export class ScrapedTicketsDataSource {
|
|
||||||
scraperUrl: string;
|
|
||||||
apiKey: string;
|
|
||||||
|
|
||||||
constructor(scraperUrl: string, apiKey: string) {
|
|
||||||
this.scraperUrl = scraperUrl;
|
|
||||||
this.apiKey = apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchForTickets(
|
|
||||||
payload: TicketSearchDTO,
|
|
||||||
): Promise<Result<FlightTicket[]>> {
|
|
||||||
const { data, error } = await betterFetch<Result<FlightTicket[]>>(
|
|
||||||
`${this.scraperUrl}/api/v1/tickets/search`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${this.apiKey}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return {
|
|
||||||
error: getError(
|
|
||||||
{
|
|
||||||
code: ERROR_CODES.NETWORK_ERROR,
|
|
||||||
message: "Failed to search for tickets",
|
|
||||||
detail: "Could not fetch tickets for the given payload",
|
|
||||||
userHint: "Please try again later",
|
|
||||||
error: error,
|
|
||||||
actionable: false,
|
|
||||||
},
|
|
||||||
JSON.stringify(error, null, 4),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(`Returning ${data.data?.length} tickets`);
|
|
||||||
|
|
||||||
return { data: data.data ?? [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { writable } from "svelte/store";
|
|
||||||
import {
|
|
||||||
CabinClass,
|
|
||||||
TicketType,
|
|
||||||
type FlightTicket,
|
|
||||||
type TicketSearchPayload,
|
|
||||||
} from "./entities";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
|
|
||||||
export const flightTicketStore = writable<FlightTicket>();
|
|
||||||
|
|
||||||
export const ticketSearchStore = writable<TicketSearchPayload>({
|
|
||||||
sessionId: nanoid(),
|
|
||||||
ticketType: TicketType.Return,
|
|
||||||
cabinClass: CabinClass.Economy,
|
|
||||||
passengerCounts: { adults: 1, children: 0 },
|
|
||||||
departure: "",
|
|
||||||
arrival: "",
|
|
||||||
departureDate: "",
|
|
||||||
returnDate: "",
|
|
||||||
loadMore: false,
|
|
||||||
});
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
import {
|
|
||||||
type FlightTicket,
|
|
||||||
ticketSearchPayloadModel,
|
|
||||||
} from "$lib/domains/ticket/data/entities/index";
|
|
||||||
import { trpcApiStore } from "$lib/stores/api";
|
|
||||||
import { toast } from "svelte-sonner";
|
|
||||||
import { get } from "svelte/store";
|
|
||||||
import {
|
|
||||||
MaxStops,
|
|
||||||
SortOption,
|
|
||||||
ticketFiltersStore,
|
|
||||||
} from "./ticket-filters.vm.svelte";
|
|
||||||
import { ticketSearchStore } from "../data/store";
|
|
||||||
|
|
||||||
export class FlightTicketViewModel {
|
|
||||||
searching = $state(false);
|
|
||||||
tickets = $state<FlightTicket[]>([]);
|
|
||||||
renderedTickets = $state<FlightTicket[]>([]);
|
|
||||||
|
|
||||||
async searchForTickets(loadMore = false) {
|
|
||||||
const api = get(trpcApiStore);
|
|
||||||
if (!api) {
|
|
||||||
return toast.error("Please try again by reloading the page", {
|
|
||||||
description: "Page state not properly initialized",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let payload = get(ticketSearchStore);
|
|
||||||
if (!payload) {
|
|
||||||
return toast.error(
|
|
||||||
"Could not search for tickets due to invalid payload",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = ticketSearchPayloadModel.safeParse(payload);
|
|
||||||
if (!parsed.success) {
|
|
||||||
console.log("Enter some parameters to search for tickets");
|
|
||||||
this.searching = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
payload = parsed.data;
|
|
||||||
|
|
||||||
if (loadMore) {
|
|
||||||
payload.loadMore = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searching = true;
|
|
||||||
const out = await api.ticket.searchTickets.query(payload);
|
|
||||||
this.searching = false;
|
|
||||||
|
|
||||||
console.log(out);
|
|
||||||
|
|
||||||
if (out.error) {
|
|
||||||
return toast.error(out.error.message, {
|
|
||||||
description: out.error.userHint,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!out.data) {
|
|
||||||
this.tickets = [];
|
|
||||||
return toast.error("No search results", {
|
|
||||||
description: "Please try again with different parameters",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tickets = out.data;
|
|
||||||
this.applyFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilters() {
|
|
||||||
this.searching = true;
|
|
||||||
const filters = get(ticketFiltersStore);
|
|
||||||
const filteredTickets = this.tickets.filter((ticket) => {
|
|
||||||
// Price filter
|
|
||||||
if (filters.priceRange.max > 0) {
|
|
||||||
if (
|
|
||||||
ticket.priceDetails.displayPrice < filters.priceRange.min ||
|
|
||||||
ticket.priceDetails.displayPrice > filters.priceRange.max
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.maxStops !== MaxStops.Any) {
|
|
||||||
// Calculate stops for outbound flight
|
|
||||||
const outboundStops =
|
|
||||||
ticket.flightIteneraries.outbound.length - 1;
|
|
||||||
// Calculate stops for inbound flight
|
|
||||||
const inboundStops = ticket.flightIteneraries.inbound.length - 1;
|
|
||||||
|
|
||||||
// Get the maximum number of stops between outbound and inbound
|
|
||||||
const maxStopsInJourney = Math.max(outboundStops, inboundStops);
|
|
||||||
|
|
||||||
switch (filters.maxStops) {
|
|
||||||
case MaxStops.Direct:
|
|
||||||
if (maxStopsInJourney > 0) return false;
|
|
||||||
break;
|
|
||||||
case MaxStops.One:
|
|
||||||
if (maxStopsInJourney > 1) return false;
|
|
||||||
break;
|
|
||||||
case MaxStops.Two:
|
|
||||||
if (maxStopsInJourney > 2) return false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time range filters
|
|
||||||
if (filters.time.departure.max > 0 || filters.time.arrival.max > 0) {
|
|
||||||
const allItineraries = [
|
|
||||||
...ticket.flightIteneraries.outbound,
|
|
||||||
...ticket.flightIteneraries.inbound,
|
|
||||||
];
|
|
||||||
for (const itinerary of allItineraries) {
|
|
||||||
const departureHour = new Date(
|
|
||||||
itinerary.departure.utcTime,
|
|
||||||
).getHours();
|
|
||||||
const arrivalHour = new Date(
|
|
||||||
itinerary.destination.utcTime,
|
|
||||||
).getHours();
|
|
||||||
|
|
||||||
if (filters.time.departure.max > 0) {
|
|
||||||
if (
|
|
||||||
departureHour < filters.time.departure.min ||
|
|
||||||
departureHour > filters.time.departure.max
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.time.arrival.max > 0) {
|
|
||||||
if (
|
|
||||||
arrivalHour < filters.time.arrival.min ||
|
|
||||||
arrivalHour > filters.time.arrival.max
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duration filter
|
|
||||||
if (filters.duration.max > 0) {
|
|
||||||
const allItineraries = [
|
|
||||||
...ticket.flightIteneraries.outbound,
|
|
||||||
...ticket.flightIteneraries.inbound,
|
|
||||||
];
|
|
||||||
const totalDuration = allItineraries.reduce(
|
|
||||||
(sum, itinerary) => sum + itinerary.durationSeconds,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const durationHours = totalDuration / 3600;
|
|
||||||
|
|
||||||
if (
|
|
||||||
durationHours < filters.duration.min ||
|
|
||||||
durationHours > filters.duration.max
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overnight filter
|
|
||||||
if (!filters.allowOvernight) {
|
|
||||||
const allItineraries = [
|
|
||||||
...ticket.flightIteneraries.outbound,
|
|
||||||
...ticket.flightIteneraries.inbound,
|
|
||||||
];
|
|
||||||
const hasOvernightFlight = allItineraries.some((itinerary) => {
|
|
||||||
const departureHour = new Date(
|
|
||||||
itinerary.departure.utcTime,
|
|
||||||
).getHours();
|
|
||||||
const arrivalHour = new Date(
|
|
||||||
itinerary.destination.utcTime,
|
|
||||||
).getHours();
|
|
||||||
|
|
||||||
// Consider a flight overnight if it departs between 8 PM (20) and 6 AM (6)
|
|
||||||
return (
|
|
||||||
departureHour >= 20 ||
|
|
||||||
departureHour <= 6 ||
|
|
||||||
arrivalHour >= 20 ||
|
|
||||||
arrivalHour <= 6
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (hasOvernightFlight) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
filteredTickets.sort((a, b) => {
|
|
||||||
switch (filters.sortBy) {
|
|
||||||
case SortOption.PriceLowToHigh:
|
|
||||||
return (
|
|
||||||
a.priceDetails.displayPrice - b.priceDetails.displayPrice
|
|
||||||
);
|
|
||||||
case SortOption.PriceHighToLow:
|
|
||||||
return (
|
|
||||||
b.priceDetails.displayPrice - a.priceDetails.displayPrice
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.renderedTickets = filteredTickets;
|
|
||||||
this.searching = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flightTicketVM = new FlightTicketViewModel();
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import BackpackIcon from "~icons/solar/backpack-broken";
|
|
||||||
import BagCheckIcon from "~icons/solar/suitcase-broken";
|
|
||||||
import PersonalBagIcon from "~icons/solar/bag-broken";
|
|
||||||
import Icon from "$lib/components/atoms/icon.svelte";
|
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
import type { FlightTicket } from "../../data/entities";
|
|
||||||
|
|
||||||
let { data }: { data: FlightTicket } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if data}
|
|
||||||
<Title size="h5" color="black">Baggage Info</Title>
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<!-- Personal Item - Always show -->
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Icon icon={PersonalBagIcon} cls="w-6 h-6" />
|
|
||||||
<span>Personal Item</span>
|
|
||||||
</div>
|
|
||||||
<span>{data.bagsInfo.includedPersonalBags} included</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cabin Baggage - Always show -->
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Icon icon={BackpackIcon} cls="w-6 h-6" />
|
|
||||||
<span>Cabin Baggage</span>
|
|
||||||
</div>
|
|
||||||
{#if data.bagsInfo.hasHandBagsSupport}
|
|
||||||
<span>{data.bagsInfo.includedHandBags} included</span>
|
|
||||||
{:else}
|
|
||||||
<span class="text-gray-500">Not available</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Checked Baggage - Always show -->
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Icon icon={BagCheckIcon} cls="w-6 h-6" />
|
|
||||||
<span>Checked Baggage</span>
|
|
||||||
</div>
|
|
||||||
{#if data.bagsInfo.hasCheckedBagsSupport}
|
|
||||||
<span>{data.bagsInfo.includedCheckedBags} included</span>
|
|
||||||
{:else}
|
|
||||||
<span class="text-gray-500">Not available</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
|
||||||
import type { FlightTicket } from "../../data/entities";
|
|
||||||
import TicketDetails from "./ticket-details.svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
data,
|
|
||||||
onCheckoutBtnClick,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
data: FlightTicket;
|
|
||||||
children: any;
|
|
||||||
onCheckoutBtnClick: (tid: number) => void;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Dialog.Root>
|
|
||||||
{@render children()}
|
|
||||||
|
|
||||||
<Dialog.Content class="w-full max-w-2xl">
|
|
||||||
<div class="flex max-h-[80vh] w-full flex-col gap-4 overflow-y-auto">
|
|
||||||
<TicketDetails {data} {onCheckoutBtnClick} />
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
import Button from "$lib/components/ui/button/button.svelte";
|
|
||||||
import { type FlightTicket } from "../../data/entities";
|
|
||||||
import TripDetails from "./trip-details.svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
data,
|
|
||||||
onCheckoutBtnClick,
|
|
||||||
}: { data: FlightTicket; onCheckoutBtnClick: (tid: number) => void } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TripDetails {data} />
|
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-end gap-4">
|
|
||||||
<Title center size="h5" color="black">
|
|
||||||
${data.priceDetails.displayPrice}
|
|
||||||
</Title>
|
|
||||||
<Button variant="default" onclick={() => onCheckoutBtnClick(data.id)}>
|
|
||||||
Continue to Checkout
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Title from "$lib/components/atoms/title.svelte";
|
|
||||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
|
||||||
import { Plane } from "@lucide/svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
departure: string;
|
|
||||||
destination: string;
|
|
||||||
durationSeconds: number;
|
|
||||||
departureTime: string;
|
|
||||||
arrivalTime: string;
|
|
||||||
departureDate: string;
|
|
||||||
arrivalDate: string;
|
|
||||||
airlineName?: string;
|
|
||||||
stops?: number;
|
|
||||||
}
|
|
||||||
let {
|
|
||||||
departure,
|
|
||||||
destination,
|
|
||||||
durationSeconds,
|
|
||||||
airlineName,
|
|
||||||
stops,
|
|
||||||
departureDate,
|
|
||||||
departureTime,
|
|
||||||
arrivalDate,
|
|
||||||
arrivalTime,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
const dH = Math.floor(durationSeconds / 3600);
|
|
||||||
const dM = Math.floor((durationSeconds % 3600) / 60);
|
|
||||||
const durationFormatted = `${dH}h ${dM}m`;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex w-full items-center justify-between gap-8 p-2">
|
|
||||||
<!-- Departure Info -->
|
|
||||||
<div class="flex flex-col items-start">
|
|
||||||
<Title color="black" size="h5">{departure}</Title>
|
|
||||||
<Title color="black" size="p" weight="normal">
|
|
||||||
{departureTime}
|
|
||||||
</Title>
|
|
||||||
<span class="text-start text-sm text-gray-500">
|
|
||||||
{departureDate}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Flight Duration & Details -->
|
|
||||||
<div class="flex flex-col items-center gap-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="h-2 w-2 rounded-full bg-gray-400"></div>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="w-40 border-t-2 border-gray-300"></div>
|
|
||||||
<Plane
|
|
||||||
class="absolute -top-[10px] left-1/2 -translate-x-1/2 transform text-primary"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="h-2 w-2 rounded-full bg-gray-400"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="text-sm font-medium text-gray-600">
|
|
||||||
{durationFormatted}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="flex flex-col items-center gap-1">
|
|
||||||
{#if stops !== undefined}
|
|
||||||
<Badge
|
|
||||||
variant={stops > 0 ? "secondary" : "outline"}
|
|
||||||
class="font-medium"
|
|
||||||
>
|
|
||||||
{#if stops > 0}
|
|
||||||
{stops} {stops === 1 ? "Stop" : "Stops"}
|
|
||||||
{:else}
|
|
||||||
Direct Flight
|
|
||||||
{/if}
|
|
||||||
</Badge>
|
|
||||||
{/if}
|
|
||||||
{#if airlineName}
|
|
||||||
<span class="text-xs text-gray-500">{airlineName}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Arrival Info -->
|
|
||||||
<div class="flex flex-col items-end">
|
|
||||||
<Title color="black" size="h5">{destination}</Title>
|
|
||||||
<Title color="black" size="p" weight="normal">
|
|
||||||
{arrivalTime}
|
|
||||||
</Title>
|
|
||||||
<span class="text-sm text-gray-500">{arrivalDate}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { TicketType, type FlightTicket } from "../../data/entities";
|
|
||||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
|
||||||
import TicketItinerary from "./ticket-itinerary.svelte";
|
|
||||||
import { formatDateTime } from "@pkg/logic/core/date.utils";
|
|
||||||
|
|
||||||
let { data }: { data: FlightTicket } = $props();
|
|
||||||
|
|
||||||
const outboundDuration = data.flightIteneraries.outbound.reduce(
|
|
||||||
(acc, curr) => acc + curr.durationSeconds,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
const inboundDuration = data.flightIteneraries.inbound.reduce(
|
|
||||||
(acc, curr) => acc + curr.durationSeconds,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isReturnTicket =
|
|
||||||
data.flightIteneraries.inbound.length > 0 &&
|
|
||||||
data.flightType === TicketType.Return;
|
|
||||||
|
|
||||||
const outboundFirst = data.flightIteneraries.outbound[0];
|
|
||||||
const outboundLast =
|
|
||||||
data.flightIteneraries.outbound[
|
|
||||||
data.flightIteneraries.outbound.length - 1
|
|
||||||
];
|
|
||||||
|
|
||||||
const inboundFirst = isReturnTicket
|
|
||||||
? data.flightIteneraries.inbound[0]
|
|
||||||
: null;
|
|
||||||
const inboundLast = isReturnTicket
|
|
||||||
? data.flightIteneraries.inbound[
|
|
||||||
data.flightIteneraries.inbound.length - 1
|
|
||||||
]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const outboundDeparture = formatDateTime(outboundFirst.departure.localTime);
|
|
||||||
const outboundArrival = formatDateTime(outboundLast.destination.localTime);
|
|
||||||
|
|
||||||
const inboundDeparture = inboundFirst
|
|
||||||
? formatDateTime(inboundFirst.departure.localTime)
|
|
||||||
: null;
|
|
||||||
const inboundArrival = inboundLast
|
|
||||||
? formatDateTime(inboundLast.destination.localTime)
|
|
||||||
: null;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if isReturnTicket}
|
|
||||||
<Badge variant="outline" class="w-max">
|
|
||||||
<span>Outbound</span>
|
|
||||||
</Badge>
|
|
||||||
{/if}
|
|
||||||
<TicketItinerary
|
|
||||||
departure={data.departure}
|
|
||||||
destination={data.arrival}
|
|
||||||
durationSeconds={outboundDuration}
|
|
||||||
stops={data.flightIteneraries.outbound.length - 1}
|
|
||||||
departureTime={outboundDeparture.time}
|
|
||||||
departureDate={outboundDeparture.date}
|
|
||||||
arrivalTime={outboundArrival.time}
|
|
||||||
arrivalDate={outboundArrival.date}
|
|
||||||
airlineName={outboundFirst.airline.name}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if isReturnTicket}
|
|
||||||
<div class="w-full border-t-2 border-dashed border-gray-400"></div>
|
|
||||||
|
|
||||||
{#if isReturnTicket}
|
|
||||||
<Badge variant="outline" class="w-max">
|
|
||||||
<span>Inbound</span>
|
|
||||||
</Badge>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<TicketItinerary
|
|
||||||
departure={data.arrival}
|
|
||||||
destination={data.departure}
|
|
||||||
durationSeconds={inboundDuration}
|
|
||||||
stops={data.flightIteneraries.inbound.length - 1}
|
|
||||||
departureTime={inboundDeparture?.time || ""}
|
|
||||||
departureDate={inboundDeparture?.date || ""}
|
|
||||||
arrivalTime={inboundArrival?.time || ""}
|
|
||||||
arrivalDate={inboundArrival?.date || ""}
|
|
||||||
airlineName={inboundFirst?.airline.name}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="w-full border-t-2 border-dashed border-gray-400"></div>
|
|
||||||
{/if}
|
|
||||||
Reference in New Issue
Block a user