🔄 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