784 lines
22 KiB
Vue
784 lines
22 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from "vue";
|
|
import { useI18n } from "vue-i18n";
|
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|
import { popups } from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
|
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
|
import {
|
|
getOrderBookingItemPreviewRows,
|
|
getOrderBookingItemPreviewTotal,
|
|
getOrderBookingNotesValue,
|
|
getOrderBookingPlateText,
|
|
getOrderBookingPoValue,
|
|
getOrderBookingReferenceValue,
|
|
getOrderBookingServiceLabels,
|
|
hasOrderBookingDisplayDetails,
|
|
} from "@/components/displays/department/pos/utils/orderBookingDisplay.js";
|
|
|
|
type BookingRefreshResult = any[] | { bookings?: any[] } | void;
|
|
|
|
type OrderBookingPopupProps = {
|
|
bookings?: any[];
|
|
onSelect?: (booking: any) => Promise<void> | void;
|
|
onSkip?: () => Promise<void> | void;
|
|
onRefreshBookings?: () => Promise<BookingRefreshResult> | BookingRefreshResult;
|
|
departmentId?: number | string | null;
|
|
matchedPlate?: string;
|
|
};
|
|
|
|
const { t, locale } = useI18n();
|
|
|
|
const popupProps = computed<OrderBookingPopupProps>(() => {
|
|
return (popups.get()?.props || {}) as OrderBookingPopupProps;
|
|
});
|
|
|
|
const bookingsFromProps = computed(() => (Array.isArray(popupProps.value.bookings) ? popupProps.value.bookings : []));
|
|
const bookings = ref<any[]>([]);
|
|
|
|
const detailedBookingsById = ref<Record<number, any>>({});
|
|
const loadingBookingIds = ref<number[]>([]);
|
|
const failedBookingIds = ref<number[]>([]);
|
|
const isRefreshingBookings = ref(false);
|
|
|
|
const toPositiveInteger = (value: any) => {
|
|
const parsedValue = Number.parseInt(String(value ?? ""), 10);
|
|
return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null;
|
|
};
|
|
|
|
const normalizePlateValue = (value: any) =>
|
|
String(value ?? "")
|
|
.trim()
|
|
.toUpperCase();
|
|
|
|
const formatBookingDateTime = (booking: any) => {
|
|
const rawValue = booking?.datetime || booking?.created_at || booking?.date || null;
|
|
if (!rawValue) {
|
|
return t("admin.pos.not_found");
|
|
}
|
|
|
|
const parsedValue = new Date(rawValue);
|
|
if (Number.isNaN(parsedValue.getTime())) {
|
|
return rawValue;
|
|
}
|
|
|
|
return new Intl.DateTimeFormat(locale.value || undefined, {
|
|
dateStyle: "short",
|
|
timeStyle: "short",
|
|
}).format(parsedValue);
|
|
};
|
|
|
|
const getBookingDateValue = (booking: any) => {
|
|
const rawValue = booking?.datetime || booking?.created_at || booking?.date || null;
|
|
if (!rawValue) {
|
|
return null;
|
|
}
|
|
|
|
const parsedValue = new Date(rawValue);
|
|
return Number.isNaN(parsedValue.getTime()) ? null : parsedValue;
|
|
};
|
|
|
|
const isSameCalendarDay = (left: Date | null, right: Date | null) => {
|
|
if (!(left instanceof Date) || !(right instanceof Date)) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
left.getFullYear() === right.getFullYear() &&
|
|
left.getMonth() === right.getMonth() &&
|
|
left.getDate() === right.getDate()
|
|
);
|
|
};
|
|
|
|
const getDetailedBooking = (booking: any) => {
|
|
const bookingId = toPositiveInteger(booking?.id);
|
|
return bookingId ? detailedBookingsById.value[bookingId] || null : null;
|
|
};
|
|
|
|
const getDisplayBooking = (booking: any) => {
|
|
const detailedBooking = getDetailedBooking(booking);
|
|
return detailedBooking ? { ...booking, ...detailedBooking } : booking;
|
|
};
|
|
|
|
const isBookingLoading = (booking: any) => {
|
|
const bookingId = toPositiveInteger(booking?.id);
|
|
return bookingId ? loadingBookingIds.value.includes(bookingId) : false;
|
|
};
|
|
|
|
const didBookingDetailFail = (booking: any) => {
|
|
const bookingId = toPositiveInteger(booking?.id);
|
|
return bookingId ? failedBookingIds.value.includes(bookingId) : false;
|
|
};
|
|
|
|
const getBookingServiceLabels = (booking: any) => getOrderBookingServiceLabels(getDisplayBooking(booking));
|
|
|
|
const getBookingItemPreviewRows = (booking: any) => getOrderBookingItemPreviewRows(getDisplayBooking(booking));
|
|
|
|
const getBookingItemPreviewTotal = (booking: any) => getOrderBookingItemPreviewTotal(getDisplayBooking(booking));
|
|
|
|
const getBookingNote = (booking: any) => getOrderBookingNotesValue(getDisplayBooking(booking));
|
|
|
|
const getBookingReference = (booking: any) => getOrderBookingReferenceValue(getDisplayBooking(booking));
|
|
|
|
const getBookingPo = (booking: any) => getOrderBookingPoValue(getDisplayBooking(booking));
|
|
|
|
const getBookingPlates = (booking: any) => getOrderBookingPlateText(getDisplayBooking(booking));
|
|
|
|
const formatCurrencyAmount = (value: any) => {
|
|
const parsedValue = Number(value);
|
|
if (!Number.isFinite(parsedValue)) {
|
|
return "-";
|
|
}
|
|
|
|
return SessionUser.functions.currency.toLocal(parsedValue);
|
|
};
|
|
|
|
const totalAmountLabel = computed(() => SessionUser.objects.orders.columns.total_net_amount?.label || "Total");
|
|
|
|
const resolvedBookingCount = computed(() => {
|
|
return bookings.value.filter((booking) => {
|
|
const displayBooking = getDisplayBooking(booking);
|
|
return hasOrderBookingDisplayDetails(displayBooking) || !isBookingLoading(booking) || didBookingDetailFail(booking);
|
|
}).length;
|
|
});
|
|
|
|
const isLoadingAnyBookingDetails = computed(() => loadingBookingIds.value.length > 0);
|
|
const isAnyBookingFetchInProgress = computed(() => isLoadingAnyBookingDetails.value || isRefreshingBookings.value);
|
|
|
|
const sortedBookings = computed(() => {
|
|
const now = new Date();
|
|
return bookings.value
|
|
.map((booking, index) => {
|
|
const displayBooking = getDisplayBooking(booking);
|
|
const bookingDate = getBookingDateValue(displayBooking);
|
|
const isToday = isSameCalendarDay(bookingDate, now);
|
|
|
|
return {
|
|
booking,
|
|
index,
|
|
bookingDate,
|
|
isToday,
|
|
};
|
|
})
|
|
.sort((left, right) => {
|
|
if (left.isToday !== right.isToday) {
|
|
return left.isToday ? -1 : 1;
|
|
}
|
|
|
|
if (left.bookingDate && right.bookingDate) {
|
|
const timeDifference = left.bookingDate.getTime() - right.bookingDate.getTime();
|
|
if (timeDifference !== 0) {
|
|
return timeDifference;
|
|
}
|
|
} else if (left.bookingDate || right.bookingDate) {
|
|
return left.bookingDate ? -1 : 1;
|
|
}
|
|
|
|
return left.index - right.index;
|
|
})
|
|
.map((entry) => entry.booking);
|
|
});
|
|
|
|
const fetchBookingDetails = async (bookingsToInspect: any[]) => {
|
|
const pendingBookings = (Array.isArray(bookingsToInspect) ? bookingsToInspect : []).filter(Boolean);
|
|
const bookingsToLoad = pendingBookings.filter((booking) => {
|
|
const bookingId = toPositiveInteger(booking?.id);
|
|
if (!bookingId) {
|
|
return false;
|
|
}
|
|
|
|
if (detailedBookingsById.value[bookingId]) {
|
|
return false;
|
|
}
|
|
|
|
if (loadingBookingIds.value.includes(bookingId)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
if (bookingsToLoad.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const bookingIdsToLoad = bookingsToLoad
|
|
.map((booking) => toPositiveInteger(booking?.id))
|
|
.filter((bookingId): bookingId is number => bookingId !== null);
|
|
|
|
loadingBookingIds.value = [...new Set([...loadingBookingIds.value, ...bookingIdsToLoad])];
|
|
|
|
const bookingResults = await Promise.allSettled(
|
|
bookingsToLoad.map((booking) =>
|
|
SessionUser.objects.order_bookings.get.single(toPositiveInteger(booking?.id), {
|
|
department_id: popupProps.value.departmentId ?? null,
|
|
})
|
|
)
|
|
);
|
|
|
|
const nextDetailedBookings = {
|
|
...detailedBookingsById.value,
|
|
};
|
|
const nextFailedBookingIds = new Set(failedBookingIds.value);
|
|
|
|
bookingsToLoad.forEach((booking, index) => {
|
|
const bookingId = bookingIdsToLoad[index];
|
|
const result = bookingResults[index];
|
|
|
|
if (result.status === "fulfilled" && result.value?.id) {
|
|
nextDetailedBookings[result.value.id] = result.value;
|
|
nextFailedBookingIds.delete(result.value.id);
|
|
return;
|
|
}
|
|
|
|
if (bookingId) {
|
|
nextFailedBookingIds.add(bookingId);
|
|
}
|
|
});
|
|
|
|
detailedBookingsById.value = nextDetailedBookings;
|
|
failedBookingIds.value = Array.from(nextFailedBookingIds);
|
|
loadingBookingIds.value = loadingBookingIds.value.filter((bookingId) => !bookingIdsToLoad.includes(bookingId));
|
|
};
|
|
|
|
watch(
|
|
bookingsFromProps,
|
|
(nextBookings) => {
|
|
bookings.value = Array.isArray(nextBookings) ? [...nextBookings] : [];
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
watch(
|
|
() => bookings.value.map((booking) => booking?.id).join("|"),
|
|
() => {
|
|
void fetchBookingDetails(bookings.value);
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
const onSelect = async (booking: any) => {
|
|
if (typeof popupProps.value.onSelect === "function") {
|
|
await popupProps.value.onSelect(getDisplayBooking(booking));
|
|
}
|
|
};
|
|
|
|
const emitRefreshBookings = async () => {
|
|
if (typeof popupProps.value.onRefreshBookings !== "function") {
|
|
return;
|
|
}
|
|
|
|
isRefreshingBookings.value = true;
|
|
try {
|
|
const refreshResult = await popupProps.value.onRefreshBookings();
|
|
if (Array.isArray(refreshResult)) {
|
|
bookings.value = refreshResult;
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(refreshResult?.bookings)) {
|
|
bookings.value = refreshResult.bookings;
|
|
}
|
|
} finally {
|
|
isRefreshingBookings.value = false;
|
|
}
|
|
};
|
|
|
|
const getActionBooking = (booking: any) => getDisplayBooking(booking);
|
|
|
|
const getActionCustomerNumber = (booking: any) => {
|
|
const displayBooking = getActionBooking(booking);
|
|
return toPositiveInteger(displayBooking?.customer_number ?? displayBooking?.customer_id);
|
|
};
|
|
|
|
const getActionDepartmentId = (booking: any) => {
|
|
const displayBooking = getActionBooking(booking);
|
|
return toPositiveInteger(displayBooking?.department ?? popupProps.value.departmentId);
|
|
};
|
|
|
|
const isBookingScheduledForToday = (booking: any) => {
|
|
return isSameCalendarDay(getBookingDateValue(getDisplayBooking(booking)), new Date());
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="booking-popup" data-testid="pos-mobile-order-booking-popup">
|
|
<p class="booking-popup__intro">
|
|
{{ popups.get()?.message || t("admin.pos.order_booking_selector.help_text") }}
|
|
</p>
|
|
|
|
<div
|
|
v-if="isAnyBookingFetchInProgress"
|
|
class="booking-popup__loading"
|
|
data-testid="pos-mobile-order-booking-loading"
|
|
>
|
|
<span class="icon is-small">
|
|
<i class="fas fa-spinner fa-spin" aria-hidden="true" />
|
|
</span>
|
|
<span>
|
|
{{ SessionUser.objects.global.language.loading }}
|
|
{{ resolvedBookingCount }} / {{ bookings.length }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="booking-popup__list" data-testid="pos-mobile-order-booking-list">
|
|
<article
|
|
v-for="booking in sortedBookings"
|
|
:key="booking.id"
|
|
class="booking-option"
|
|
:class="{ 'booking-option--today': isBookingScheduledForToday(booking) }"
|
|
:data-testid="`pos-mobile-order-booking-option-${booking.id}`"
|
|
>
|
|
<div class="booking-option__header">
|
|
<div class="booking-option__header-main">
|
|
<p class="booking-option__eyebrow">Booking #{{ booking.id }}</p>
|
|
<h3 class="booking-option__title">{{ formatBookingDateTime(getDisplayBooking(booking)) }}</h3>
|
|
</div>
|
|
<div class="booking-option__header-actions">
|
|
<span
|
|
v-if="isBookingScheduledForToday(booking)"
|
|
class="tag booking-option__today-tag"
|
|
:data-testid="`pos-mobile-order-booking-today-${booking.id}`"
|
|
>
|
|
{{ t("common.today") }}
|
|
</span>
|
|
<span v-if="didBookingDetailFail(booking)" class="tag is-light booking-option__tag">
|
|
{{ t("admin.pos.not_found") }}
|
|
</span>
|
|
<div class="booking-option__settings" :data-testid="`pos-mobile-order-booking-settings-${booking.id}`">
|
|
<ActionSettingsWheelButton
|
|
:order_booking_id="toPositiveInteger(getActionBooking(booking)?.id)"
|
|
:order_id="toPositiveInteger(getActionBooking(booking)?.order_id)"
|
|
:customer_number="getActionCustomerNumber(booking)"
|
|
:department_id="getActionDepartmentId(booking)"
|
|
:reg_1="getActionBooking(booking)?.reg_1 ?? null"
|
|
:reg_2="getActionBooking(booking)?.reg_2 ?? null"
|
|
:refreshFunction="emitRefreshBookings"
|
|
:icon="'fas fa-ellipsis-v'"
|
|
trigger-button-variant="text"
|
|
allow-booking-completion
|
|
>
|
|
<template #actions />
|
|
</ActionSettingsWheelButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="booking-option__customer">
|
|
{{ getDisplayBooking(booking)?.customer_name || t("admin.pos.not_found") }}
|
|
</p>
|
|
|
|
<dl class="booking-option__meta-grid">
|
|
<div class="booking-option__meta-item">
|
|
<dt>{{ t("admin.pos.order_booking_selector.plates_label") }}</dt>
|
|
<dd>{{ getBookingPlates(booking) || t("admin.pos.not_found") }}</dd>
|
|
</div>
|
|
<div class="booking-option__meta-item">
|
|
<dt>{{ t("common.reference") }}</dt>
|
|
<dd>{{ getBookingReference(booking) || "-" }}</dd>
|
|
</div>
|
|
<div v-if="getBookingPo(booking)" class="booking-option__meta-item">
|
|
<dt>{{ SessionUser.objects.orders.columns.po.label }}</dt>
|
|
<dd>{{ getBookingPo(booking) }}</dd>
|
|
</div>
|
|
<div v-if="getBookingNote(booking)" class="booking-option__meta-item">
|
|
<dt>{{ SessionUser.objects.orders.columns.notes.label }}</dt>
|
|
<dd>{{ getBookingNote(booking) }}</dd>
|
|
</div>
|
|
</dl>
|
|
|
|
<div
|
|
v-if="getBookingItemPreviewRows(booking).length > 0 || getBookingServiceLabels(booking).length > 0 || isBookingLoading(booking)"
|
|
class="booking-option__services"
|
|
:data-testid="`pos-mobile-order-booking-services-${booking.id}`"
|
|
>
|
|
<div class="booking-option__section-header">
|
|
<p class="booking-option__section-title">{{ t("common.services") }}</p>
|
|
<div
|
|
v-if="!isBookingLoading(booking) && getBookingItemPreviewTotal(booking) !== null"
|
|
class="booking-option__section-total"
|
|
:data-testid="`pos-mobile-order-booking-service-total-${booking.id}`"
|
|
>
|
|
<span>{{ totalAmountLabel }}</span>
|
|
<strong>{{ formatCurrencyAmount(getBookingItemPreviewTotal(booking)) }}</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="isBookingLoading(booking)" class="booking-option__item-preview-skeleton">
|
|
<div class="booking-option__item-preview-row is-skeleton" />
|
|
<div class="booking-option__item-preview-row is-skeleton" />
|
|
</div>
|
|
<div v-else-if="getBookingItemPreviewRows(booking).length > 0" class="booking-option__item-preview">
|
|
<div class="booking-option__item-preview-head">
|
|
<span>{{ t("common.quantity") }}</span>
|
|
<span>{{ t("common.services") }}</span>
|
|
<span>{{ t("admin.pos.price_dkk") }}</span>
|
|
</div>
|
|
<div
|
|
v-for="(item, index) in getBookingItemPreviewRows(booking)"
|
|
:key="item.key"
|
|
class="booking-option__item-preview-row"
|
|
:data-testid="`pos-mobile-order-booking-service-row-${booking.id}-${index}`"
|
|
>
|
|
<span class="booking-option__item-preview-qty">{{ item.quantity }}x</span>
|
|
<div class="booking-option__item-preview-main">
|
|
<span
|
|
class="booking-option__item-preview-name"
|
|
:class="{ 'booking-option__item-preview-name--addon': item.depth > 0 }"
|
|
>
|
|
{{ item.depth > 0 ? "+ " : "" }}{{ item.label }}
|
|
</span>
|
|
<span v-if="item.unitPrice !== null && item.quantity > 1" class="booking-option__item-preview-meta">
|
|
{{ formatCurrencyAmount(item.unitPrice) }} / stk
|
|
</span>
|
|
</div>
|
|
<span class="booking-option__item-preview-amount">
|
|
{{ item.totalPrice !== null ? formatCurrencyAmount(item.totalPrice) : "-" }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div v-else class="booking-option__service-list">
|
|
<span
|
|
v-for="service in getBookingServiceLabels(booking)"
|
|
:key="`${booking.id}-${service}`"
|
|
class="booking-option__service-pill"
|
|
>
|
|
{{ service }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="booking-option__actions">
|
|
<button
|
|
class="button is-primary is-fullwidth"
|
|
type="button"
|
|
:class="{ 'is-loading': isBookingLoading(booking) }"
|
|
:disabled="isBookingLoading(booking)"
|
|
:data-testid="`pos-mobile-order-booking-use-${booking.id}`"
|
|
@click="onSelect(booking)"
|
|
>
|
|
{{ t("common.select") }}
|
|
</button>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.booking-popup {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
|
|
.booking-popup__intro {
|
|
margin-bottom: 0.75rem;
|
|
color: #4c5a6e;
|
|
font-size: 0.98rem;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.booking-popup__loading {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.55rem;
|
|
margin-bottom: 0.8rem;
|
|
padding: 0.45rem 0.75rem;
|
|
border-radius: 999px;
|
|
background: rgba(15, 35, 77, 0.08);
|
|
color: #234;
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.booking-popup__list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.85rem;
|
|
}
|
|
|
|
.booking-option {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.7rem;
|
|
border: 1px solid #d7e2f0;
|
|
border-radius: 14px;
|
|
padding: 0.85rem;
|
|
background: #fff;
|
|
box-shadow: 0 8px 22px rgba(21, 42, 76, 0.08);
|
|
overflow: visible;
|
|
}
|
|
|
|
.booking-option--today {
|
|
border-color: #3d7ef0;
|
|
background: linear-gradient(180deg, rgba(61, 126, 240, 0.08), rgba(61, 126, 240, 0.02)), #fff;
|
|
}
|
|
|
|
.booking-option__header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 0.65rem;
|
|
}
|
|
|
|
.booking-option__header-main {
|
|
min-width: 0;
|
|
}
|
|
|
|
.booking-option__header-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
gap: 0.4rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.booking-option__eyebrow {
|
|
margin: 0;
|
|
color: #5c6d84;
|
|
font-size: 0.74rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.booking-option__title {
|
|
margin: 0.15rem 0 0;
|
|
color: #0f234d;
|
|
font-size: 1rem;
|
|
font-weight: 800;
|
|
line-height: 1.25;
|
|
}
|
|
|
|
.booking-option__today-tag {
|
|
border-radius: 999px;
|
|
border: 1px solid rgba(61, 126, 240, 0.2);
|
|
background: rgba(61, 126, 240, 0.12);
|
|
color: #18408b;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.booking-option__tag {
|
|
color: #49576b;
|
|
}
|
|
|
|
.booking-option__settings :deep(.dropdown-menu) {
|
|
z-index: 30;
|
|
}
|
|
|
|
.booking-option__settings :deep(.action-settings-wheel-trigger--text) {
|
|
border-radius: 999px;
|
|
padding: 0.2rem;
|
|
}
|
|
|
|
.booking-option__customer {
|
|
margin: 0;
|
|
color: #1b314f;
|
|
font-size: 0.95rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.booking-option__meta-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 0.7rem;
|
|
margin: 0;
|
|
}
|
|
|
|
.booking-option__meta-item dt,
|
|
.booking-option__section-title {
|
|
margin: 0 0 0.25rem;
|
|
color: #6b7c92;
|
|
font-size: 0.71rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.booking-option__meta-item dd {
|
|
margin: 0;
|
|
color: #14243b;
|
|
font-size: 0.9rem;
|
|
line-height: 1.4;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.booking-option__section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.65rem;
|
|
margin-bottom: 0.4rem;
|
|
}
|
|
|
|
.booking-option__section-total {
|
|
display: inline-flex;
|
|
align-items: baseline;
|
|
gap: 0.35rem;
|
|
color: #173559;
|
|
font-size: 0.71rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.booking-option__section-total strong {
|
|
color: #0f234d;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.booking-option__item-preview {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
.booking-option__item-preview-head,
|
|
.booking-option__item-preview-row {
|
|
display: grid;
|
|
grid-template-columns: 2.5rem minmax(0, 1fr) auto;
|
|
gap: 0.45rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.booking-option__item-preview-head {
|
|
padding: 0 0.1rem;
|
|
color: #6b7c92;
|
|
font-size: 0.68rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.booking-option__item-preview-head span:last-child {
|
|
text-align: right;
|
|
}
|
|
|
|
.booking-option__item-preview-row {
|
|
padding: 0.45rem 0.55rem;
|
|
border: 1px solid #e5edf7;
|
|
border-radius: 10px;
|
|
background: linear-gradient(180deg, #fdfefe 0%, #f7fbff 100%);
|
|
}
|
|
|
|
.booking-option__item-preview-qty {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 1.55rem;
|
|
padding: 0.1rem 0.38rem;
|
|
border-radius: 999px;
|
|
background: #e9f1fb;
|
|
color: #18408b;
|
|
font-size: 0.74rem;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.booking-option__item-preview-main {
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.1rem;
|
|
}
|
|
|
|
.booking-option__item-preview-name {
|
|
color: #14243b;
|
|
font-size: 0.84rem;
|
|
font-weight: 700;
|
|
line-height: 1.2;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.booking-option__item-preview-name--addon {
|
|
color: #24456e;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.booking-option__item-preview-meta {
|
|
color: #6a7890;
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.booking-option__item-preview-amount {
|
|
color: #0f234d;
|
|
font-size: 0.84rem;
|
|
font-weight: 800;
|
|
text-align: right;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.booking-option__item-preview-skeleton {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
.booking-option__item-preview-row.is-skeleton {
|
|
min-height: 2.7rem;
|
|
border-color: transparent;
|
|
color: transparent;
|
|
background: linear-gradient(90deg, #ebf0f7 0%, #f7f9fc 50%, #ebf0f7 100%);
|
|
background-size: 200% 100%;
|
|
animation: mobile-booking-selector-pulse 1.2s linear infinite;
|
|
}
|
|
|
|
.booking-option__service-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
.booking-option__service-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: 1.85rem;
|
|
padding: 0.25rem 0.6rem;
|
|
border-radius: 999px;
|
|
background: #eef4fb;
|
|
color: #173559;
|
|
font-size: 0.82rem;
|
|
font-weight: 600;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.booking-option__actions {
|
|
margin-top: 0.1rem;
|
|
}
|
|
|
|
.booking-option__actions .button {
|
|
border-radius: 10px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
@keyframes mobile-booking-selector-pulse {
|
|
from {
|
|
background-position: 200% 0;
|
|
}
|
|
|
|
to {
|
|
background-position: -200% 0;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 420px) {
|
|
.booking-option__meta-grid {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
|
|
.booking-option__section-header {
|
|
align-items: flex-start;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.booking-option__item-preview-head,
|
|
.booking-option__item-preview-row {
|
|
grid-template-columns: 2.3rem minmax(0, 1fr);
|
|
}
|
|
|
|
.booking-option__item-preview-head span:last-child,
|
|
.booking-option__item-preview-amount {
|
|
grid-column: 2;
|
|
text-align: left;
|
|
}
|
|
}
|
|
</style>
|