Files
pleno-vue/src/components/displays/department/pos/steps/PosDepartmentStep1.vue
T
Jeppe B 5cc22f14bc Fix date-only selection handling
Use local date-only parsing and formatting across date inputs and filters so clicked dates persist exactly.\n\nVerified with local format, lint, i18n, build, full unit tests, PR E2E, focused date E2E, broad smoke reruns, and GitHub PR checks.
2026-07-06 15:52:19 +02:00

810 lines
25 KiB
Vue

<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch, nextTick } from "vue";
import { useI18n } from "vue-i18n";
import NextStep from "@/components/forms/department/pos/buttons/NextStep.vue";
import NextStepError from "@/components/forms/department/pos/error/NextStepError.vue";
import PosSelectedCustomer from "@/components/displays/department/pos/PosSelectedCustomer.vue";
import Cancel from "@/components/forms/department/pos/buttons/Cancel.vue";
import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
import ButtonsBox from "@/components/displays/boxes/ButtonsBox.vue";
import {
customer_name,
isCustomerSelected,
department_id,
getDepartment,
getCurrentStep,
clearCache,
nextStep,
copyLastWashItemsToCurrentOrder,
order_id,
customer_id,
step,
setDesktopStep1PreflightHandler,
clearDesktopStep1PreflightHandler,
pushPosRouteState,
} from "@/components/shop/POSDepartmentProcess.vue";
import ElementTabsBox from "@/components/displays/boxes/ElementTabsBox.vue";
import { POS_STEP_1_VERSION } from "@/config.js";
import SelectVehicleFormPOS from "@/components/forms/department/pos/SelectVehicleFormPOS.vue";
import PosLastScannedLicensePlatesV2 from "@/components/displays/department/pos/PosLastScannedLicensePlatesV2.vue";
import PosDesktopOrderBookingSelectorModal from "@/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue";
import PosDesktopCustomerConflictModal from "@/components/displays/department/pos/steps/elements/PosDesktopCustomerConflictModal.vue";
import PosDesktopDuplicateWarning from "@/components/displays/department/pos/steps/elements/PosDesktopDuplicateWarning.vue";
import { todayLocalDateOnly } from "@/services/dateOnly.js";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { parsePosRouteSearch } from "@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js";
import {
getOrderBookingReferenceValue,
getOrderBookingServiceText,
} from "@/components/displays/department/pos/utils/orderBookingDisplay.js";
const { t, locale } = useI18n();
const routeState = parsePosRouteSearch(window.location.search);
const shouldResetFreshStepOne =
routeState.orderId === null && routeState.customerId === null && (routeState.step === null || routeState.step === 1);
if (shouldResetFreshStepOne) {
clearCache();
}
const right_tabs = ref([
{
name: t("admin.pos.license_plates"),
slot: "license_plates",
icon: "fas fa-car",
hidden: false,
},
{
name: t("admin.pos.customer"),
slot: "customer",
icon: "fas fa-user",
hidden: () => !isCustomerSelected(),
},
]);
const getPreferredRailTab = () => (customer_name.value ? "customer" : "license_plates");
const forceActiveTab = ref(getPreferredRailTab());
const activeRailTab = ref(getPreferredRailTab());
const customerPanelActiveTab = ref("details");
const selectVehicleFormRef = ref(null);
const createEmptyDesktopStep1Context = () => ({
reg1: "",
reg2: "",
bookingMatches: [],
selectedBookingId: null,
skippedBooking: false,
requiresBookingSelection: false,
bookingResolution: "none",
selectionSource: "none",
referenceSource: "empty",
requiresCustomerConflict: false,
customerConflict: null,
committed: false,
source: "initial",
});
const desktopStep1Context = ref(createEmptyDesktopStep1Context());
const desktopModalState = ref(null);
const duplicateCheckId = ref(0);
const duplicateOrders = ref([]);
const duplicateWarningKey = ref("");
const duplicateDetailsExpanded = ref(false);
const pendingNextResolution = ref(false);
const isDesktopLastWashCopying = ref(false);
let desktopStep1CoordinationPromise = Promise.resolve({ canProceed: true });
let focusOnReg1TimeoutId = null;
const isDesktopStep1Active = computed(() => getCurrentStep() === 1);
const setTab = (tab) => {
forceActiveTab.value = tab;
activeRailTab.value = tab;
};
const setCustomerPanelTab = (tab) => {
customerPanelActiveTab.value = tab || "details";
};
watch(
customer_name,
(newValue) => {
const nextRailTab = newValue ? "customer" : "license_plates";
forceActiveTab.value = nextRailTab;
activeRailTab.value = nextRailTab;
if (!newValue) {
customerPanelActiveTab.value = "details";
}
},
{ immediate: true }
);
const focusOnReg1 = () => {
if (focusOnReg1TimeoutId !== null) {
clearTimeout(focusOnReg1TimeoutId);
}
focusOnReg1TimeoutId = setTimeout(() => {
focusOnReg1TimeoutId = null;
if (typeof document === "undefined") {
return;
}
const reg1Input = document.getElementById("reg_1");
if (reg1Input) {
reg1Input.focus();
}
}, 100);
};
const getDepartmentVersion = () => {
if (parseInt(getDepartment()) === 6) {
return 2;
}
return POS_STEP_1_VERSION;
};
const department_version = ref(getDepartmentVersion());
onMounted(() => {
focusOnReg1();
if (getDepartmentVersion() === 1) {
document.getElementById("pos_select_customer_input")?.focus();
}
});
const normalizeDuplicateContext = (context = desktopStep1Context.value) => {
return {
...createEmptyDesktopStep1Context(),
...(context || {}),
reg1: String(context?.reg1 ?? "")
.trim()
.toUpperCase(),
reg2: String(context?.reg2 ?? "")
.trim()
.toUpperCase(),
bookingMatches: Array.isArray(context?.bookingMatches) ? context.bookingMatches : [],
};
};
const getDuplicateStateKey = (context = desktopStep1Context.value) => {
const normalizedContext = normalizeDuplicateContext(context);
return [
normalizedContext.reg1,
normalizedContext.reg2,
normalizedContext.selectedBookingId ?? "",
normalizedContext.bookingResolution,
normalizedContext.skippedBooking ? "skip" : "",
].join("|");
};
const setDesktopStep1Context = (context = {}) => {
desktopStep1Context.value = normalizeDuplicateContext(context);
if (!isDesktopStep1Active.value) {
duplicateOrders.value = [];
duplicateWarningKey.value = "";
duplicateDetailsExpanded.value = false;
desktopModalState.value = null;
return;
}
if (!desktopStep1Context.value.reg1) {
duplicateOrders.value = [];
duplicateWarningKey.value = "";
duplicateDetailsExpanded.value = false;
desktopModalState.value = null;
return;
}
if (desktopStep1Context.value.requiresCustomerConflict) {
duplicateOrders.value = [];
duplicateWarningKey.value = "";
duplicateDetailsExpanded.value = false;
desktopModalState.value = "customer_conflict";
return;
}
if (desktopStep1Context.value.requiresBookingSelection) {
duplicateOrders.value = [];
duplicateWarningKey.value = "";
duplicateDetailsExpanded.value = false;
desktopModalState.value = "booking_selection";
return;
}
if (desktopModalState.value === "booking_selection" || desktopModalState.value === "customer_conflict") {
desktopModalState.value = null;
}
};
watch(
isDesktopStep1Active,
(isActive) => {
if (!isActive) {
desktopModalState.value = null;
pendingNextResolution.value = false;
return;
}
setDesktopStep1Context(desktopStep1Context.value);
},
{ immediate: true }
);
const formatBookingDateTime = (booking) => {
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 formatDuplicateOrderDate = (value) => {
if (!value) {
return t("admin.pos.not_found");
}
const parsedValue = new Date(value);
if (Number.isNaN(parsedValue.getTime())) {
return String(value);
}
return new Intl.DateTimeFormat(locale.value || undefined, {
dateStyle: "short",
timeStyle: "short",
}).format(parsedValue);
};
const getDuplicateOrderContent = (order) => {
return [
`${SessionUser.objects.orders.columns.reg_1.label}: ${order?.reg_1 || t("admin.pos.not_found")}`,
`${SessionUser.objects.orders.columns.reference.label}: ${order?.reference || t("admin.pos.not_found")}`,
`${t("admin.pos.customer")}: ${order?.customer_id || t("admin.pos.not_found")}`,
].join(" • ");
};
const bookingSelectionObjects = computed(() => {
return desktopStep1Context.value.bookingMatches.map((booking) => {
const plateText = [booking?.reg_1, booking?.reg_2].filter(Boolean).join(" / ");
const contentSegments = [
`${t("admin.pos.order_booking_selector.customer_label")}: ${booking?.customer_name || t("admin.pos.not_found")}`,
`${t("admin.pos.order_booking_selector.plates_label")}: ${plateText || t("admin.pos.not_found")}`,
`${t("common.reference")}: ${getOrderBookingReferenceValue(booking) || t("admin.pos.not_found")}`,
`${t("common.services")}: ${getOrderBookingServiceText(booking) || t("admin.pos.not_found")}`,
];
return {
id: booking.id,
label: t("admin.pos.order_booking_selector.option_title", {
id: booking.id,
datetime: formatBookingDateTime(booking),
}),
content: contentSegments.join(" • "),
buttons: [
{
label: t("common.select"),
action: () => handleBookingSelection(booking),
color: "primary",
testId: `pos-desktop-order-booking-use-${booking.id}`,
},
],
};
});
});
const duplicateDetailsObjects = computed(() => {
return duplicateOrders.value.map((order) => ({
id: Number(order.id),
label: `${t("admin.pos.order")} #${order.id} - ${formatDuplicateOrderDate(order.created_at)}`,
content: getDuplicateOrderContent(order),
buttons: [
{
label: t("admin.pos.show_details"),
action: () => {
SessionUser.functions.redirectTo.department(department_id.value, `/modules/pos/orders/${order.id}`);
},
color: "dark",
testId: `pos-desktop-duplicate-order-open-${order.id}`,
},
],
}));
});
const isInlineDuplicateWarningVisible = computed(() => {
if (!isDesktopStep1Active.value || desktopModalState.value !== null) {
return false;
}
const normalizedContext = normalizeDuplicateContext(desktopStep1Context.value);
if (!normalizedContext.committed || !normalizedContext.reg1 || duplicateOrders.value.length === 0) {
return false;
}
return duplicateWarningKey.value === getDuplicateStateKey(normalizedContext);
});
const isCustomerRulesPanelActive = computed(
() => activeRailTab.value === "customer" && customerPanelActiveTab.value === "rules"
);
const shouldShowDuplicateWarningInRulesFooter = computed(
() => isInlineDuplicateWarningVisible.value && isCustomerRulesPanelActive.value
);
const shouldShowDuplicateWarningInActionRail = computed(
() => isInlineDuplicateWarningVisible.value && !isCustomerRulesPanelActive.value
);
const shouldShowRulesFooterActions = computed(() => isCustomerRulesPanelActive.value);
const shouldShowActionRailControls = computed(() => !isCustomerRulesPanelActive.value);
const duplicateDetailsToggleLabel = computed(() =>
duplicateDetailsExpanded.value ? t("admin.pos.hide_order_details") : t("admin.pos.show_order_details")
);
const resumePendingNextStep = async () => {
if (!pendingNextResolution.value) {
return;
}
pendingNextResolution.value = false;
await nextTick();
await nextStep({ isMobile: false, orderCreation: true });
};
const toggleDuplicateDetails = () => {
duplicateDetailsExpanded.value = !duplicateDetailsExpanded.value;
};
const fetchDuplicateOrdersForContext = async (context) => {
const normalizedContext = normalizeDuplicateContext(context);
if (!normalizedContext.reg1) {
duplicateOrders.value = [];
duplicateWarningKey.value = "";
duplicateDetailsExpanded.value = false;
return [];
}
const requestId = Date.now();
duplicateCheckId.value = requestId;
try {
const response = await SessionUser.request(SessionUser.objects.orders.meta.endpoint, "GET", {
filters: `reg_1:${normalizedContext.reg1},department_id:${department_id.value},created_at-date_from:${
todayLocalDateOnly()
},created_at-date_to:${todayLocalDateOnly()}`,
limit: 5,
});
if (duplicateCheckId.value !== requestId) {
return duplicateOrders.value;
}
if (getDuplicateStateKey(normalizedContext) !== getDuplicateStateKey(desktopStep1Context.value)) {
return duplicateOrders.value;
}
const orders = Array.isArray(response?.data?.data) ? response.data.data : [];
const nextDuplicateWarningKey = getDuplicateStateKey(normalizedContext);
if (duplicateWarningKey.value !== nextDuplicateWarningKey) {
duplicateDetailsExpanded.value = false;
}
duplicateOrders.value = orders;
duplicateWarningKey.value = nextDuplicateWarningKey;
return orders;
} catch (error) {
console.error("Error checking for duplicate orders:", error);
duplicateOrders.value = [];
duplicateWarningKey.value = "";
duplicateDetailsExpanded.value = false;
return [];
}
};
const ensureDuplicateWarningState = async (context, options = {}) => {
const normalizedContext = normalizeDuplicateContext(context);
if (!normalizedContext.reg1) {
duplicateOrders.value = [];
duplicateWarningKey.value = "";
duplicateDetailsExpanded.value = false;
if (desktopModalState.value !== "booking_selection") {
desktopModalState.value = null;
}
return {
canProceed: true,
duplicateOrders: [],
};
}
const orders = await fetchDuplicateOrdersForContext(normalizedContext);
if (orders.length === 0) {
duplicateDetailsExpanded.value = false;
return {
canProceed: true,
duplicateOrders: [],
};
}
return {
canProceed: true,
duplicateOrders: orders,
};
};
const coordinateDesktopStep1 = async (options = {}) => {
const normalizedOptions = {
reason: "commit",
finalizeReg1Input: false,
...options,
};
const context =
(await selectVehicleFormRef.value?.finalizeDesktopStep1Context?.({
source: normalizedOptions.reason,
finalizeReg1Input: normalizedOptions.finalizeReg1Input,
committed: true,
})) || desktopStep1Context.value;
setDesktopStep1Context(context);
if (!context.reg1) {
desktopModalState.value = null;
duplicateOrders.value = [];
duplicateWarningKey.value = "";
duplicateDetailsExpanded.value = false;
pendingNextResolution.value = false;
return {
canProceed: true,
context,
};
}
if (context.requiresCustomerConflict) {
duplicateOrders.value = [];
duplicateWarningKey.value = "";
duplicateDetailsExpanded.value = false;
desktopModalState.value = "customer_conflict";
pendingNextResolution.value = normalizedOptions.reason === "next";
return {
canProceed: false,
context,
};
}
if (context.requiresBookingSelection || context.bookingResolution === "selection_required") {
duplicateOrders.value = [];
duplicateWarningKey.value = "";
duplicateDetailsExpanded.value = false;
desktopModalState.value = "booking_selection";
pendingNextResolution.value = normalizedOptions.reason === "next";
return {
canProceed: false,
context,
};
}
const duplicateState = await ensureDuplicateWarningState(context, normalizedOptions);
if (!duplicateState.canProceed) {
return {
canProceed: false,
context,
};
}
desktopModalState.value = null;
if (normalizedOptions.reason === "next") {
pendingNextResolution.value = false;
}
return {
canProceed: true,
context,
};
};
const handleDesktopStep1Commit = async (context) => {
setDesktopStep1Context(context);
desktopStep1CoordinationPromise = coordinateDesktopStep1({
reason: context?.source || "commit",
finalizeReg1Input: false,
});
await desktopStep1CoordinationPromise;
};
const handleDesktopStep1Preflight = async ({ reason } = {}) => {
await desktopStep1CoordinationPromise;
if (desktopModalState.value !== null) {
if ((reason || "next") === "next") {
pendingNextResolution.value = true;
}
return false;
}
desktopStep1CoordinationPromise = coordinateDesktopStep1({
reason: reason || "next",
finalizeReg1Input: true,
});
const result = await desktopStep1CoordinationPromise;
return result.canProceed;
};
const handleBookingSelection = async (booking) => {
const context = await selectVehicleFormRef.value?.applyBookingChoice?.(booking, {
source: "booking_selection",
committed: true,
});
setDesktopStep1Context(context);
desktopStep1CoordinationPromise = coordinateDesktopStep1({
reason: "booking_selection",
finalizeReg1Input: false,
});
const result = await desktopStep1CoordinationPromise;
if (result.canProceed) {
await resumePendingNextStep();
}
};
const handleBookingSkip = async () => {
const context = await selectVehicleFormRef.value?.skipBookingChoice?.({
source: "booking_skip",
committed: true,
});
setDesktopStep1Context(context);
desktopStep1CoordinationPromise = coordinateDesktopStep1({
reason: "booking_skip",
finalizeReg1Input: false,
});
const result = await desktopStep1CoordinationPromise;
if (result.canProceed) {
await resumePendingNextStep();
}
};
const handleBookingRefresh = async () => {
await selectVehicleFormRef.value?.refreshOrderBookingMatches?.({
source: "booking_refresh",
committed: false,
});
desktopStep1CoordinationPromise = coordinateDesktopStep1({
reason: "booking_refresh",
finalizeReg1Input: false,
});
const result = await desktopStep1CoordinationPromise;
if (result.canProceed) {
await resumePendingNextStep();
}
};
const handleCustomerConflictKeep = async () => {
const context = await selectVehicleFormRef.value?.keepCustomerConflictChoice?.({
source: "customer_conflict_keep",
committed: true,
});
setDesktopStep1Context(context);
desktopStep1CoordinationPromise = coordinateDesktopStep1({
reason: "customer_conflict_keep",
finalizeReg1Input: false,
});
const result = await desktopStep1CoordinationPromise;
if (result.canProceed) {
await resumePendingNextStep();
}
};
const handleCustomerConflictUseVehicle = async () => {
const context = await selectVehicleFormRef.value?.useVehicleCustomerConflictChoice?.({
source: "customer_conflict_use_vehicle",
committed: true,
});
setDesktopStep1Context(context);
desktopStep1CoordinationPromise = coordinateDesktopStep1({
reason: "customer_conflict_use_vehicle",
finalizeReg1Input: false,
});
const result = await desktopStep1CoordinationPromise;
if (result.canProceed) {
await resumePendingNextStep();
}
};
const handleCustomerConflictCancel = async () => {
const context = await selectVehicleFormRef.value?.cancelCustomerConflictChoice?.({
source: "customer_conflict_cancel",
committed: true,
});
setDesktopStep1Context(context);
desktopStep1CoordinationPromise = coordinateDesktopStep1({
reason: "customer_conflict_cancel",
finalizeReg1Input: false,
});
const result = await desktopStep1CoordinationPromise;
if (result.canProceed) {
await resumePendingNextStep();
}
};
const pushDesktopStepTwoRoute = () => {
if (!order_id.value) {
return;
}
pushPosRouteState(`id=${order_id.value}&customer_id=${customer_id.value}&step=2`);
};
const handleDesktopLastWashCopy = async (payload = {}) => {
if (isDesktopLastWashCopying.value) {
return;
}
isDesktopLastWashCopying.value = true;
try {
await desktopStep1CoordinationPromise;
desktopStep1CoordinationPromise = coordinateDesktopStep1({
reason: "copy_last_wash",
finalizeReg1Input: true,
});
const result = await desktopStep1CoordinationPromise;
if (!result.canProceed) {
return;
}
const didCopy = await copyLastWashItemsToCurrentOrder(payload?.items || [], {
sourceReference: payload?.order?.reference,
});
if (!didCopy) {
return;
}
step.value = 2;
pushDesktopStepTwoRoute();
} catch (error) {
console.error("Unable to copy last wash to current order:", error);
} finally {
isDesktopLastWashCopying.value = false;
}
};
onMounted(() => {
setDesktopStep1PreflightHandler(handleDesktopStep1Preflight);
});
onBeforeUnmount(() => {
if (focusOnReg1TimeoutId !== null) {
clearTimeout(focusOnReg1TimeoutId);
focusOnReg1TimeoutId = null;
}
clearDesktopStep1PreflightHandler(handleDesktopStep1Preflight);
});
watch(
isInlineDuplicateWarningVisible,
(isVisible) => {
if (!isVisible) {
duplicateDetailsExpanded.value = false;
}
},
{ immediate: true }
);
</script>
<template>
<div data-testid="pos-step-1">
<div class="pos-shell">
<div class="pos-main">
<WhiteBox class="pos-card">
<template #default>
<SelectVehicleFormPOS
ref="selectVehicleFormRef"
:is-desktop-last-wash-copying="isDesktopLastWashCopying"
@update:desktopStep1Context="setDesktopStep1Context"
@commit:desktopStep1="handleDesktopStep1Commit"
@copy-last-wash="handleDesktopLastWashCopy"
/>
</template>
</WhiteBox>
</div>
<div class="pos-rail pos-rail--sticky">
<WhiteBox class="pos-card pos-card--flush">
<template #default>
<ElementTabsBox
:tabs="right_tabs"
default-active-tab="license_plates"
class="pos-card-tabs"
:allowCompactWhenOneTab="true"
:force-active-tab="forceActiveTab"
@update:activeTab="setTab"
>
<template #customer>
<PosSelectedCustomer variant="sidebar" @update:activeTab="setCustomerPanelTab">
<template v-if="shouldShowRulesFooterActions" #rules-footer>
<PosDesktopDuplicateWarning
v-if="shouldShowDuplicateWarningInRulesFooter"
:title="t('admin.pos.warning')"
:message="t('admin.pos.duplicate_order_warning')"
:toggle-label="duplicateDetailsToggleLabel"
:details-expanded="duplicateDetailsExpanded"
:duplicate-orders="duplicateDetailsObjects"
@toggle-details="toggleDuplicateDetails"
/>
<NextStepError class="is-fullwidth" :clear-automatically="true" :clear-delay="8000" />
<ButtonsBox class="pos-actions pos-actions--stacked">
<NextStep class="is-fullwidth" tabindex="6" />
</ButtonsBox>
</template>
</PosSelectedCustomer>
</template>
<template #license_plates>
<PosLastScannedLicensePlatesV2 />
</template>
</ElementTabsBox>
</template>
</WhiteBox>
</div>
<div class="pos-shell-actions">
<ButtonsBox class="pos-actions pos-actions--stacked">
<Cancel tabindex="5" class="is-fullwidth" />
</ButtonsBox>
<div v-if="shouldShowActionRailControls" class="pos-shell-actions__rail" data-testid="pos-step-1-action-rail">
<PosDesktopDuplicateWarning
v-if="shouldShowDuplicateWarningInActionRail"
:title="t('admin.pos.warning')"
:message="t('admin.pos.duplicate_order_warning')"
:toggle-label="duplicateDetailsToggleLabel"
:details-expanded="duplicateDetailsExpanded"
:duplicate-orders="duplicateDetailsObjects"
@toggle-details="toggleDuplicateDetails"
/>
<NextStepError class="is-fullwidth" :clear-automatically="true" :clear-delay="8000" />
<ButtonsBox class="pos-actions pos-actions--stacked">
<NextStep class="is-fullwidth" tabindex="6" />
</ButtonsBox>
</div>
</div>
</div>
<PosDesktopOrderBookingSelectorModal
:isActive="isDesktopStep1Active && desktopModalState === 'booking_selection'"
:allowClose="false"
:title="t('admin.pos.order_booking_selector.title')"
:message="t('admin.pos.order_booking_selector.help_text')"
:bookings="desktopStep1Context.bookingMatches"
:departmentId="department_id"
:matchedPlate="desktopStep1Context.reg1"
@select="handleBookingSelection"
@skip="handleBookingSkip"
@refresh-bookings="handleBookingRefresh"
/>
<PosDesktopCustomerConflictModal
:isActive="isDesktopStep1Active && desktopModalState === 'customer_conflict'"
:allowClose="false"
:title="t('admin.pos.customer_conflict.title')"
:message="t('admin.pos.customer_conflict.help_text')"
:conflict="desktopStep1Context.customerConflict"
@keep-selected-customer="handleCustomerConflictKeep"
@use-vehicle-customer="handleCustomerConflictUseVehicle"
@cancel="handleCustomerConflictCancel"
/>
</div>
</template>
<style scoped></style>