PR #345's order_item_reason_policy wiring introduced a path where syncCurrentTransactionToOrder can throw inside the next-step click handler (e.g. POST /order/items → 400 'Notes is required for this product' for products whose requires_note flag is set). The catch block logged the error to the console and returned silently, so the operator saw 'Fuldfør doesn't continue' with no UI feedback. Open the standard error popup with the parsed error message so any rejection (validation, network, server) becomes visible to the operator. Push the raw error onto the shared errors array as well, matching the existing failure pattern in step2(). Adds a regression E2E test in tests/e2e/pos-mobile-order-flow.spec.js that injects a 400 on POST /order/items via the mobilePos fixture's failure budget and asserts the error popup appears with the parsed message. Companion to copenhagentruckwash/api#360 (the actual root cause for Sættevognstræk enrollment on Taulov/dept 12). User report: 'Problemer med indskrivning. Når man trykker fuldfør forsætter den ikke'.
758 lines
22 KiB
Vue
758 lines
22 KiB
Vue
<script setup lang="ts">
|
|
import { useI18n } from "vue-i18n";
|
|
import { onMounted, computed, ref } from "vue";
|
|
import {
|
|
reset_all_values,
|
|
customer_name,
|
|
nextStep,
|
|
searchAndSelectCustomer,
|
|
isCustomerSelected,
|
|
order_id,
|
|
customer_id,
|
|
step,
|
|
reg_1,
|
|
reg_2,
|
|
reg_3,
|
|
reference,
|
|
order_notes,
|
|
order_safety_seal,
|
|
loadCustomerAttributes,
|
|
hasAttribute,
|
|
uploadAttachment,
|
|
restoreStoredPosOrderId,
|
|
saveOrderMetadataField,
|
|
} from "@/components/shop/POSDepartmentProcess.vue";
|
|
import * as POSDepartmentProcess from "@/components/shop/POSDepartmentProcess.vue";
|
|
import { errors } from "@/components/request/HandleGlobalError.vue";
|
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
|
|
import ContinueArrow from "@/components/viewport/elements/icons/ContinueArrow.vue";
|
|
import {
|
|
vehicles,
|
|
metadata,
|
|
getCustomerId,
|
|
popups,
|
|
resetPos,
|
|
attachments,
|
|
transactionHistory,
|
|
transactionItems,
|
|
} from "../objects/PosDepartmentStepMobileFlow.vue";
|
|
import {
|
|
finalizeCurrentMobileOrder,
|
|
getResolvedMobileSafetySeal,
|
|
syncMobileSafetySealState,
|
|
} from "../objects/mobileOrderCompletion.js";
|
|
import Swal from "sweetalert2";
|
|
import LongPressListener from "@/components/viewport/elements/wrappers/LongPressListener.vue";
|
|
import { views } from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
|
|
|
const { t } = useI18n();
|
|
const selectCustomerString = `${
|
|
SessionUser.objects.global.language.select
|
|
} ${SessionUser.objects.global.language.customer.toLowerCase()}`;
|
|
const isCreatingOrder = computed(() => POSDepartmentProcess.isCreatingOrder?.value ?? false);
|
|
|
|
const props = defineProps({
|
|
// If true, the button will have a white background. Default is true.
|
|
isWhite: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
isDark: {
|
|
type: Boolean,
|
|
default: false, // When set to true, this overrides the isWhite prop.
|
|
},
|
|
// Function that returns a Promise, to be executed before proceeding to the next step.
|
|
onBeforeStep: {
|
|
type: Function,
|
|
default: () => Promise.resolve(),
|
|
},
|
|
// Custom action will override the default onClick behavior, if provided.
|
|
customAction: {
|
|
type: Function,
|
|
default: null,
|
|
},
|
|
// Custom is the button disabled state. If provided, this will override the default disabled logic.
|
|
customDisabled: {
|
|
type: Boolean,
|
|
default: null,
|
|
},
|
|
// Custom long press
|
|
customLongPress: {
|
|
type: Function,
|
|
default: null,
|
|
},
|
|
// Button classes
|
|
buttonClasses: {
|
|
type: Array as () => string[],
|
|
default: () => [],
|
|
},
|
|
actionKey: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
copyKey: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
});
|
|
|
|
const getActiveOrderId = () => {
|
|
const parsedOrderId = Number.parseInt(String(order_id.value ?? ""), 10);
|
|
return Number.isInteger(parsedOrderId) && parsedOrderId > 0 ? parsedOrderId : null;
|
|
};
|
|
|
|
const updateReference = async () => {
|
|
const activeOrderId = getActiveOrderId();
|
|
if (activeOrderId) {
|
|
await saveOrderMetadataField("reference", reference.value, activeOrderId);
|
|
}
|
|
};
|
|
|
|
const updateNotes = async () => {
|
|
const activeOrderId = getActiveOrderId();
|
|
if (activeOrderId) {
|
|
await saveOrderMetadataField("notes", order_notes.value, activeOrderId);
|
|
}
|
|
};
|
|
|
|
const updateSafetySeal = async () => {
|
|
const activeOrderId = getActiveOrderId();
|
|
if (activeOrderId) {
|
|
await saveOrderMetadataField("safety_seal", order_safety_seal.value, activeOrderId);
|
|
}
|
|
};
|
|
|
|
// Helpers: extracted for clarity and reuse
|
|
const isNonEmptyString = (val) => typeof val === "string" && val.trim().length > 0;
|
|
|
|
const resolveReferenceFromSources = () => {
|
|
// 1) Metadata reference
|
|
const metaRef = metadata.getReference?.();
|
|
if (isNonEmptyString(metaRef)) return metaRef.trim();
|
|
|
|
// 2) Local reactive reference field
|
|
if (isNonEmptyString(reference.value)) return reference.value.trim();
|
|
|
|
// 3) Vehicle #1 reference (fallback)
|
|
const vehicleRef = vehicles.get(1)?.reference;
|
|
if (isNonEmptyString(vehicleRef)) return String(vehicleRef).trim();
|
|
|
|
return null;
|
|
};
|
|
|
|
const promptForReference = async (initialValue = "") => {
|
|
const result = await Swal.fire({
|
|
title: t("admin.pos.reference_required_title"),
|
|
text: t("admin.pos.reference_required_text"),
|
|
input: "text",
|
|
inputLabel: t("common.reference"),
|
|
inputValue: initialValue,
|
|
showCancelButton: true,
|
|
confirmButtonText: t("admin.pos.confirm"),
|
|
cancelButtonText: t("admin.pos.cancel"),
|
|
preConfirm: (newReference) => {
|
|
if (!isNonEmptyString(newReference)) {
|
|
Swal.showValidationMessage(t("admin.pos.reference_cannot_be_empty"));
|
|
}
|
|
return newReference;
|
|
},
|
|
});
|
|
if (result.isConfirmed && isNonEmptyString(result.value)) {
|
|
return result.value.trim();
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Refactored main function
|
|
const validateReferenceRequirements = async () => {
|
|
await loadCustomerAttributes();
|
|
|
|
const customerRequiresReference = hasAttribute("requiresReferenceNumber");
|
|
|
|
// Try to resolve an existing reference from known sources
|
|
const resolvedRef = resolveReferenceFromSources();
|
|
if (isNonEmptyString(resolvedRef)) {
|
|
// Ensure both local ref and metadata are synchronized
|
|
if (reference.value !== resolvedRef) reference.value = resolvedRef;
|
|
if (metadata.getReference() !== resolvedRef) metadata.setReference(resolvedRef);
|
|
return true;
|
|
}
|
|
|
|
// If customer does not require a reference, we are good even if none was resolved
|
|
if (!customerRequiresReference) {
|
|
return true;
|
|
}
|
|
|
|
// Prompt user for a reference if required and none available
|
|
try {
|
|
const enteredRef = await promptForReference(reference.value ?? "");
|
|
if (isNonEmptyString(enteredRef)) {
|
|
reference.value = enteredRef;
|
|
metadata.setReference(enteredRef);
|
|
console.warn("New reference set:", enteredRef);
|
|
return true;
|
|
}
|
|
console.warn("Reference number is required but was not provided. Aborting operation.");
|
|
return false;
|
|
} catch (error) {
|
|
console.warn("An error occurred while setting the reference number:", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const step1 = async () => {
|
|
/** This function can be used to perform any specific actions for step 1 */
|
|
// Set registration numbers
|
|
reg_1.value = vehicles.vehicle_1.value?.reg || "";
|
|
reg_2.value = vehicles.vehicle_2.value?.reg || "";
|
|
reg_3.value = vehicles.vehicle_3.value?.reg || "";
|
|
// Reset the vehicle selection
|
|
views.vehicleSelection.value = false;
|
|
// Merge references and notes
|
|
await mergeReferences();
|
|
await mergeNotes();
|
|
await mergeSafetySeal();
|
|
// We only need to check if the customer requires reference, if the field is empty.
|
|
if (!isNonEmptyString(reference.value)) {
|
|
const isValid = await validateReferenceRequirements();
|
|
if (!isValid) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Reference is resolved, persist metadata and pending attachments.
|
|
return finalizeStep1();
|
|
};
|
|
const mergeReferences = () => {
|
|
if (metadata.getReference() && metadata.getReference().length >= 1) {
|
|
// If there is no reference, set it to the registration number of vehicle 1
|
|
reference.value = metadata.getReference();
|
|
return updateReference();
|
|
} else if (reference.value && reference.value.length >= 1) {
|
|
// If there is a reference in the field, set it to the metadata
|
|
metadata.setReference(reference.value);
|
|
return updateReference();
|
|
} else if (vehicles.vehicle_1.value?.reference && vehicles.vehicle_1.value?.reference.length >= 1) {
|
|
// If there is no reference, set it to the registration number of vehicle 1
|
|
reference.value = vehicles.vehicle_1.value?.reference;
|
|
metadata.setReference(reference.value);
|
|
return updateReference();
|
|
}
|
|
return Promise.resolve();
|
|
};
|
|
const mergeNotes = () => {
|
|
if (metadata.getNotes() && metadata.getNotes().length >= 1) {
|
|
order_notes.value = metadata.getNotes();
|
|
return updateNotes();
|
|
} else if (order_notes.value && order_notes.value.length >= 1) {
|
|
metadata.setNotes(order_notes.value);
|
|
return updateNotes();
|
|
}
|
|
return Promise.resolve();
|
|
};
|
|
const mergeSafetySeal = () => {
|
|
const resolvedSafetySeal = getResolvedMobileSafetySeal();
|
|
order_safety_seal.value = resolvedSafetySeal;
|
|
syncMobileSafetySealState(resolvedSafetySeal);
|
|
return updateSafetySeal();
|
|
};
|
|
const finalizeStep1 = async () => {
|
|
await mergeReferences();
|
|
await mergeNotes();
|
|
await mergeSafetySeal();
|
|
// Upload the attachments if there are any (And the order id has been created)
|
|
if (attachments.getBase64().length > 0 && order_id.value > 0) {
|
|
const uploadResults = await Promise.all(
|
|
attachments.getBase64().map(async (attachment: any) => {
|
|
try {
|
|
const didUpload = await uploadAttachment(attachment);
|
|
if (didUpload) {
|
|
console.warn("Attachment uploaded successfully");
|
|
attachments.removeBase64(attachment);
|
|
}
|
|
return didUpload;
|
|
} catch (error: any) {
|
|
errors.value.push(error);
|
|
console.warn("An error occurred while uploading the attachment:", error);
|
|
return false;
|
|
}
|
|
})
|
|
);
|
|
|
|
if (uploadResults.some((didUpload) => didUpload === false)) {
|
|
console.warn("One or more attachments could not be uploaded before completion.");
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const addTransactionToHistory = async (orderId: number) => {
|
|
SessionUser.objects.orders.get
|
|
.single(orderId)
|
|
.then((transaction: any) => {
|
|
if (transaction) {
|
|
transactionHistory.add(transaction);
|
|
console.warn("Transaction added to history:", transaction);
|
|
} else {
|
|
console.warn("Unable to add transaction to history: Order not found.");
|
|
}
|
|
})
|
|
.catch((error: any) => {
|
|
errors.value.push(error);
|
|
console.warn("An error occurred while fetching the transaction for history:", error);
|
|
});
|
|
};
|
|
|
|
const toPositiveInteger = (value: any) => {
|
|
const parsedValue = Number.parseInt(String(value ?? ""), 10);
|
|
return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null;
|
|
};
|
|
|
|
const getSelectedBookingId = () => {
|
|
return toPositiveInteger(metadata.getBookingId?.() ?? metadata.bookingId?.value ?? null);
|
|
};
|
|
|
|
const completeStep2Order = async ({
|
|
bookingSafetySeal = null,
|
|
}: {
|
|
bookingSafetySeal?: string | null;
|
|
} = {}) => {
|
|
await finalizeCurrentMobileOrder({
|
|
bookingSafetySeal,
|
|
markOrderCompleted: true,
|
|
});
|
|
popups.select("completed_transaction", {
|
|
message: `Order #${order_id.value} successfully created.`,
|
|
});
|
|
completeOrder();
|
|
};
|
|
|
|
const step2 = async () => {
|
|
/** This function can be used to perform any specific actions for step 2 */
|
|
// If the customer is paying with a card, go to the payment step
|
|
if (isCustomerSelected() && getCustomerId() === 999) {
|
|
nextStep({ isMobile: true });
|
|
return;
|
|
}
|
|
|
|
const selectedBookingId = getSelectedBookingId();
|
|
const hasWashCertificateInBasket = Boolean(transactionItems.containsWashCertificate?.());
|
|
const resolvedSafetySeal = getResolvedMobileSafetySeal();
|
|
const hasResolvedSafetySeal = isNonEmptyString(resolvedSafetySeal);
|
|
const requiresBookingCompletionPopup = Boolean(selectedBookingId && hasWashCertificateInBasket);
|
|
|
|
try {
|
|
if (requiresBookingCompletionPopup && hasResolvedSafetySeal) {
|
|
syncMobileSafetySealState(resolvedSafetySeal);
|
|
await completeStep2Order({
|
|
bookingSafetySeal: resolvedSafetySeal,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (requiresBookingCompletionPopup) {
|
|
const onCompleteBookingWithSafetySeal = async ({ safetySeal = "" } = {}) => {
|
|
syncMobileSafetySealState(safetySeal);
|
|
await completeStep2Order({
|
|
bookingSafetySeal: String(safetySeal ?? ""),
|
|
});
|
|
};
|
|
|
|
popups.select("complete_booking", {
|
|
props: {
|
|
safetySeal: getResolvedMobileSafetySeal(),
|
|
onCompleteWithCertificate: onCompleteBookingWithSafetySeal,
|
|
onCompleteWithoutCertificate: onCompleteBookingWithSafetySeal,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
await completeStep2Order();
|
|
} catch (error: any) {
|
|
errors.value.push(error);
|
|
console.warn("An error occurred while completing the mobile order:", error);
|
|
popups.select("error");
|
|
}
|
|
};
|
|
|
|
const completeOrder = () => {
|
|
addTransactionToHistory(order_id.value);
|
|
setTimeout(() => {
|
|
// If the customer is not paying with a card, reset the POS state (After 3 seconds, to make sure the popup is visible)
|
|
reset_all_values(); // Reset all values
|
|
order_id.value = 0; // Reset the order ID
|
|
popups.clear(); // Clear the popup
|
|
resetPos(); // Reset the POS state
|
|
// Start the process again
|
|
}, 3000);
|
|
};
|
|
|
|
const step3 = () => {
|
|
/** This function can be used to perform any specific actions for step 3 */
|
|
// This step is not defined, but you can add your logic here if needed
|
|
console.warn("Step 3: Card payment step is not defined.");
|
|
};
|
|
|
|
const isProcessingClick = ref(false);
|
|
const isBusy = computed(() => metadata.getLoadingState() || isCreatingOrder.value || isProcessingClick.value);
|
|
|
|
const onClick = async () => {
|
|
// Check if the click is cancelled
|
|
if (isClickCancelled.value || isProcessingClick.value) {
|
|
return;
|
|
}
|
|
// Check if a custom action is provided
|
|
if (props.customAction) {
|
|
isProcessingClick.value = true;
|
|
try {
|
|
await Promise.resolve(props.customAction());
|
|
} finally {
|
|
isProcessingClick.value = false;
|
|
}
|
|
return;
|
|
}
|
|
if (!(isCustomerSelected() && getCustomerId() > 0)) {
|
|
// Show the popup to select a customer
|
|
popups.select("select_customer");
|
|
return;
|
|
}
|
|
|
|
isProcessingClick.value = true;
|
|
try {
|
|
const beforeStepResult = await props.onBeforeStep();
|
|
if (beforeStepResult === false) {
|
|
return;
|
|
}
|
|
// Proceed to the next step
|
|
switch (step.value) {
|
|
case 1: {
|
|
if (!(await step1())) {
|
|
return;
|
|
}
|
|
const restoredOrderId = await restoreStoredPosOrderId({
|
|
validateOrder: true,
|
|
customerId: getCustomerId(),
|
|
departmentId: POSDepartmentProcess.getDepartment(),
|
|
allowCompleted: false,
|
|
syncDepartment: true,
|
|
});
|
|
if (restoredOrderId) {
|
|
console.warn("Order ID retrieved from local storage:", restoredOrderId);
|
|
nextStep({
|
|
isMobile: true,
|
|
orderCreation: false,
|
|
bookingId: metadata.getBookingId?.() ?? null,
|
|
});
|
|
return;
|
|
}
|
|
nextStep({
|
|
isMobile: true,
|
|
orderCreation: true,
|
|
bookingId: metadata.getBookingId?.() ?? null,
|
|
});
|
|
break;
|
|
}
|
|
case 2:
|
|
if (!(await step1())) {
|
|
return;
|
|
}
|
|
await step2();
|
|
break;
|
|
case 3:
|
|
// This step is not defined, but you can add your logic here if needed
|
|
step3();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
console.warn("Next-step action was interrupted:", error);
|
|
// Surface the failure to the operator so silent API rejections don't look
|
|
// like the button did nothing. Backend validation errors typically carry a
|
|
// human-readable message on error.response.data.data.message.
|
|
const parsedError =
|
|
SessionUser.functions.parseErrorMessage(error) || error?.message || "Unknown error";
|
|
errors.value.push(error);
|
|
popups.select("error", { message: parsedError });
|
|
} finally {
|
|
isProcessingClick.value = false;
|
|
}
|
|
};
|
|
|
|
/** Synchronize customer selection state */
|
|
const applyCustomerSelection = () => {
|
|
let shouldApplyCustomerSelection = false;
|
|
if (!isCustomerSelected() && getCustomerId() > 0) {
|
|
// Search and select the customer
|
|
shouldApplyCustomerSelection = true;
|
|
}
|
|
if (isCustomerSelected() && parseInt(customer_id.value) !== getCustomerId()) {
|
|
// The customers are out of sync
|
|
shouldApplyCustomerSelection = true;
|
|
}
|
|
// If any checks failed, apply the customer selection.
|
|
if (shouldApplyCustomerSelection) {
|
|
searchAndSelectCustomer(getCustomerId());
|
|
}
|
|
};
|
|
|
|
// Check customer selection states.
|
|
onMounted(() => {
|
|
applyCustomerSelection();
|
|
});
|
|
|
|
const isRegistrationNumberFilled = () => {
|
|
return vehicles?.vehicle_1?.value?.reg && vehicles.vehicle_1.value?.reg.length >= 4;
|
|
};
|
|
// Is the requirements for clicking the button met?
|
|
const isRequirementsForClickMet = () => {
|
|
// If a custom disabled state is provided, use that.
|
|
if (props.customDisabled !== null) {
|
|
return !props.customDisabled;
|
|
}
|
|
/**
|
|
* States, and their requirements:
|
|
* If any popup is open, the button cannot be clicked.
|
|
* If the metadata state is loading, the button cannot be clicked.
|
|
* If the customer is not selected, the button can be clicked to open the customer selection popup.
|
|
* If the customer is selected, the button can be clicked if the reg_1 is filled. (>= 4 characters)
|
|
*/
|
|
// If any popup is open, the button cannot be clicked.
|
|
if (popups.isSet.value) {
|
|
return false;
|
|
}
|
|
// Prevent duplicate click handling while one action is in-flight.
|
|
if (isBusy.value) {
|
|
return false;
|
|
}
|
|
if (!isCustomerSelected()) {
|
|
return true; // The customer is not selected, so the button can be clicked to open the customer selection popup.
|
|
} else {
|
|
// The customer is selected, so check if the reg_1 is filled.
|
|
return isRegistrationNumberFilled();
|
|
}
|
|
};
|
|
|
|
// Should the button be visible?
|
|
const isVisible = computed(() => {
|
|
/**
|
|
* No popups are open.
|
|
* The step is 1, 2 or 3.
|
|
*/
|
|
return !popups.isSet.value && (step.value === 1 || step.value === 2 || step.value === 3);
|
|
});
|
|
|
|
const isClickCancelled = ref(false);
|
|
const cancelOnClick = () => {
|
|
isClickCancelled.value = true;
|
|
setTimeout(() => {
|
|
isClickCancelled.value = false;
|
|
}, 100); // Reset after 100ms
|
|
};
|
|
|
|
const onLongPress = () => {
|
|
// Cancel the onClick event (since it's a long press)
|
|
cancelOnClick();
|
|
// Check if there's a custom long press handler
|
|
if (props.customLongPress) {
|
|
props.customLongPress();
|
|
return;
|
|
}
|
|
// If no custom long press handler is provided, proceed with the default behavior
|
|
defaultLongPressBehavior();
|
|
};
|
|
|
|
const defaultLongPressBehavior = () => {
|
|
// Select the customer
|
|
popups.select("select_customer");
|
|
};
|
|
|
|
/**
|
|
* Colors:
|
|
* Primary:
|
|
* :buttonClasses="['has-background-primary', 'has-text-black']"
|
|
* Secondary:
|
|
* :buttonClasses="['has-background-primary-dark', 'has-text-black']"
|
|
*/
|
|
</script>
|
|
|
|
<template>
|
|
<LongPressListener @long-press="onLongPress">
|
|
<GenericButton
|
|
@click="onClick"
|
|
:class="{
|
|
white: props.isWhite,
|
|
'has-background-black': props.isDark,
|
|
'is-loading': isBusy,
|
|
...props.buttonClasses.reduce((acc, curr) => ({ ...acc, [curr]: true }), {}),
|
|
}"
|
|
data-testid="pos-mobile-next-step"
|
|
:action-key="props.actionKey"
|
|
:copy-key="props.copyKey"
|
|
class="is-size-6-mobile is-size-5-tablet is-size-4-desktop"
|
|
:disabled="!isRequirementsForClickMet()"
|
|
v-show="isVisible"
|
|
>
|
|
<!-- Default content if no slot is provided -->
|
|
<template v-if="!$slots.default">
|
|
<template v-if="isCustomerSelected()">
|
|
<span class="pos-mobile-cta-content">
|
|
<span class="pos-mobile-cta-label">{{ customer_name }}</span>
|
|
<span class="pos-mobile-cta-value">
|
|
<ContinueArrow style="width: 100%; height: 100%; padding: 6px 12px; gap: 5px" />
|
|
</span>
|
|
</span>
|
|
</template>
|
|
<template v-else>
|
|
<span>{{ selectCustomerString }}</span>
|
|
</template>
|
|
</template>
|
|
<!-- Default slot for custom content -->
|
|
<template v-else>
|
|
<!-- {{ transactionItems.primaryItem.value?.addons?.length }} -->
|
|
<slot></slot>
|
|
</template>
|
|
</GenericButton>
|
|
</LongPressListener>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.pos-mobile-cta-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
.pos-mobile-cta-label {
|
|
min-width: 0;
|
|
flex: 1;
|
|
display: -webkit-box;
|
|
text-align: left;
|
|
line-height: 1.2;
|
|
overflow: hidden;
|
|
overflow-wrap: anywhere;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 2;
|
|
}
|
|
|
|
.pos-mobile-cta-value {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.has-text-black > :not(.icon) {
|
|
color: black !important;
|
|
}
|
|
|
|
:deep(.pos-mobile-cta-content) {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
:deep(.pos-mobile-cta-label) {
|
|
min-width: 0;
|
|
flex: 1;
|
|
display: -webkit-box;
|
|
text-align: left;
|
|
line-height: 1.2;
|
|
overflow: hidden;
|
|
overflow-wrap: anywhere;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 2;
|
|
}
|
|
|
|
:deep(.pos-mobile-cta-value) {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
:deep(.pos-mobile-action-content) {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
width: 100%;
|
|
min-width: 0;
|
|
min-height: 100%;
|
|
box-sizing: border-box;
|
|
padding-inline: 0.75rem;
|
|
}
|
|
|
|
:deep(.pos-mobile-action-label) {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
text-align: left;
|
|
line-height: 1;
|
|
}
|
|
|
|
:deep(.pos-mobile-action-icon) {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 0 0 auto;
|
|
margin-left: auto;
|
|
min-width: 1.25rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
:deep(.pos-mobile-action-icon i) {
|
|
display: block;
|
|
line-height: 1;
|
|
}
|
|
|
|
:deep(.generic-button__content) {
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
/* Loading state */
|
|
.is-loading {
|
|
pointer-events: none;
|
|
cursor: not-allowed;
|
|
}
|
|
.is-loading > *:not(.icon) {
|
|
visibility: hidden;
|
|
opacity: 0;
|
|
transition: opacity 0.6s ease-in-out;
|
|
}
|
|
/* Loading animation for the button */
|
|
.is-loading::after {
|
|
transition: opacity 0.6s ease-in-out;
|
|
content: "";
|
|
position: absolute;
|
|
top: auto;
|
|
left: 50%;
|
|
width: 1em;
|
|
height: 1em;
|
|
margin-top: -0.5em;
|
|
margin-left: -0.5em;
|
|
/* Set the color to the original text color */
|
|
border: 0.2em solid white;
|
|
border-top-color: transparent;
|
|
border-radius: 50%;
|
|
animation: spin 0.6s linear infinite;
|
|
display: inline-block;
|
|
vertical-align: top;
|
|
border-right-color: transparent;
|
|
border-bottom-color: transparent;
|
|
pointer-events: none;
|
|
}
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
</style>
|