3308 lines
104 KiB
Vue
3308 lines
104 KiB
Vue
<script setup>
|
|
import { onMounted, onBeforeUnmount, computed, nextTick, watch } from "vue";
|
|
import { useSlots } from "vue";
|
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
|
import ActionSettingsWheelToggleItem from "@/components/displays/buttons/ActionSettingsWheelToggleItem.vue";
|
|
import Swal from "sweetalert2";
|
|
import { ref } from "vue";
|
|
import CustomerModal from "@/components/displays/modals/CustomerModal.vue";
|
|
import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue";
|
|
import { useI18n } from "vue-i18n";
|
|
import { getReleaseRuntimeApiBaseUrl } from "@/services/releaseTimeline.js";
|
|
import { getCustomerRuleDefinitions } from "@/features/customer/customerRuleRegistry.js";
|
|
import {
|
|
buildCustomerAttributeTargetPayload,
|
|
createCustomerAttribute,
|
|
deleteCustomerAttribute,
|
|
extractCustomerAttributesData,
|
|
listCustomerAttributes,
|
|
} from "@/features/customer/customerAttributeService.js";
|
|
|
|
const { t } = useI18n();
|
|
const slots = useSlots();
|
|
const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"];
|
|
const officeExtensions = [".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"];
|
|
const emit = defineEmits(["deleted", "flagCreated"]);
|
|
const emitDeleted = () => {
|
|
emit("deleted");
|
|
};
|
|
const emitFlagCreated = (flag) => {
|
|
emit("flagCreated", flag);
|
|
};
|
|
const props = defineProps({
|
|
icon: {
|
|
type: String,
|
|
default: "fas fa-cog",
|
|
},
|
|
label: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
triggerButtonVariant: {
|
|
type: String,
|
|
default: "dark",
|
|
},
|
|
user_id: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
order_id: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
invoice_collection_id: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
reg_1: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
reg_2: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
reg_3: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
order_booking_id: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
customer_number: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
refreshFunction: {
|
|
type: Function,
|
|
default: () => {
|
|
console.warn("Refresh function is not defined");
|
|
},
|
|
},
|
|
department_id: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
attachments: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
displayActionsDirectly: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
directSectionKeys: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
allowBookingCompletion: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
allowBookingDeletion: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
department_lane_id: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
subuserGrant: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
flagTarget: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
});
|
|
|
|
const customerModalVisible = ref(false);
|
|
const userIdFromCustomerNumber = ref(null);
|
|
const userLookupRequestId = ref(0);
|
|
const isResolvingUserId = ref(false);
|
|
const customerRuleAttributes = ref([]);
|
|
const customerRuleAttributesLoaded = ref(false);
|
|
const customerRuleAttributesLoading = ref(false);
|
|
const dropdownRoot = ref(null);
|
|
const dropdownContent = ref(null);
|
|
const isDropdownOpen = ref(false);
|
|
const dropdownInstanceId = Math.random().toString(36).substring(2, 15);
|
|
const dropdownPlacement = ref("bottom");
|
|
const isDropdownPlacementLocked = ref(false);
|
|
const shouldOpenDropdownUp = ref(false);
|
|
const dropdownMaxHeight = ref(null);
|
|
const isDesktopFlyoutLayout = ref(false);
|
|
const isFixedPosition = ref(false);
|
|
const fixedPositionStyles = ref({});
|
|
const activeDesktopFlyoutSectionKey = ref(null);
|
|
const activeAttachmentId = ref(null);
|
|
const previewLoadingAttachmentId = ref(null);
|
|
const deletingAttachmentId = ref(null);
|
|
const previewSourcesById = ref({});
|
|
const previewRequestsInFlight = new Set();
|
|
const generatedObjectUrls = new Set();
|
|
let previewStateGeneration = 0;
|
|
let contentResizeObserver = null;
|
|
let dropdownLayoutUpdateId = 0;
|
|
const desktopFlyoutMinViewportWidth = 1400;
|
|
const desktopFlyoutRootPanelWidthRem = 15;
|
|
const desktopFlyoutSubmenuWidthRem = 17;
|
|
const desktopFlyoutPanelGapPx = 12;
|
|
|
|
const closeDropdown = () => {
|
|
isDropdownOpen.value = false;
|
|
};
|
|
|
|
const openDropdown = () => {
|
|
if (!isDropdownOpen.value) {
|
|
window.dispatchEvent(
|
|
new CustomEvent("action-settings-wheel:opened", {
|
|
detail: {
|
|
id: dropdownInstanceId,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
isDropdownOpen.value = true;
|
|
};
|
|
|
|
const toggleDropdown = () => {
|
|
if (isDropdownOpen.value) {
|
|
closeDropdown();
|
|
return;
|
|
}
|
|
|
|
openDropdown();
|
|
};
|
|
|
|
const onActionSelected = () => {
|
|
closeDropdown();
|
|
};
|
|
|
|
const isTextTriggerButton = computed(() => props.triggerButtonVariant === "text");
|
|
const isIconOnlyTriggerButton = computed(() => props.label.length === 0);
|
|
const getTriggerButtonClass = computed(() => ({
|
|
button: true,
|
|
"is-small": true,
|
|
"is-dark": !isTextTriggerButton.value,
|
|
"action-settings-wheel-trigger": true,
|
|
"action-settings-wheel-trigger--active": isDropdownOpen.value,
|
|
"action-settings-wheel-trigger--text": isTextTriggerButton.value,
|
|
"action-settings-wheel-trigger--icon-only": isIconOnlyTriggerButton.value,
|
|
}));
|
|
const dropdownClass = computed(() => ({
|
|
"is-active": isDropdownOpen.value,
|
|
"is-up": shouldOpenDropdownUp.value,
|
|
}));
|
|
const dropdownContentClass = computed(() => ({
|
|
"action-settings-wheel-dropdown-content--desktop-flyout": isDesktopFlyoutLayout.value,
|
|
}));
|
|
const dropdownMenuStyle = computed(() => {
|
|
if (!isFixedPosition.value) {
|
|
return {};
|
|
}
|
|
|
|
return {
|
|
position: "fixed",
|
|
zIndex: 10000,
|
|
left: "auto",
|
|
bottom: "auto",
|
|
paddingTop: 0,
|
|
paddingBottom: 0,
|
|
...fixedPositionStyles.value,
|
|
};
|
|
});
|
|
const dropdownContentStyle = computed(() => {
|
|
if (isFixedPosition.value) {
|
|
return {
|
|
maxHeight: dropdownMaxHeight.value ? `${dropdownMaxHeight.value}px` : "95vh",
|
|
overflowY: "auto",
|
|
overscrollBehavior: "contain",
|
|
};
|
|
}
|
|
|
|
if (!dropdownMaxHeight.value) {
|
|
return {};
|
|
}
|
|
|
|
return {
|
|
maxHeight: `${dropdownMaxHeight.value}px`,
|
|
overflowY: "auto",
|
|
overscrollBehavior: "contain",
|
|
};
|
|
});
|
|
|
|
const resetDropdownLayout = () => {
|
|
dropdownPlacement.value = "bottom";
|
|
isDropdownPlacementLocked.value = false;
|
|
shouldOpenDropdownUp.value = false;
|
|
dropdownMaxHeight.value = null;
|
|
isFixedPosition.value = false;
|
|
fixedPositionStyles.value = {};
|
|
};
|
|
|
|
const isVisibleFixedInsetElement = (element) => {
|
|
if (!(element instanceof HTMLElement)) {
|
|
return false;
|
|
}
|
|
|
|
const styles = window.getComputedStyle(element);
|
|
if (styles.display === "none" || styles.visibility === "hidden" || Number(styles.opacity || "1") === 0) {
|
|
return false;
|
|
}
|
|
|
|
return styles.position === "fixed" || styles.position === "sticky";
|
|
};
|
|
|
|
const getInsetElements = (selectors) =>
|
|
selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
|
|
|
|
const getViewportInsets = () => {
|
|
const viewportPadding = 12;
|
|
const fixedTopElements = Array.from(new Set(getInsetElements([".navbar.is-fixed-top"])));
|
|
const fixedBottomElements = Array.from(
|
|
new Set(getInsetElements([".navbar.is-fixed-bottom", ".fixed-bottom-footer", ".request-queue-progress-shell"]))
|
|
);
|
|
|
|
const topInset = fixedTopElements.reduce((maxInset, element) => {
|
|
if (!isVisibleFixedInsetElement(element)) {
|
|
return maxInset;
|
|
}
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
if (rect.bottom <= 0 || rect.top > viewportPadding) {
|
|
return maxInset;
|
|
}
|
|
|
|
return Math.max(maxInset, Math.ceil(rect.bottom));
|
|
}, 0);
|
|
|
|
const bottomInset = fixedBottomElements.reduce((maxInset, element) => {
|
|
if (!isVisibleFixedInsetElement(element)) {
|
|
return maxInset;
|
|
}
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
if (rect.top >= window.innerHeight || rect.bottom < window.innerHeight - viewportPadding) {
|
|
return maxInset;
|
|
}
|
|
|
|
return Math.max(maxInset, Math.ceil(window.innerHeight - rect.top));
|
|
}, 0);
|
|
|
|
return {
|
|
top: topInset + viewportPadding,
|
|
bottom: bottomInset + viewportPadding,
|
|
};
|
|
};
|
|
|
|
const getDropdownPlacementSpaces = (triggerRect, viewportInsets = getViewportInsets()) => ({
|
|
bottom: Math.max(window.innerHeight - triggerRect.bottom - viewportInsets.bottom, 0),
|
|
top: Math.max(triggerRect.top - viewportInsets.top, 0),
|
|
});
|
|
|
|
const resolveDropdownPlacement = (triggerRect, menuHeight, viewportInsets = getViewportInsets()) => {
|
|
const spaces = getDropdownPlacementSpaces(triggerRect, viewportInsets);
|
|
|
|
return menuHeight > spaces.bottom && spaces.top > spaces.bottom ? "top" : "bottom";
|
|
};
|
|
|
|
const getDropdownAvailableHeight = (triggerRect, viewportInsets = getViewportInsets()) => {
|
|
const spaces = getDropdownPlacementSpaces(triggerRect, viewportInsets);
|
|
|
|
return dropdownPlacement.value === "top" ? spaces.top : spaces.bottom;
|
|
};
|
|
|
|
const lockDropdownPlacement = (triggerRect, menuHeight, viewportInsets = getViewportInsets()) => {
|
|
if (!isDropdownPlacementLocked.value) {
|
|
dropdownPlacement.value = resolveDropdownPlacement(triggerRect, menuHeight, viewportInsets);
|
|
isDropdownPlacementLocked.value = true;
|
|
}
|
|
|
|
shouldOpenDropdownUp.value = dropdownPlacement.value === "top";
|
|
};
|
|
|
|
const updateDropdownLayout = async () => {
|
|
const updateId = ++dropdownLayoutUpdateId;
|
|
|
|
if (!isDropdownOpen.value || !dropdownRoot.value || !dropdownContent.value) {
|
|
resetDropdownLayout();
|
|
return;
|
|
}
|
|
|
|
await nextTick();
|
|
|
|
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownRoot.value || !dropdownContent.value) {
|
|
return;
|
|
}
|
|
|
|
const triggerElement = dropdownRoot.value.querySelector(".dropdown-trigger");
|
|
const triggerRect = (triggerElement ?? dropdownRoot.value).getBoundingClientRect();
|
|
syncDesktopFlyoutState(triggerRect);
|
|
|
|
await nextTick();
|
|
|
|
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownContent.value) {
|
|
return;
|
|
}
|
|
|
|
const viewportInsets = getViewportInsets();
|
|
const viewportTop = viewportInsets.top;
|
|
const viewportBottom = window.innerHeight - viewportInsets.bottom;
|
|
const menuHeight = Math.ceil(dropdownContent.value.scrollHeight);
|
|
lockDropdownPlacement(triggerRect, menuHeight, viewportInsets);
|
|
const availableHeight = getDropdownAvailableHeight(triggerRect, viewportInsets);
|
|
const nextMaxHeight = availableHeight > 0 && menuHeight > availableHeight ? Math.floor(availableHeight) : null;
|
|
|
|
dropdownMaxHeight.value = nextMaxHeight;
|
|
|
|
await nextTick();
|
|
|
|
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownContent.value) {
|
|
return;
|
|
}
|
|
|
|
syncDesktopFlyoutPosition();
|
|
|
|
await nextTick();
|
|
|
|
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownContent.value) {
|
|
return;
|
|
}
|
|
|
|
const renderedMenuRect = dropdownContent.value.getBoundingClientRect();
|
|
const overflowAbove = Math.max(viewportTop - renderedMenuRect.top, 0);
|
|
const overflowBelow = Math.max(renderedMenuRect.bottom - viewportBottom, 0);
|
|
|
|
if (overflowAbove <= 0 && overflowBelow <= 0) {
|
|
return;
|
|
}
|
|
|
|
const boundedHeight = Math.floor(renderedMenuRect.height - overflowAbove - overflowBelow);
|
|
dropdownMaxHeight.value = boundedHeight > 0 ? boundedHeight : 1;
|
|
|
|
await nextTick();
|
|
|
|
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value) {
|
|
return;
|
|
}
|
|
|
|
syncDesktopFlyoutPosition();
|
|
};
|
|
|
|
const onDocumentClick = (event) => {
|
|
if (!isDropdownOpen.value) {
|
|
return;
|
|
}
|
|
|
|
if (dropdownRoot.value && !dropdownRoot.value.contains(event.target)) {
|
|
closeDropdown();
|
|
}
|
|
};
|
|
|
|
const onDocumentKeydown = (event) => {
|
|
if (event.key === "Escape") {
|
|
closeDropdown();
|
|
}
|
|
};
|
|
|
|
const onActionSettingsWheelOpened = (event) => {
|
|
if (event?.detail?.id !== dropdownInstanceId) {
|
|
closeDropdown();
|
|
}
|
|
};
|
|
|
|
const onViewportChange = () => {
|
|
if (!isDropdownOpen.value) {
|
|
return;
|
|
}
|
|
|
|
void updateDropdownLayout();
|
|
};
|
|
|
|
const resolveUserId = async () => {
|
|
if (props.user_id) {
|
|
isResolvingUserId.value = false;
|
|
userIdFromCustomerNumber.value = props.user_id;
|
|
return;
|
|
}
|
|
|
|
if (!props.customer_number) {
|
|
isResolvingUserId.value = false;
|
|
userIdFromCustomerNumber.value = null;
|
|
return;
|
|
}
|
|
|
|
const currentLookupId = userLookupRequestId.value + 1;
|
|
userLookupRequestId.value = currentLookupId;
|
|
isResolvingUserId.value = true;
|
|
|
|
try {
|
|
const response = await SessionUser.adminUser.customers.fromCustomerNumber.getUserId(props.customer_number);
|
|
if (currentLookupId !== userLookupRequestId.value) {
|
|
return;
|
|
}
|
|
|
|
userIdFromCustomerNumber.value = response?.data?.data?.user_id ?? null;
|
|
} catch (error) {
|
|
if (currentLookupId !== userLookupRequestId.value) {
|
|
return;
|
|
}
|
|
|
|
userIdFromCustomerNumber.value = null;
|
|
console.error(error);
|
|
} finally {
|
|
if (currentLookupId === userLookupRequestId.value) {
|
|
isResolvingUserId.value = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
const resetCustomerRuleState = () => {
|
|
customerRuleAttributes.value = [];
|
|
customerRuleAttributesLoaded.value = false;
|
|
customerRuleAttributesLoading.value = false;
|
|
};
|
|
|
|
const loadCustomerRuleAttributes = async ({ force = false } = {}) => {
|
|
if (!canViewCustomerRules.value || !customerAttributeTarget.value) {
|
|
return customerRuleAttributes.value;
|
|
}
|
|
|
|
if (!force && (customerRuleAttributesLoaded.value || customerRuleAttributesLoading.value)) {
|
|
return customerRuleAttributes.value;
|
|
}
|
|
|
|
customerRuleAttributesLoading.value = true;
|
|
|
|
try {
|
|
const response = await listCustomerAttributes(customerAttributeTarget.value);
|
|
customerRuleAttributes.value = extractCustomerAttributesData(response);
|
|
customerRuleAttributesLoaded.value = true;
|
|
} catch (error) {
|
|
console.warn("Unable to load customer attributes for action wheel", error);
|
|
} finally {
|
|
customerRuleAttributesLoading.value = false;
|
|
}
|
|
|
|
return customerRuleAttributes.value;
|
|
};
|
|
|
|
const toggleCustomerRuleAttribute = async (attribute, enabled) => {
|
|
if (!customerAttributeTarget.value || !attribute) {
|
|
return;
|
|
}
|
|
|
|
if (enabled) {
|
|
await createCustomerAttribute(customerAttributeTarget.value, attribute);
|
|
|
|
if (!activeCustomerRuleAttributes.value.has(attribute)) {
|
|
customerRuleAttributes.value = [
|
|
...customerRuleAttributes.value,
|
|
{
|
|
attribute,
|
|
...customerAttributeTarget.value,
|
|
},
|
|
];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
await deleteCustomerAttribute(customerAttributeTarget.value, attribute);
|
|
customerRuleAttributes.value = customerRuleAttributes.value.filter((entry) => entry?.attribute !== attribute);
|
|
};
|
|
|
|
const showSetCustomerPassword = (userId) => {
|
|
Swal.fire({
|
|
title: t("admin.pos.settings_wheel.change_password"),
|
|
html: `<input id="swal-input1" class="swal2-input" placeholder="${t("admin.pos.settings_wheel.enter_password")}">`,
|
|
focusConfirm: false,
|
|
preConfirm: () => {
|
|
const password = document.getElementById("swal-input1").value;
|
|
if (password === "") {
|
|
Swal.showValidationMessage(t("admin.pos.settings_wheel.please_enter_password"));
|
|
}
|
|
return password;
|
|
},
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
const password = result.value;
|
|
// Call the API to set the password
|
|
SessionUser.request("/superuser/user/password", "POST", {
|
|
user_id: userId,
|
|
password: password,
|
|
})
|
|
.then(() => {
|
|
Swal.fire({
|
|
title: t("admin.pos.settings_wheel.password_changed"),
|
|
text: t("admin.pos.settings_wheel.password_changed_text"),
|
|
icon: "success",
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
Swal.fire({
|
|
title: t("common.error"),
|
|
text: t("admin.pos.settings_wheel.error_changing_password"),
|
|
icon: "error",
|
|
});
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
const redirectSuperUserInvoiceCollectionPage = (invoiceCollectionId) => {
|
|
// Send the user to the invoice collection page (In a new tab)
|
|
window.open(`/superuser/invoices/${invoiceCollectionId}`, "_blank");
|
|
};
|
|
|
|
const redirectDepartmentOrderPage = async (orderId, newTab = false) => {
|
|
// Send the user to the order page (In a new tab)
|
|
// Get the department id from the order
|
|
await SessionUser.objects.orders.functions
|
|
.get_department_id(props.order_id)
|
|
.then((response) => {
|
|
console.warn(response);
|
|
// Get the department id from the response
|
|
SessionUser.functions.redirectTo.department(response, "modules/pos/orders/" + orderId, !!newTab);
|
|
})
|
|
.catch((error) => {
|
|
console.error(error);
|
|
});
|
|
};
|
|
|
|
const normalizeInvoicePeriodFlagTarget = (target) => {
|
|
if (!target || typeof target !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const targetType = String(target.target_type || target.targetType || "").trim();
|
|
const targetId = Number.parseInt(String(target.target_id || target.targetId || ""), 10);
|
|
if (!targetType || !Number.isInteger(targetId) || targetId < 1) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
target_type: targetType,
|
|
target_id: targetId,
|
|
field: target.field || null,
|
|
};
|
|
};
|
|
|
|
const getInvoicePeriodFlagTargetLabel = (target) => {
|
|
if (!target || typeof target !== "object") {
|
|
return t("admin.pos.settings_wheel.add_flag_target");
|
|
}
|
|
|
|
if (target.target_type === "collected_order_invoice") {
|
|
return t("admin.pos.settings_wheel.add_flag_invoice_collection");
|
|
}
|
|
|
|
if (target.target_type === "order") {
|
|
return t("admin.pos.settings_wheel.add_flag_order");
|
|
}
|
|
|
|
if (target.target_type === "order_field") {
|
|
const orderFieldLabels = {
|
|
customer: t("admin.pos.settings_wheel.add_flag_order_customer"),
|
|
reference: t("admin.pos.settings_wheel.add_flag_order_reference"),
|
|
po: t("admin.pos.settings_wheel.add_flag_order_po"),
|
|
notes: t("admin.pos.settings_wheel.add_flag_order_notes"),
|
|
};
|
|
|
|
return orderFieldLabels[target.field] ?? t("admin.pos.settings_wheel.add_flag_order");
|
|
}
|
|
|
|
if (target.target_type === "order_item") {
|
|
return t("admin.pos.settings_wheel.add_flag_order_item");
|
|
}
|
|
|
|
if (target.target_type === "order_item_field") {
|
|
const orderItemFieldLabels = {
|
|
notes: t("admin.pos.settings_wheel.add_flag_order_item_notes"),
|
|
quantity: t("admin.pos.settings_wheel.add_flag_order_item_quantity"),
|
|
reference: t("admin.pos.settings_wheel.add_flag_order_item_reference"),
|
|
price: t("admin.pos.settings_wheel.add_flag_order_item_price"),
|
|
};
|
|
|
|
return orderItemFieldLabels[target.field] ?? t("admin.pos.settings_wheel.add_flag_order_item");
|
|
}
|
|
|
|
if (target.target_type === "customer") {
|
|
return t("admin.pos.settings_wheel.add_flag_customer");
|
|
}
|
|
|
|
return t("admin.pos.settings_wheel.add_flag_target");
|
|
};
|
|
|
|
const defaultInvoicePeriodFlagTargets = computed(() => {
|
|
const targets = [];
|
|
const explicitTarget = normalizeInvoicePeriodFlagTarget(props.flagTarget);
|
|
|
|
if (explicitTarget && explicitTarget.target_type !== "order_item") {
|
|
targets.push({
|
|
key: "custom",
|
|
label: getInvoicePeriodFlagTargetLabel(explicitTarget),
|
|
target: explicitTarget,
|
|
});
|
|
}
|
|
|
|
if (props.invoice_collection_id) {
|
|
targets.push({
|
|
key: "collected-order-invoice",
|
|
label: t("admin.pos.settings_wheel.add_flag_invoice_collection"),
|
|
target: {
|
|
target_type: "collected_order_invoice",
|
|
target_id: props.invoice_collection_id,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (props.order_id) {
|
|
targets.push({
|
|
key: "order",
|
|
label: t("admin.pos.settings_wheel.add_flag_order"),
|
|
target: {
|
|
target_type: "order",
|
|
target_id: props.order_id,
|
|
},
|
|
});
|
|
[
|
|
["customer", t("admin.pos.settings_wheel.add_flag_order_customer")],
|
|
["reference", t("admin.pos.settings_wheel.add_flag_order_reference")],
|
|
["po", t("admin.pos.settings_wheel.add_flag_order_po")],
|
|
["notes", t("admin.pos.settings_wheel.add_flag_order_notes")],
|
|
].forEach(([field, label]) => {
|
|
targets.push({
|
|
key: `order-${field}`,
|
|
label,
|
|
target: {
|
|
target_type: "order_field",
|
|
target_id: props.order_id,
|
|
field,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
if (explicitTarget?.target_type === "order_item") {
|
|
[
|
|
[null, t("admin.pos.settings_wheel.add_flag_order_item")],
|
|
["notes", t("admin.pos.settings_wheel.add_flag_order_item_notes")],
|
|
["quantity", t("admin.pos.settings_wheel.add_flag_order_item_quantity")],
|
|
["reference", t("admin.pos.settings_wheel.add_flag_order_item_reference")],
|
|
["price", t("admin.pos.settings_wheel.add_flag_order_item_price")],
|
|
].forEach(([field, label]) => {
|
|
targets.push({
|
|
key: `order-item-${field || "all"}`,
|
|
label,
|
|
target: {
|
|
target_type: field ? "order_item_field" : "order_item",
|
|
target_id: explicitTarget.target_id,
|
|
field,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
if (props.customer_number && !targets.some((item) => item.target.target_type === "customer")) {
|
|
targets.push({
|
|
key: "customer",
|
|
label: t("admin.pos.settings_wheel.add_flag_customer"),
|
|
target: {
|
|
target_type: "customer",
|
|
target_id: props.customer_number,
|
|
},
|
|
});
|
|
}
|
|
|
|
const seen = new Set();
|
|
return targets.filter((item) => {
|
|
const key = `${item.target.target_type}:${item.target.target_id}:${item.target.field || ""}`;
|
|
if (seen.has(key)) {
|
|
return false;
|
|
}
|
|
seen.add(key);
|
|
return true;
|
|
});
|
|
});
|
|
|
|
const canAddInvoicePeriodFlag = computed(
|
|
() =>
|
|
SessionUser.canAccessSuperUser() &&
|
|
SessionUser.hasPermission("add_invoice_period_flag") &&
|
|
defaultInvoicePeriodFlagTargets.value.length > 0
|
|
);
|
|
|
|
const showCreateInvoicePeriodFlagForm = async (target) => {
|
|
const result = await Swal.fire({
|
|
title: t("admin.pos.settings_wheel.add_flag"),
|
|
input: "textarea",
|
|
inputPlaceholder: t("admin.pos.settings_wheel.add_flag_reason_placeholder"),
|
|
inputValidator: (value) => {
|
|
if (!value || String(value).trim() === "") {
|
|
return t("admin.pos.settings_wheel.add_flag_reason_required");
|
|
}
|
|
return null;
|
|
},
|
|
showCancelButton: true,
|
|
confirmButtonText: t("admin.pos.settings_wheel.add_flag"),
|
|
cancelButtonText: t("common.cancel"),
|
|
});
|
|
|
|
if (!result.isConfirmed) {
|
|
return;
|
|
}
|
|
|
|
const response = await SessionUser.request("/superuser/invoicing/period/flags", "POST", {
|
|
...target,
|
|
reason: String(result.value || "").trim(),
|
|
});
|
|
|
|
emitFlagCreated(response?.data?.data ?? response?.data ?? response);
|
|
};
|
|
|
|
const hasUser = computed(() => Boolean(props.user_id || props.customer_number || userIdFromCustomerNumber.value));
|
|
const customerAttributeTarget = computed(() => {
|
|
if (!hasUser.value) {
|
|
return null;
|
|
}
|
|
|
|
const payload = buildCustomerAttributeTargetPayload({
|
|
userId: props.user_id,
|
|
customerNumber: props.customer_number,
|
|
});
|
|
|
|
return Object.keys(payload).length > 0 ? payload : null;
|
|
});
|
|
const canViewCustomerRules = computed(
|
|
() => hasUser.value && SessionUser.canAccessSuperUser() && SessionUser.hasPermission("list_customer_attributes")
|
|
);
|
|
const customerRuleDefinitions = computed(() => getCustomerRuleDefinitions());
|
|
const activeCustomerRuleAttributes = computed(() => {
|
|
const enabledAttributes = new Set();
|
|
|
|
customerRuleAttributes.value.forEach((entry) => {
|
|
const attributeKey = String(entry?.attribute || "").trim();
|
|
if (attributeKey) {
|
|
enabledAttributes.add(attributeKey);
|
|
}
|
|
});
|
|
|
|
return enabledAttributes;
|
|
});
|
|
const customerRuleItems = computed(() =>
|
|
customerRuleDefinitions.value.map((rule) => {
|
|
const isEnabled = activeCustomerRuleAttributes.value.has(rule.attribute);
|
|
const requiredPermission = isEnabled ? rule.deletePermission : rule.addPermission;
|
|
|
|
return {
|
|
attribute: rule.attribute,
|
|
description: t(rule.descriptionKey),
|
|
disabled: !SessionUser.hasPermission(requiredPermission),
|
|
key: `customer-rule-${rule.attribute}`,
|
|
label: t(rule.labelKey),
|
|
testId: `action-settings-wheel-toggle-customer-rule-${rule.attribute}`,
|
|
type: "toggle",
|
|
value: isEnabled,
|
|
};
|
|
})
|
|
);
|
|
const customerShortcutItems = computed(() => {
|
|
if (!SessionUser.canAccessSuperUser() || !userIdFromCustomerNumber.value) {
|
|
return [];
|
|
}
|
|
|
|
const userId = userIdFromCustomerNumber.value;
|
|
return [
|
|
{
|
|
key: "customer-shortcut-overview",
|
|
icon: "fas fa-user",
|
|
label: t("admin.pos.settings_wheel.shortcut_overview"),
|
|
clickAction: () => SessionUser.functions.redirectTo.superUser(`/users/${userId}`, true),
|
|
},
|
|
{
|
|
key: "customer-shortcut-orders",
|
|
icon: "fas fa-file-alt",
|
|
label: t("admin.pos.settings_wheel.shortcut_orders"),
|
|
clickAction: () => SessionUser.functions.redirectTo.superUser(`/users/${userId}/orders`, true),
|
|
},
|
|
{
|
|
key: "customer-shortcut-pricing",
|
|
icon: "fas fa-tags",
|
|
label: t("admin.pos.settings_wheel.shortcut_pricing"),
|
|
clickAction: () => SessionUser.functions.redirectTo.superUser(`/users/${userId}/pricing`, true),
|
|
},
|
|
{
|
|
key: "customer-shortcut-other",
|
|
icon: "fas fa-sliders-h",
|
|
label: t("admin.pos.settings_wheel.shortcut_other"),
|
|
clickAction: () => SessionUser.functions.redirectTo.superUser(`/users/${userId}/other`, true),
|
|
},
|
|
{
|
|
key: "customer-shortcut-vehicles",
|
|
icon: "fas fa-car",
|
|
label: t("common.vehicles"),
|
|
clickAction: () => SessionUser.functions.redirectTo.superUser(`/users/${userId}/vehicles`, true),
|
|
},
|
|
];
|
|
});
|
|
|
|
watch(
|
|
() => [props.user_id, props.customer_number],
|
|
() => {
|
|
resetCustomerRuleState();
|
|
void resolveUserId();
|
|
|
|
if (props.displayActionsDirectly || isDropdownOpen.value) {
|
|
void loadCustomerRuleAttributes({ force: true });
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
watch(
|
|
() => props.displayActionsDirectly,
|
|
(displayActionsDirectly) => {
|
|
if (displayActionsDirectly) {
|
|
void loadCustomerRuleAttributes();
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
watch(isDropdownOpen, async (isOpen) => {
|
|
if (!isOpen) {
|
|
isDesktopFlyoutLayout.value = false;
|
|
activeDesktopFlyoutSectionKey.value = null;
|
|
clearAttachmentPreviewState();
|
|
resetDropdownLayout();
|
|
return;
|
|
}
|
|
|
|
await nextTick();
|
|
|
|
if (dropdownContent.value) {
|
|
dropdownContent.value.scrollTop = 0;
|
|
}
|
|
|
|
void loadCustomerRuleAttributes();
|
|
void updateDropdownLayout();
|
|
});
|
|
|
|
const attachmentsFromOrder = ref([]);
|
|
const attachmentsFromOrderError = ref(null);
|
|
const SELF_SERVE_WASH_ATTACHMENT_TYPE = "SELF_SERVE_WASH";
|
|
|
|
const normalizePositiveInteger = (value) => {
|
|
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
};
|
|
|
|
const getAttachmentOtherPayload = (attachment) => attachment?.content?.other ?? null;
|
|
|
|
const isSelfServeWashAttachment = (attachment) => {
|
|
const other = getAttachmentOtherPayload(attachment);
|
|
return Boolean(other && typeof other === "object" && other.type === SELF_SERVE_WASH_ATTACHMENT_TYPE);
|
|
};
|
|
|
|
const getSelfServeWashAttachment = computed(() =>
|
|
attachmentsFromOrder.value.find((attachment) => isSelfServeWashAttachment(attachment)) || null
|
|
);
|
|
|
|
const getSelfServeWashPayload = computed(() => getAttachmentOtherPayload(getSelfServeWashAttachment.value));
|
|
|
|
const getSelfServeWashCustomerNumber = computed(() =>
|
|
normalizePositiveInteger(getSelfServeWashPayload.value?.customer_number)
|
|
);
|
|
|
|
const canAcceptSelfServeWashDraft = computed(() =>
|
|
Boolean(
|
|
props.order_id
|
|
&& getSelfServeWashCustomerNumber.value
|
|
&& normalizePositiveInteger(props.customer_number) !== getSelfServeWashCustomerNumber.value
|
|
&& (SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser())
|
|
)
|
|
);
|
|
|
|
const isObjectUrl = (value) => typeof value === "string" && value.startsWith("blob:");
|
|
|
|
const releaseObjectUrl = (value) => {
|
|
if (!isObjectUrl(value)) {
|
|
return;
|
|
}
|
|
|
|
URL.revokeObjectURL(value);
|
|
generatedObjectUrls.delete(value);
|
|
};
|
|
|
|
const clearAttachmentPreviewState = () => {
|
|
previewStateGeneration += 1;
|
|
previewRequestsInFlight.clear();
|
|
Object.values(previewSourcesById.value).forEach((value) => {
|
|
releaseObjectUrl(value);
|
|
});
|
|
previewSourcesById.value = {};
|
|
previewLoadingAttachmentId.value = null;
|
|
activeAttachmentId.value = null;
|
|
};
|
|
|
|
const getAttachmentLabel = (attachment) => {
|
|
if (isSelfServeWashAttachment(attachment)) {
|
|
const customerNumber = normalizePositiveInteger(attachment?.content?.other?.customer_number);
|
|
return customerNumber
|
|
? t("admin.pos.settings_wheel.self_serve_wash_attachment_for_customer", { customerNumber })
|
|
: t("admin.pos.settings_wheel.self_serve_wash_attachment");
|
|
}
|
|
|
|
const other = getAttachmentOtherPayload(attachment);
|
|
const otherLabel = typeof other === "string"
|
|
? other
|
|
: other && typeof other === "object"
|
|
? (other.label || other.type || JSON.stringify(other))
|
|
: null;
|
|
|
|
return (
|
|
attachment?.content?.document ||
|
|
attachment?.content?.image ||
|
|
otherLabel ||
|
|
`Attachment ${attachment?.id ?? ""}`.trim()
|
|
);
|
|
};
|
|
|
|
const isWashCertificateAttachment = (attachment) => {
|
|
const marker = String(getAttachmentOtherPayload(attachment) || "").trim().toUpperCase();
|
|
if (marker === "WASH_CERTIFICATE") {
|
|
return true;
|
|
}
|
|
|
|
return /(?:^|[/\\])wash[_-]?certificate.*\.pdf$/i.test(getAttachmentLabel(attachment));
|
|
};
|
|
|
|
const getAttachmentExtension = (attachment) => {
|
|
const match = String(getAttachmentLabel(attachment))
|
|
.toLowerCase()
|
|
.match(/(\.[a-z0-9]+)$/);
|
|
|
|
return match?.[1] || "";
|
|
};
|
|
|
|
const getAttachmentPreviewKind = (attachment) => {
|
|
const extension = getAttachmentExtension(attachment);
|
|
|
|
if (attachment?.content?.image || imageExtensions.includes(extension)) {
|
|
return "image";
|
|
}
|
|
|
|
if (attachment?.content?.document || extension === ".pdf") {
|
|
return officeExtensions.includes(extension) ? "office" : "document";
|
|
}
|
|
|
|
if (officeExtensions.includes(extension)) {
|
|
return "office";
|
|
}
|
|
|
|
const other = getAttachmentOtherPayload(attachment);
|
|
if (typeof other === "string" && other.startsWith("http")) {
|
|
return "link";
|
|
}
|
|
|
|
if (other) {
|
|
return "text";
|
|
}
|
|
|
|
return "none";
|
|
};
|
|
|
|
const formatAttachmentText = (attachment) => {
|
|
const other = getAttachmentOtherPayload(attachment);
|
|
if (isSelfServeWashAttachment(attachment)) {
|
|
const parts = [
|
|
t("admin.pos.settings_wheel.self_serve_wash_attachment"),
|
|
other?.customer_number
|
|
? `${t("admin.pos.settings_wheel.self_serve_customer")}: #${other.customer_number}`
|
|
: null,
|
|
other?.subuser?.name || other?.subuser?.username || other?.subuser_id
|
|
? `${t("admin.pos.settings_wheel.self_serve_driver")}: ${other?.subuser?.name || other?.subuser?.username || `#${other.subuser_id}`}`
|
|
: null,
|
|
other?.license_plate
|
|
? `${t("pos.license_plate")}: ${other.license_plate}`
|
|
: null,
|
|
other?.elapsed_wash_time_seconds
|
|
? `${t("admin.pos.settings_wheel.self_serve_elapsed")}: ${Math.ceil(Number(other.elapsed_wash_time_seconds) / 60)} min`
|
|
: null,
|
|
].filter(Boolean);
|
|
|
|
return parts.join("\n");
|
|
}
|
|
|
|
if (typeof other === "string") {
|
|
return other;
|
|
}
|
|
|
|
if (other && typeof other === "object") {
|
|
return JSON.stringify(other, null, 2);
|
|
}
|
|
|
|
return "";
|
|
};
|
|
|
|
const getAttachmentPreviewPlaceholderIcon = (attachment) => {
|
|
const previewKind = getAttachmentPreviewKind(attachment);
|
|
|
|
if (previewKind === "image") {
|
|
return "fas fa-image";
|
|
}
|
|
|
|
if (previewKind === "document" || previewKind === "office") {
|
|
return "fas fa-file-alt";
|
|
}
|
|
|
|
if (previewKind === "link") {
|
|
return "fas fa-link";
|
|
}
|
|
|
|
if (previewKind === "text") {
|
|
return "fas fa-align-left";
|
|
}
|
|
|
|
return "fas fa-paperclip";
|
|
};
|
|
|
|
const getAttachmentPreviewPlaceholderLabel = (attachment) => {
|
|
const extension = getAttachmentExtension(attachment).replace(".", "").toUpperCase();
|
|
if (extension) {
|
|
return extension;
|
|
}
|
|
|
|
const previewKind = getAttachmentPreviewKind(attachment);
|
|
if (previewKind === "link") {
|
|
return "LINK";
|
|
}
|
|
|
|
if (previewKind === "text") {
|
|
return "TEXT";
|
|
}
|
|
|
|
return "FILE";
|
|
};
|
|
|
|
const activeAttachment = computed(() => {
|
|
return attachmentsFromOrder.value.find((attachment) => attachment.id === activeAttachmentId.value) || null;
|
|
});
|
|
|
|
const activeAttachmentPreviewKind = computed(() => {
|
|
if (!activeAttachment.value) {
|
|
return "none";
|
|
}
|
|
|
|
return getAttachmentPreviewKind(activeAttachment.value);
|
|
});
|
|
|
|
const activeAttachmentPreviewSource = computed(() => {
|
|
if (!activeAttachment.value) {
|
|
return null;
|
|
}
|
|
|
|
return previewSourcesById.value[activeAttachment.value.id] ?? null;
|
|
});
|
|
|
|
const hasWashCertificateAttachment = computed(() =>
|
|
attachmentsFromOrder.value.some((attachment) => isWashCertificateAttachment(attachment))
|
|
);
|
|
|
|
const hasCachedPreviewSource = (attachmentId) => {
|
|
return Object.prototype.hasOwnProperty.call(previewSourcesById.value, attachmentId);
|
|
};
|
|
|
|
const buildAttachmentPreviewHeaders = () => {
|
|
const headers = {};
|
|
const token = window.localStorage.getItem("token");
|
|
if (token) {
|
|
headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
const isSubuser = window.localStorage.getItem("is_subuser") === "true";
|
|
const selectedCustomerNumber = window.localStorage.getItem("selected_customer_number");
|
|
if (isSubuser && selectedCustomerNumber) {
|
|
headers["X-Customer-Number"] = selectedCustomerNumber;
|
|
}
|
|
|
|
return headers;
|
|
};
|
|
|
|
const shouldRetryPreviewRequestWithAuth = (downloadLink) => {
|
|
if (!downloadLink) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const previewUrl = new URL(downloadLink, window.location.origin);
|
|
const apiOrigin = new URL(getReleaseRuntimeApiBaseUrl(), window.location.origin).origin;
|
|
return previewUrl.origin === apiOrigin || previewUrl.origin === window.location.origin;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const fetchAttachmentPreviewBlob = async (downloadLink, withAuthHeaders = false) => {
|
|
const requestConfig = {
|
|
method: "GET",
|
|
};
|
|
|
|
if (withAuthHeaders) {
|
|
requestConfig.headers = buildAttachmentPreviewHeaders();
|
|
}
|
|
|
|
const response = await fetch(downloadLink, requestConfig);
|
|
if (!response.ok) {
|
|
return null;
|
|
}
|
|
|
|
const fileBlob = await response.blob();
|
|
if (!fileBlob || fileBlob.size === 0) {
|
|
return null;
|
|
}
|
|
|
|
return fileBlob;
|
|
};
|
|
|
|
const createEmbeddablePreviewUrl = async (downloadLink) => {
|
|
if (!downloadLink) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
let fileBlob = await fetchAttachmentPreviewBlob(downloadLink, false);
|
|
if (!fileBlob && shouldRetryPreviewRequestWithAuth(downloadLink)) {
|
|
fileBlob = await fetchAttachmentPreviewBlob(downloadLink, true);
|
|
}
|
|
|
|
if (!fileBlob || fileBlob.size === 0) {
|
|
return null;
|
|
}
|
|
|
|
const objectUrl = URL.createObjectURL(fileBlob);
|
|
generatedObjectUrls.add(objectUrl);
|
|
return objectUrl;
|
|
} catch (error) {
|
|
console.warn("Unable to create embeddable attachment preview blob", error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const loadOrderAttachments = async () => {
|
|
if (!props.order_id) {
|
|
attachmentsFromOrder.value = [];
|
|
attachmentsFromOrderError.value = null;
|
|
clearAttachmentPreviewState();
|
|
return [];
|
|
}
|
|
|
|
attachmentsFromOrderError.value = null;
|
|
|
|
try {
|
|
let response = await SessionUser.objects.orders.functions.fetchAttachments(props.order_id);
|
|
if (!Array.isArray(response)) {
|
|
response = [];
|
|
}
|
|
|
|
attachmentsFromOrder.value = response;
|
|
|
|
if (!response.some((attachment) => attachment.id === activeAttachmentId.value)) {
|
|
activeAttachmentId.value = null;
|
|
}
|
|
|
|
return response;
|
|
} catch (error) {
|
|
const parsedErrorMessage = SessionUser.functions.parseErrorMessage(error);
|
|
console.warn("Error fetching attachments from order:", error, parsedErrorMessage);
|
|
attachmentsFromOrder.value = [];
|
|
attachmentsFromOrderError.value = `ERROR: ${parsedErrorMessage || "Failed to fetch attachments"}`;
|
|
clearAttachmentPreviewState();
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const resolveAttachmentDownloadLink = async (attachment) => {
|
|
if (!props.order_id || !attachment?.id) {
|
|
return null;
|
|
}
|
|
|
|
return SessionUser.objects.orders.functions.downloadAttachment(props.order_id, attachment.id, false);
|
|
};
|
|
|
|
const ensurePreviewSource = async (attachment) => {
|
|
if (!attachment) {
|
|
return null;
|
|
}
|
|
|
|
const currentPreviewStateGeneration = previewStateGeneration;
|
|
const previewKind = getAttachmentPreviewKind(attachment);
|
|
if (!["image", "document"].includes(previewKind)) {
|
|
return null;
|
|
}
|
|
|
|
if (hasCachedPreviewSource(attachment.id) || previewRequestsInFlight.has(attachment.id)) {
|
|
return previewSourcesById.value[attachment.id] ?? null;
|
|
}
|
|
|
|
previewRequestsInFlight.add(attachment.id);
|
|
previewLoadingAttachmentId.value = attachment.id;
|
|
|
|
try {
|
|
const downloadLink = await resolveAttachmentDownloadLink(attachment);
|
|
const previewSource = await createEmbeddablePreviewUrl(downloadLink);
|
|
|
|
if (currentPreviewStateGeneration !== previewStateGeneration) {
|
|
releaseObjectUrl(previewSource);
|
|
return null;
|
|
}
|
|
|
|
previewSourcesById.value = {
|
|
...previewSourcesById.value,
|
|
[attachment.id]: previewSource,
|
|
};
|
|
return previewSource;
|
|
} catch (error) {
|
|
console.warn("Unable to load attachment preview", attachment.id, error);
|
|
if (currentPreviewStateGeneration === previewStateGeneration) {
|
|
previewSourcesById.value = {
|
|
...previewSourcesById.value,
|
|
[attachment.id]: null,
|
|
};
|
|
}
|
|
return null;
|
|
} finally {
|
|
previewRequestsInFlight.delete(attachment.id);
|
|
if (previewLoadingAttachmentId.value === attachment.id) {
|
|
previewLoadingAttachmentId.value = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
const setActiveAttachment = async (attachment) => {
|
|
if (!attachment) {
|
|
activeAttachmentId.value = null;
|
|
return;
|
|
}
|
|
|
|
activeAttachmentId.value = attachment.id;
|
|
await ensurePreviewSource(attachment);
|
|
};
|
|
|
|
const openAttachmentInNewTab = (url) => {
|
|
if (!url) {
|
|
return null;
|
|
}
|
|
|
|
return window.open(url, "_blank", "noopener,noreferrer");
|
|
};
|
|
|
|
const downloadAttachmentFile = (url, attachment) => {
|
|
if (!url) {
|
|
return;
|
|
}
|
|
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.target = "_blank";
|
|
link.rel = "noopener noreferrer";
|
|
link.download = getAttachmentLabel(attachment);
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
};
|
|
|
|
const previewAttachment = async (attachment) => {
|
|
try {
|
|
const previewKind = getAttachmentPreviewKind(attachment);
|
|
if (["image", "document"].includes(previewKind)) {
|
|
const previewSource = (await ensurePreviewSource(attachment)) || (await resolveAttachmentDownloadLink(attachment));
|
|
openAttachmentInNewTab(previewSource);
|
|
return;
|
|
}
|
|
|
|
const downloadLink = await resolveAttachmentDownloadLink(attachment);
|
|
openAttachmentInNewTab(downloadLink);
|
|
} catch (error) {
|
|
console.warn("Unable to preview attachment", attachment?.id, error);
|
|
Swal.fire({
|
|
title: t("common.error"),
|
|
text: t("admin.pos.settings_wheel.error_downloading_attachment"),
|
|
icon: "error",
|
|
});
|
|
}
|
|
};
|
|
|
|
const downloadAttachment = async (attachment) => {
|
|
try {
|
|
const downloadLink = await resolveAttachmentDownloadLink(attachment);
|
|
downloadAttachmentFile(downloadLink, attachment);
|
|
} catch (error) {
|
|
Swal.fire({
|
|
title: t("common.error"),
|
|
text: t("admin.pos.settings_wheel.error_downloading_attachment"),
|
|
icon: "error",
|
|
});
|
|
}
|
|
};
|
|
|
|
const printAttachment = async (attachment) => {
|
|
try {
|
|
const previewKind = getAttachmentPreviewKind(attachment);
|
|
const downloadLink = await resolveAttachmentDownloadLink(attachment);
|
|
|
|
if (!downloadLink) {
|
|
return;
|
|
}
|
|
|
|
if (!["image", "document"].includes(previewKind)) {
|
|
openAttachmentInNewTab(downloadLink);
|
|
return;
|
|
}
|
|
|
|
const printableSource = (await ensurePreviewSource(attachment)) || downloadLink;
|
|
if (previewKind === "image") {
|
|
const printWindow = window.open("", "_blank", "noopener,noreferrer,width=960,height=720");
|
|
if (!printWindow) {
|
|
openAttachmentInNewTab(printableSource);
|
|
return;
|
|
}
|
|
|
|
const escapedTitle = getAttachmentLabel(attachment).replace(/"/g, """);
|
|
printWindow.document.write(`<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>${escapedTitle}</title>
|
|
<style>
|
|
html, body {
|
|
margin: 0;
|
|
min-height: 100%;
|
|
background: #ffffff;
|
|
}
|
|
body {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
img {
|
|
display: block;
|
|
max-width: 100%;
|
|
max-height: 100vh;
|
|
object-fit: contain;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<img id="attachment-print-image" src="${printableSource}" alt="" />
|
|
</body>
|
|
</html>`);
|
|
printWindow.document.close();
|
|
|
|
const printableImage = printWindow.document.getElementById("attachment-print-image");
|
|
if (!printableImage) {
|
|
openAttachmentInNewTab(printableSource);
|
|
return;
|
|
}
|
|
|
|
printableImage.addEventListener(
|
|
"load",
|
|
() => {
|
|
printWindow.focus();
|
|
window.setTimeout(() => {
|
|
printWindow.print();
|
|
}, 150);
|
|
},
|
|
{ once: true }
|
|
);
|
|
printableImage.addEventListener(
|
|
"error",
|
|
() => {
|
|
openAttachmentInNewTab(printableSource);
|
|
},
|
|
{ once: true }
|
|
);
|
|
return;
|
|
}
|
|
|
|
const printWindow = window.open(printableSource, "_blank", "noopener,noreferrer");
|
|
if (!printWindow) {
|
|
openAttachmentInNewTab(printableSource);
|
|
return;
|
|
}
|
|
|
|
const triggerPrint = () => {
|
|
printWindow.focus();
|
|
window.setTimeout(() => {
|
|
printWindow.print();
|
|
}, 250);
|
|
};
|
|
|
|
printWindow.addEventListener("load", triggerPrint, { once: true });
|
|
window.setTimeout(triggerPrint, 900);
|
|
} catch (error) {
|
|
console.warn("Unable to print attachment", attachment?.id, error);
|
|
Swal.fire({
|
|
title: t("common.error"),
|
|
text: t("admin.pos.settings_wheel.error_downloading_attachment"),
|
|
icon: "error",
|
|
});
|
|
}
|
|
};
|
|
|
|
const removeAttachment = async (attachment) => {
|
|
if (!props.order_id || !attachment?.id || deletingAttachmentId.value === attachment.id) {
|
|
return;
|
|
}
|
|
|
|
deletingAttachmentId.value = attachment.id;
|
|
|
|
try {
|
|
const response = await SessionUser.objects.orders.functions.removeAttachment(props.order_id, attachment.id);
|
|
if (!response) {
|
|
throw new Error("Attachment delete failed");
|
|
}
|
|
|
|
releaseObjectUrl(previewSourcesById.value[attachment.id]);
|
|
|
|
const nextPreviewSourcesById = { ...previewSourcesById.value };
|
|
delete nextPreviewSourcesById[attachment.id];
|
|
previewSourcesById.value = nextPreviewSourcesById;
|
|
|
|
await loadOrderAttachments();
|
|
await props.refreshFunction();
|
|
} catch (error) {
|
|
console.warn("Unable to delete attachment", attachment?.id, error);
|
|
Swal.fire({
|
|
title: t("common.error"),
|
|
text: t("admin.pos.settings_wheel.error_downloading_attachment"),
|
|
icon: "error",
|
|
});
|
|
} finally {
|
|
deletingAttachmentId.value = null;
|
|
}
|
|
};
|
|
|
|
watch(
|
|
() => [attachmentsFromOrder.value.length, attachmentsFromOrderError.value, userIdFromCustomerNumber.value],
|
|
() => {
|
|
if (isDropdownOpen.value) {
|
|
void updateDropdownLayout();
|
|
}
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => props.order_id,
|
|
() => {
|
|
void loadOrderAttachments();
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
onMounted(() => {
|
|
document.addEventListener("click", onDocumentClick);
|
|
document.addEventListener("keydown", onDocumentKeydown);
|
|
window.addEventListener("action-settings-wheel:opened", onActionSettingsWheelOpened);
|
|
window.addEventListener("resize", onViewportChange);
|
|
document.addEventListener("scroll", onViewportChange, true);
|
|
|
|
contentResizeObserver = new ResizeObserver(() => {
|
|
if (isDropdownOpen.value) {
|
|
void updateDropdownLayout();
|
|
}
|
|
});
|
|
|
|
if (dropdownContent.value) {
|
|
contentResizeObserver.observe(dropdownContent.value);
|
|
}
|
|
});
|
|
|
|
watch(dropdownContent, (newEl, oldEl) => {
|
|
if (oldEl && contentResizeObserver) {
|
|
contentResizeObserver.unobserve(oldEl);
|
|
}
|
|
if (newEl && contentResizeObserver) {
|
|
contentResizeObserver.observe(newEl);
|
|
}
|
|
});
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
document.removeEventListener("click", onDocumentClick);
|
|
document.removeEventListener("keydown", onDocumentKeydown);
|
|
window.removeEventListener("action-settings-wheel:opened", onActionSettingsWheelOpened);
|
|
window.removeEventListener("resize", onViewportChange);
|
|
document.removeEventListener("scroll", onViewportChange, true);
|
|
clearAttachmentPreviewState();
|
|
|
|
if (contentResizeObserver) {
|
|
contentResizeObserver.disconnect();
|
|
contentResizeObserver = null;
|
|
}
|
|
});
|
|
|
|
const onShowImpersonationQRCode = (src, directLink) => {
|
|
Swal.fire({
|
|
title: t("admin.pos.settings_wheel.scan_qr_to_login"),
|
|
html: `<img src="${src}" alt="QR-kode" />`,
|
|
showCloseButton: true,
|
|
showConfirmButton: !!directLink,
|
|
showCancelButton: true,
|
|
focusConfirm: false,
|
|
confirmButtonText: t("admin.pos.settings_wheel.copy_link"),
|
|
cancelButtonText: t("common.close"),
|
|
preConfirm: () => {
|
|
navigator.clipboard.writeText(directLink);
|
|
Swal.showValidationMessage(t("admin.pos.settings_wheel.link_copied"));
|
|
},
|
|
});
|
|
};
|
|
|
|
const onClickShowImpersonationQRCode = async (userId) => {
|
|
// Get the session token
|
|
const newToken = await SessionUser.superUser.intimidate.getImpersonationToken(userId);
|
|
// Create the impersonation link
|
|
const link = `${window.location.protocol}//${window.location.host}/login/qr?token=${newToken}`;
|
|
// Generate a QR code for the impersonation link
|
|
const response = await SessionUser.superUser.intimidate.showImpersonationQRCode(link);
|
|
// Show the QR code in a modal
|
|
onShowImpersonationQRCode(response.value, link);
|
|
};
|
|
|
|
const hasCustomActionsSlot = computed(() => Boolean(slots.actions));
|
|
|
|
const buildMenuAction = (key, config) => ({
|
|
key,
|
|
disabled: false,
|
|
template: "default",
|
|
type: "action",
|
|
...config,
|
|
});
|
|
|
|
const buildMenuToggleAction = (key, config) => ({
|
|
key,
|
|
disabled: false,
|
|
type: "toggle",
|
|
value: false,
|
|
...config,
|
|
});
|
|
|
|
const buildMenuSection = (key, label, items) => {
|
|
const visibleItems = items.filter(Boolean);
|
|
if (visibleItems.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
key,
|
|
label,
|
|
items: visibleItems,
|
|
};
|
|
};
|
|
|
|
const isToggleMenuItem = (item) => item?.type === "toggle";
|
|
|
|
const downloadOrderAttachment = (attachment) =>
|
|
downloadAttachment(attachment).catch(() => {
|
|
Swal.fire({
|
|
title: t("common.error"),
|
|
text: t("admin.pos.settings_wheel.error_downloading_attachment"),
|
|
icon: "error",
|
|
});
|
|
});
|
|
|
|
const openDepartmentWorkspaceTab = (tabId) =>
|
|
SessionUser.functions.redirectTo.superUser(
|
|
`/departments/${props.department_id}/gateways?tab=${encodeURIComponent(String(tabId))}`,
|
|
true
|
|
);
|
|
|
|
const openPrimaryGatewayPage = async () => {
|
|
if (!props.department_id) {
|
|
SessionUser.functions.redirectTo.superUser("/selfserve/edge-agents", true);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await SessionUser.request(
|
|
`/modules/edge-gateways/workspace/departments/${props.department_id}`,
|
|
"GET"
|
|
);
|
|
const primaryGatewayId = response?.data?.data?.summary?.primary_gateway?.id ?? null;
|
|
|
|
if (primaryGatewayId) {
|
|
SessionUser.functions.redirectTo.superUser(
|
|
`/selfserve/edge-agents/${encodeURIComponent(String(primaryGatewayId))}/overview`,
|
|
true
|
|
);
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.warn("Unable to resolve primary gateway from department workspace", error);
|
|
}
|
|
|
|
SessionUser.functions.redirectTo.superUser("/selfserve/edge-agents", true);
|
|
};
|
|
|
|
const showCompleteOrderBookingConfirmation = async () => {
|
|
const result = await Swal.fire({
|
|
title: t("bookings.complete_booking"),
|
|
text: t("pos.confirm_complete_order"),
|
|
icon: "warning",
|
|
showDenyButton: true,
|
|
showCancelButton: true,
|
|
confirmButtonText: t("admin.pos.settings_wheel.view_booking_new_tab"),
|
|
denyButtonText: t("tables.bookings.complete_wash_without_certificate"),
|
|
cancelButtonText: t("common.cancel"),
|
|
reverseButtons: true,
|
|
});
|
|
|
|
if (result.isConfirmed) {
|
|
SessionUser.functions.redirectTo.department(
|
|
props.department_id,
|
|
"modules/bookings/order/" + props.order_booking_id,
|
|
true
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (result.isDenied) {
|
|
await SessionUser.objects.order_bookings.functions.complete(props.order_booking_id, null, () => {
|
|
props.refreshFunction();
|
|
});
|
|
}
|
|
};
|
|
|
|
const showEmailNotificationActionResult = async (requestAction, successKey, errorKey) => {
|
|
try {
|
|
await requestAction();
|
|
await Swal.fire({
|
|
title: t(successKey),
|
|
icon: "success",
|
|
showConfirmButton: false,
|
|
timer: 2000,
|
|
heightAuto: false,
|
|
});
|
|
} catch (error) {
|
|
console.error(error);
|
|
await Swal.fire({
|
|
title: t("common.error"),
|
|
text: [t(errorKey), SessionUser.functions.parseErrorMessage?.(error)].filter(Boolean).join(": "),
|
|
icon: "error",
|
|
heightAuto: false,
|
|
});
|
|
}
|
|
};
|
|
|
|
const resendBookingConfirmation = () =>
|
|
showEmailNotificationActionResult(
|
|
() => SessionUser.objects.order_bookings.functions.resendBookingConfirmation(props.order_booking_id),
|
|
"admin.pos.settings_wheel.resend_booking_confirmation_success",
|
|
"admin.pos.settings_wheel.resend_booking_confirmation_error"
|
|
);
|
|
|
|
const resendBookingCompletionConfirmation = () =>
|
|
showEmailNotificationActionResult(
|
|
() => SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation(props.order_booking_id),
|
|
"admin.pos.settings_wheel.resend_booking_completion_confirmation_success",
|
|
"admin.pos.settings_wheel.resend_booking_completion_confirmation_error"
|
|
);
|
|
|
|
const resendWashCertificate = () =>
|
|
showEmailNotificationActionResult(
|
|
() => SessionUser.objects.orders.functions.resendWashCertificate(props.order_id),
|
|
"admin.pos.settings_wheel.resend_wash_certificate_success",
|
|
"admin.pos.settings_wheel.resend_wash_certificate_error"
|
|
);
|
|
|
|
const getAvailableInvoiceCollectionsForCustomer = async (customerNumber) => {
|
|
const normalizedCustomerNumber = normalizePositiveInteger(customerNumber);
|
|
if (!normalizedCustomerNumber) {
|
|
return [];
|
|
}
|
|
|
|
const response = await SessionUser.request("/collected-invoices", "GET", {
|
|
page: 1,
|
|
limit: 100,
|
|
order: "closed_at:asc",
|
|
filters: `customer_number:${normalizedCustomerNumber},booked_invoice_id:is_null`,
|
|
});
|
|
|
|
return Array.isArray(response?.data?.data) ? response.data.data : [];
|
|
};
|
|
|
|
const ensureOpenInvoiceCollectionForCustomer = async (customerNumber) => {
|
|
const collections = await getAvailableInvoiceCollectionsForCustomer(customerNumber);
|
|
const openCollection = collections.find((collection) => collection?.closed_at === null);
|
|
const openCollectionId = normalizePositiveInteger(openCollection?.id);
|
|
if (openCollectionId) {
|
|
return openCollectionId;
|
|
}
|
|
|
|
const response = await SessionUser.objects.collectedOrderInvoices.add(
|
|
customerNumber,
|
|
t("admin.pos.drafts_assignment.new_collection_name"),
|
|
t("admin.pos.drafts_assignment.new_collection_description"),
|
|
null
|
|
);
|
|
|
|
return normalizePositiveInteger(response?.data?.data?.id ?? response?.data?.id);
|
|
};
|
|
|
|
const acceptSelfServeWashDraft = async () => {
|
|
const customerNumber = getSelfServeWashCustomerNumber.value;
|
|
const orderId = normalizePositiveInteger(props.order_id);
|
|
if (!customerNumber || !orderId) {
|
|
return;
|
|
}
|
|
|
|
const result = await Swal.fire({
|
|
title: t("admin.pos.settings_wheel.accept_self_serve_wash"),
|
|
text: t("admin.pos.settings_wheel.accept_self_serve_wash_confirm", { customerNumber }),
|
|
icon: "question",
|
|
showCancelButton: true,
|
|
confirmButtonText: t("common.confirm"),
|
|
cancelButtonText: t("common.cancel"),
|
|
});
|
|
|
|
if (!result.isConfirmed) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const invoiceCollectionId = await ensureOpenInvoiceCollectionForCustomer(customerNumber);
|
|
if (!invoiceCollectionId) {
|
|
throw new Error(t("admin.pos.drafts_assignment.invoice_collection_empty"));
|
|
}
|
|
|
|
await SessionUser.objects.orders.functions.assignDraftCustomer({
|
|
order_id: orderId,
|
|
customer_id: customerNumber,
|
|
invoice_collection_id: invoiceCollectionId,
|
|
department_id: normalizePositiveInteger(props.department_id),
|
|
recalculate_prices: true,
|
|
});
|
|
|
|
await props.refreshFunction();
|
|
await Swal.fire({
|
|
icon: "success",
|
|
title: t("admin.pos.settings_wheel.accept_self_serve_wash_success"),
|
|
timer: 1800,
|
|
showConfirmButton: false,
|
|
});
|
|
} catch (error) {
|
|
await Swal.fire({
|
|
icon: "error",
|
|
title: t("admin.pos.drafts_assignment.error"),
|
|
text: SessionUser.functions.parseErrorMessage(error) || t("admin.pos.drafts_assignment.error"),
|
|
});
|
|
}
|
|
};
|
|
|
|
const canDeleteOrderBooking = computed(() =>
|
|
!props.order_id &&
|
|
(props.allowBookingDeletion || SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser())
|
|
);
|
|
|
|
const flatBuiltInMenuSections = computed(() => {
|
|
const sections = [];
|
|
|
|
if (props.order_booking_id) {
|
|
const bookingSection = buildMenuSection("booking", t("admin.pos.settings_wheel.booking"), [
|
|
props.allowBookingCompletion
|
|
? buildMenuAction("booking-complete", {
|
|
icon: "fas fa-check",
|
|
label: t("admin.pos.settings_wheel.mark_as_completed"),
|
|
template: "success",
|
|
clickAction: showCompleteOrderBookingConfirmation,
|
|
})
|
|
: null,
|
|
buildMenuAction("booking-view", {
|
|
icon: "fas fa-external-link-alt",
|
|
label: t("admin.pos.settings_wheel.view_booking_new_tab"),
|
|
clickAction: () =>
|
|
SessionUser.functions.redirectTo.department(
|
|
props.department_id,
|
|
"modules/bookings/order/" + props.order_booking_id,
|
|
true
|
|
),
|
|
}),
|
|
SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("booking-change-association", {
|
|
icon: "fas fa-edit",
|
|
label: !props.order_id
|
|
? t("admin.pos.settings_wheel.associate_order", {
|
|
order: SessionUser.objects.orders.meta.labels.single.toLowerCase(),
|
|
})
|
|
: t("admin.pos.settings_wheel.change_order", {
|
|
order: SessionUser.objects.orders.meta.labels.single.toLowerCase(),
|
|
}),
|
|
clickAction: () =>
|
|
SessionUser.objects.order_bookings.showEditObjectFieldForm(
|
|
props.order_booking_id,
|
|
"order_id",
|
|
props.order_id,
|
|
() => {
|
|
props.refreshFunction();
|
|
},
|
|
{
|
|
filters: {
|
|
...(props.customer_number ? { customer_id: props.customer_number } : {}),
|
|
...(props.department_id ? { department_id: props.department_id } : {}),
|
|
},
|
|
pagination: {
|
|
page: 1,
|
|
limit: 100,
|
|
},
|
|
}
|
|
),
|
|
})
|
|
: null,
|
|
canDeleteOrderBooking.value
|
|
? buildMenuAction("booking-delete", {
|
|
icon: "fas fa-trash-alt",
|
|
label: t("admin.pos.settings_wheel.delete_booking"),
|
|
template: "danger",
|
|
clickAction: () =>
|
|
SessionUser.objects.order_bookings.functions.showDeleteConfirmationModal(props.order_booking_id, () => {
|
|
props.refreshFunction();
|
|
}),
|
|
})
|
|
: null,
|
|
]);
|
|
|
|
if (bookingSection) {
|
|
sections.push(bookingSection);
|
|
}
|
|
}
|
|
|
|
if (props.order_id) {
|
|
const orderSection = buildMenuSection(
|
|
"order",
|
|
SessionUser.functions.ucFirst(SessionUser.objects.orders.meta.labels.single),
|
|
[
|
|
buildMenuAction("order-view", {
|
|
icon: "fas fa-external-link-alt",
|
|
label: t("admin.pos.settings_wheel.view_order_new_tab"),
|
|
clickAction: () =>
|
|
SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()
|
|
? redirectDepartmentOrderPage(props.order_id, true)
|
|
: SessionUser.functions.redirectTo.user("/orders/" + props.order_id, true),
|
|
}),
|
|
SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()
|
|
? canAcceptSelfServeWashDraft.value
|
|
? buildMenuAction("order-accept-self-serve-wash", {
|
|
icon: "fas fa-check-circle",
|
|
label: t("admin.pos.settings_wheel.accept_self_serve_wash"),
|
|
template: "success",
|
|
clickAction: acceptSelfServeWashDraft,
|
|
})
|
|
: null
|
|
: null,
|
|
SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("order-attach-wash-certificate", {
|
|
icon: "fas fa-paperclip",
|
|
label: t("admin.pos.settings_wheel.attach_wash_certificate"),
|
|
clickAction: () =>
|
|
SessionUser.objects.orders.functions.showAttachWashCertificateForm(props.order_id, () => {
|
|
props.refreshFunction();
|
|
}),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("order-change-customer", {
|
|
icon: "fas fa-user-edit",
|
|
label: t("admin.pos.settings_wheel.change_customer"),
|
|
clickAction: () => SessionUser.objects.orders.functions.showChangeCustomerForm(props.order_id),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("order-change-invoice-collection", {
|
|
icon: "fas fa-file-invoice-dollar",
|
|
label: t("admin.pos.settings_wheel.change_invoice_collection"),
|
|
clickAction: () =>
|
|
SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm(
|
|
props.order_id,
|
|
props.refreshFunction
|
|
),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("order-delete", {
|
|
icon: "fas fa-trash-alt",
|
|
label: t("admin.pos.settings_wheel.delete_order"),
|
|
template: "danger",
|
|
clickAction: () =>
|
|
SessionUser.objects.orders.functions.showDeleteConfirmationModal(props.order_id, () => {
|
|
emitDeleted();
|
|
}),
|
|
})
|
|
: null,
|
|
]
|
|
);
|
|
|
|
if (orderSection) {
|
|
sections.push(orderSection);
|
|
}
|
|
}
|
|
|
|
if (props.order_booking_id || hasWashCertificateAttachment.value) {
|
|
const emailNotificationsSection = buildMenuSection(
|
|
"email-notifications",
|
|
t("admin.pos.settings_wheel.email_notifications_section"),
|
|
[
|
|
props.order_booking_id
|
|
? buildMenuAction("email-notifications-resend-booking-confirmation", {
|
|
icon: "fas fa-envelope",
|
|
label: t("admin.pos.settings_wheel.resend_booking_confirmation"),
|
|
clickAction: resendBookingConfirmation,
|
|
})
|
|
: null,
|
|
props.order_booking_id && hasWashCertificateAttachment.value
|
|
? buildMenuAction("email-notifications-resend-booking-completion-confirmation", {
|
|
icon: "fas fa-envelope-open-text",
|
|
label: t("admin.pos.settings_wheel.resend_booking_completion_confirmation"),
|
|
clickAction: resendBookingCompletionConfirmation,
|
|
})
|
|
: null,
|
|
!props.order_booking_id && hasWashCertificateAttachment.value
|
|
? buildMenuAction("email-notifications-resend-wash-certificate", {
|
|
icon: "fas fa-file-pdf",
|
|
label: t("admin.pos.settings_wheel.resend_wash_certificate"),
|
|
clickAction: resendWashCertificate,
|
|
})
|
|
: null,
|
|
]
|
|
);
|
|
|
|
if (emailNotificationsSection) {
|
|
sections.push(emailNotificationsSection);
|
|
}
|
|
}
|
|
|
|
if (props.invoice_collection_id && SessionUser.canAccessSuperUser()) {
|
|
const invoiceCollectionLinkSection = buildMenuSection(
|
|
"invoice-collection-link",
|
|
SessionUser.objects.collectedOrderInvoices.meta.title,
|
|
[
|
|
buildMenuAction("invoice-collection-view", {
|
|
icon: "fas fa-file-invoice-dollar",
|
|
label: t("admin.pos.settings_wheel.view_invoice_collection_new_tab"),
|
|
clickAction: () => redirectSuperUserInvoiceCollectionPage(props.invoice_collection_id),
|
|
}),
|
|
]
|
|
);
|
|
|
|
if (invoiceCollectionLinkSection) {
|
|
sections.push(invoiceCollectionLinkSection);
|
|
}
|
|
}
|
|
|
|
if (props.department_lane_id) {
|
|
const departmentLaneSection = buildMenuSection(
|
|
"department-lane",
|
|
SessionUser.objects.department_lanes.meta.labels.single,
|
|
[
|
|
(SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()) && props.department_id
|
|
? buildMenuAction("department-lane-view-admin", {
|
|
icon: "fas fa-external-link-alt",
|
|
label: t("admin.pos.settings_wheel.view_lane_new_tab"),
|
|
clickAction: () =>
|
|
SessionUser.functions.redirectTo.department(
|
|
props.department_id,
|
|
"modules/wash-lanes/" + props.department_lane_id,
|
|
!SessionUser.functions.device.isMobile()
|
|
),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-lane-view-superuser", {
|
|
icon: "fas fa-external-link-alt",
|
|
label: t("admin.pos.settings_wheel.view_lane_setup_new_tab"),
|
|
clickAction: () =>
|
|
SessionUser.functions.redirectTo.superUser("/department/lanes/" + props.department_lane_id, true),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-lane-force-enable", {
|
|
icon: "fas fa-play",
|
|
label: t("superuser.department_lane.force_enable_machine"),
|
|
template: "success",
|
|
clickAction: () => SessionUser.objects.department_lanes.functions.forceEnableMachine(props.department_lane_id),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-lane-force-disable", {
|
|
icon: "fas fa-stop",
|
|
label: t("superuser.department_lane.force_disable_machine"),
|
|
template: "danger",
|
|
clickAction: () => SessionUser.objects.department_lanes.functions.forceDisableMachine(props.department_lane_id),
|
|
})
|
|
: null,
|
|
]
|
|
);
|
|
|
|
if (departmentLaneSection) {
|
|
sections.push(departmentLaneSection);
|
|
}
|
|
}
|
|
|
|
if (props.department_lane_id && props.department_id) {
|
|
const selfServeStudioSection = buildMenuSection(
|
|
"department-self-serve-studio",
|
|
t("admin.pos.settings_wheel.self_serve_studio_section"),
|
|
[
|
|
(SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()) && props.department_id
|
|
? buildMenuAction("department-self-serve-studio-open", {
|
|
icon: "fas fa-external-link-alt",
|
|
label: t("admin.pos.settings_wheel.open_studio"),
|
|
clickAction: () =>
|
|
SessionUser.functions.redirectTo.department(
|
|
props.department_id,
|
|
"modules/self-serve/studio",
|
|
!SessionUser.functions.device.isMobile()
|
|
),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-self-serve-studio-open-legacy", {
|
|
icon: "fas fa-external-link-alt",
|
|
label: t("admin.pos.settings_wheel.open_legacy_self_serve"),
|
|
clickAction: () => SessionUser.functions.redirectTo.superUser("/selfserve", true),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-self-serve-studio-open-workspace", {
|
|
icon: "fas fa-microchip",
|
|
label: t("admin.pos.settings_wheel.open_hardware_workspace_lanes"),
|
|
clickAction: () => openDepartmentWorkspaceTab("lanes"),
|
|
})
|
|
: null,
|
|
]
|
|
);
|
|
|
|
if (selfServeStudioSection) {
|
|
sections.push(selfServeStudioSection);
|
|
}
|
|
|
|
const gatesSection = buildMenuSection("department-gates", t("admin.pos.settings_wheel.gates_section"), [
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-gates-open-tab", {
|
|
icon: "fas fa-door-open",
|
|
label: t("admin.pos.settings_wheel.open_gates_tab"),
|
|
clickAction: () => openDepartmentWorkspaceTab("gates"),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-gates-open-legacy", {
|
|
icon: "fas fa-external-link-alt",
|
|
label: t("admin.pos.settings_wheel.open_legacy_gates"),
|
|
clickAction: () => SessionUser.functions.redirectTo.superUser("/department/gates", true),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-gates-create", {
|
|
icon: "fas fa-plus",
|
|
label: t("admin.pos.settings_wheel.add_gate"),
|
|
clickAction: () =>
|
|
SessionUser.objects.department_gates.showCreateObjectForm(() => {
|
|
props.refreshFunction();
|
|
}, { department: props.department_id }),
|
|
})
|
|
: null,
|
|
]);
|
|
|
|
if (gatesSection) {
|
|
sections.push(gatesSection);
|
|
}
|
|
|
|
const relaysSection = buildMenuSection("department-relays", t("admin.pos.settings_wheel.relays_section"), [
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-relays-open-tab", {
|
|
icon: "fas fa-bolt",
|
|
label: t("admin.pos.settings_wheel.open_relays_tab"),
|
|
clickAction: () => openDepartmentWorkspaceTab("relays"),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-relays-open-legacy", {
|
|
icon: "fas fa-external-link-alt",
|
|
label: t("admin.pos.settings_wheel.open_legacy_relays"),
|
|
clickAction: () => SessionUser.functions.redirectTo.superUser("/department/relays", true),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-relays-create", {
|
|
icon: "fas fa-plus",
|
|
label: t("admin.pos.settings_wheel.add_relay"),
|
|
clickAction: () =>
|
|
SessionUser.objects.department_relays.showCreateObjectForm(() => {
|
|
props.refreshFunction();
|
|
}, { department: props.department_id }),
|
|
})
|
|
: null,
|
|
]);
|
|
|
|
if (relaysSection) {
|
|
sections.push(relaysSection);
|
|
}
|
|
|
|
const gatewaysSection = buildMenuSection(
|
|
"department-gateways",
|
|
t("admin.pos.settings_wheel.gateways_section"),
|
|
[
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-gateways-open-tab", {
|
|
icon: "fas fa-network-wired",
|
|
label: t("admin.pos.settings_wheel.open_gateways_tab"),
|
|
clickAction: () => openDepartmentWorkspaceTab("gateways"),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-gateways-open-fleet", {
|
|
icon: "fas fa-sitemap",
|
|
label: t("admin.pos.settings_wheel.open_fleet_landing"),
|
|
clickAction: () => SessionUser.functions.redirectTo.superUser("/selfserve/edge-agents", true),
|
|
})
|
|
: null,
|
|
SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("department-gateways-open-primary", {
|
|
icon: "fas fa-star",
|
|
label: t("admin.pos.settings_wheel.open_primary_gateway"),
|
|
clickAction: () => openPrimaryGatewayPage(),
|
|
})
|
|
: null,
|
|
]
|
|
);
|
|
|
|
if (gatewaysSection) {
|
|
sections.push(gatewaysSection);
|
|
}
|
|
}
|
|
|
|
if (hasUser.value && SessionUser.canAccessSuperUser()) {
|
|
const customerSectionItems = userIdFromCustomerNumber.value
|
|
? [
|
|
buildMenuAction("customer-view", {
|
|
icon: "fas fa-user-edit",
|
|
label: t("admin.pos.settings_wheel.view_customer_new_tab"),
|
|
clickAction: () =>
|
|
SessionUser.functions.redirectTo.superUser("/users/" + userIdFromCustomerNumber.value, true),
|
|
}),
|
|
buildMenuAction("customer-login", {
|
|
icon: "fas fa-user-shield",
|
|
label: t("admin.pos.settings_wheel.login_as_user"),
|
|
clickAction: () => SessionUser.superUser.intimidate.intimidateUser(userIdFromCustomerNumber.value),
|
|
}),
|
|
buildMenuAction("customer-login-qr", {
|
|
icon: "fas fa-user-shield",
|
|
label: t("admin.pos.settings_wheel.login_as_user_qr"),
|
|
clickAction: () => onClickShowImpersonationQRCode(userIdFromCustomerNumber.value),
|
|
}),
|
|
buildMenuAction("customer-change-password", {
|
|
icon: "fas fa-key",
|
|
label: t("admin.pos.settings_wheel.change_password"),
|
|
clickAction: () => showSetCustomerPassword(userIdFromCustomerNumber.value),
|
|
}),
|
|
buildMenuAction("customer-show", {
|
|
icon: "fas fa-user",
|
|
label: t("admin.pos.settings_wheel.show_customer"),
|
|
clickAction: () => {
|
|
customerModalVisible.value = true;
|
|
},
|
|
}),
|
|
]
|
|
: isResolvingUserId.value
|
|
? [
|
|
buildMenuAction("customer-loading", {
|
|
disabled: true,
|
|
icon: "fas fa-spinner",
|
|
label: t("global.loading"),
|
|
}),
|
|
]
|
|
: [];
|
|
const customerSection = buildMenuSection(
|
|
"customer",
|
|
t("admin.pos.settings_wheel.customer_section"),
|
|
customerSectionItems
|
|
);
|
|
|
|
if (customerSection) {
|
|
sections.push(customerSection);
|
|
}
|
|
|
|
if (canViewCustomerRules.value) {
|
|
const ruleSection = buildMenuSection(
|
|
"rules",
|
|
t("admin.pos.settings_wheel.rules_section"),
|
|
customerRuleAttributesLoading.value && !customerRuleAttributesLoaded.value
|
|
? [
|
|
buildMenuAction("customer-rules-loading", {
|
|
disabled: true,
|
|
icon: "fas fa-spinner",
|
|
label: t("global.loading"),
|
|
}),
|
|
]
|
|
: customerRuleItems.value.map((rule) =>
|
|
buildMenuToggleAction(rule.key, {
|
|
description: rule.description,
|
|
disabled: rule.disabled,
|
|
label: rule.label,
|
|
testId: rule.testId,
|
|
value: rule.value,
|
|
clickAction: () => toggleCustomerRuleAttribute(rule.attribute, !rule.value),
|
|
})
|
|
)
|
|
);
|
|
|
|
if (ruleSection) {
|
|
sections.push(ruleSection);
|
|
}
|
|
}
|
|
|
|
const shortcutsSection = buildMenuSection(
|
|
"shortcuts",
|
|
t("admin.pos.settings_wheel.shortcuts_section"),
|
|
userIdFromCustomerNumber.value
|
|
? customerShortcutItems.value
|
|
: isResolvingUserId.value
|
|
? [
|
|
buildMenuAction("customer-shortcuts-loading", {
|
|
disabled: true,
|
|
icon: "fas fa-spinner",
|
|
label: t("global.loading"),
|
|
}),
|
|
]
|
|
: []
|
|
);
|
|
|
|
if (shortcutsSection) {
|
|
sections.push(shortcutsSection);
|
|
}
|
|
}
|
|
|
|
if (props.reg_1 || props.reg_2 || props.reg_3) {
|
|
const vehicleSection = buildMenuSection(
|
|
"vehicle",
|
|
[props.reg_1, props.reg_2, props.reg_3].filter(Boolean).length > 1
|
|
? SessionUser.objects.vehicles.meta.labels.multiple
|
|
: SessionUser.objects.vehicles.meta.labels.single,
|
|
[
|
|
props.reg_1 && SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("vehicle-reg-1", {
|
|
icon: "fas fa-car",
|
|
label: t("admin.pos.settings_wheel.view_vehicle_new_tab", { reg: props.reg_1 }),
|
|
clickAction: () => SessionUser.functions.redirectTo.superUser("/vehicles/" + props.reg_1, true),
|
|
})
|
|
: null,
|
|
props.reg_2 && SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("vehicle-reg-2", {
|
|
icon: "fas fa-car",
|
|
label: t("admin.pos.settings_wheel.view_vehicle_new_tab", { reg: props.reg_2 }),
|
|
clickAction: () => SessionUser.functions.redirectTo.superUser("/vehicles/" + props.reg_2, true),
|
|
})
|
|
: null,
|
|
props.reg_3 && SessionUser.canAccessSuperUser()
|
|
? buildMenuAction("vehicle-reg-3", {
|
|
icon: "fas fa-car",
|
|
label: t("admin.pos.settings_wheel.view_vehicle_new_tab", { reg: props.reg_3 }),
|
|
clickAction: () => SessionUser.functions.redirectTo.superUser("/vehicles/" + props.reg_3, true),
|
|
})
|
|
: null,
|
|
]
|
|
);
|
|
|
|
if (vehicleSection) {
|
|
sections.push(vehicleSection);
|
|
}
|
|
}
|
|
|
|
if (attachmentsFromOrder.value.length > 0) {
|
|
const attachmentsSection = buildMenuSection("attachments", t("admin.pos.settings_wheel.attached_files"), [
|
|
...attachmentsFromOrder.value.map((attachment) =>
|
|
buildMenuAction(`attachment-${attachment.id}`, {
|
|
icon: "fas fa-paperclip",
|
|
label: getAttachmentLabel(attachment),
|
|
clickAction: () => downloadOrderAttachment(attachment),
|
|
})
|
|
),
|
|
]);
|
|
|
|
if (attachmentsSection) {
|
|
sections.push({
|
|
...attachmentsSection,
|
|
attachments: [...attachmentsFromOrder.value],
|
|
});
|
|
}
|
|
}
|
|
|
|
if (props.invoice_collection_id) {
|
|
const invoiceCollectionDownloadSection = buildMenuSection(
|
|
"invoice-collection-download",
|
|
SessionUser.objects.collectedOrderInvoices.meta.title,
|
|
[
|
|
buildMenuAction("invoice-collection-download-invoice", {
|
|
icon: "fas fa-download",
|
|
label: t("admin.pos.settings_wheel.download_invoice"),
|
|
clickAction: () => SessionUser.objects.collectedOrderInvoices.functions.download(props.invoice_collection_id),
|
|
}),
|
|
]
|
|
);
|
|
|
|
if (invoiceCollectionDownloadSection) {
|
|
sections.push(invoiceCollectionDownloadSection);
|
|
}
|
|
}
|
|
|
|
if (canAddInvoicePeriodFlag.value) {
|
|
const flagSection = buildMenuSection(
|
|
"invoice-period-flags",
|
|
t("admin.pos.settings_wheel.flags_section"),
|
|
defaultInvoicePeriodFlagTargets.value.map((item) =>
|
|
buildMenuAction(`add-invoice-period-flag-${item.key}`, {
|
|
icon: "fas fa-flag",
|
|
label: item.label,
|
|
template: "danger",
|
|
clickAction: () => showCreateInvoicePeriodFlagForm(item.target),
|
|
})
|
|
)
|
|
);
|
|
|
|
if (flagSection) {
|
|
sections.push(flagSection);
|
|
}
|
|
}
|
|
|
|
return sections;
|
|
});
|
|
|
|
const standaloneMenuActions = computed(() => {
|
|
const actions = [];
|
|
|
|
if (props.subuserGrant && SessionUser.canAccessUser()) {
|
|
actions.push(
|
|
buildMenuAction("subuser-grant-edit-permissions", {
|
|
icon: "fas fa-edit",
|
|
label: t("admin.pos.settings_wheel.edit_permissions"),
|
|
clickAction: () =>
|
|
SessionUser.objects.subuser_grants.functions.showPermissionEditForm(props.subuserGrant, () => {
|
|
props.refreshFunction();
|
|
}),
|
|
})
|
|
);
|
|
actions.push(
|
|
buildMenuAction("subuser-grant-delete-user", {
|
|
icon: "fas fa-trash",
|
|
label: t("admin.pos.settings_wheel.delete_user"),
|
|
clickAction: () =>
|
|
SessionUser.objects.subuser_grants.delete(props.subuserGrant.id, () => {
|
|
props.refreshFunction();
|
|
}),
|
|
})
|
|
);
|
|
}
|
|
|
|
return actions;
|
|
});
|
|
|
|
const directBuiltInMenuSections = computed(() => {
|
|
if (!props.displayActionsDirectly || !Array.isArray(props.directSectionKeys) || props.directSectionKeys.length === 0) {
|
|
return flatBuiltInMenuSections.value;
|
|
}
|
|
|
|
const allowedKeys = new Set(props.directSectionKeys.map((key) => String(key)));
|
|
return flatBuiltInMenuSections.value.filter((section) => allowedKeys.has(String(section.key)));
|
|
});
|
|
|
|
const desktopFlyoutMenuSections = computed(() => {
|
|
const mergedSections = [];
|
|
const mergedSectionsByLabel = new Map();
|
|
|
|
flatBuiltInMenuSections.value.forEach((section) => {
|
|
const labelKey = String(section.label);
|
|
const existingSection = mergedSectionsByLabel.get(labelKey);
|
|
|
|
if (existingSection) {
|
|
existingSection.items.push(...section.items);
|
|
return;
|
|
}
|
|
|
|
const mergedSection = {
|
|
...section,
|
|
items: [...section.items],
|
|
};
|
|
|
|
mergedSectionsByLabel.set(labelKey, mergedSection);
|
|
mergedSections.push(mergedSection);
|
|
});
|
|
|
|
return mergedSections;
|
|
});
|
|
|
|
watch(
|
|
[
|
|
activeDesktopFlyoutSectionKey,
|
|
activeAttachmentId,
|
|
isDesktopFlyoutLayout,
|
|
desktopFlyoutMenuSections,
|
|
activeAttachmentPreviewSource,
|
|
attachmentsFromOrder,
|
|
],
|
|
() => {
|
|
if (isDropdownOpen.value) {
|
|
void nextTick(() => {
|
|
void updateDropdownLayout();
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
const activeDesktopFlyoutSection = computed(() => {
|
|
return (
|
|
desktopFlyoutMenuSections.value.find((section) => section.key === activeDesktopFlyoutSectionKey.value) ??
|
|
desktopFlyoutMenuSections.value[0] ??
|
|
null
|
|
);
|
|
});
|
|
|
|
const hasDropdownContent = computed(
|
|
() => hasCustomActionsSlot.value || flatBuiltInMenuSections.value.length > 0 || standaloneMenuActions.value.length > 0
|
|
);
|
|
|
|
const getDesktopFlyoutEstimatedWidth = () => {
|
|
const rootFontSize = Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize || "16") || 16;
|
|
return (desktopFlyoutRootPanelWidthRem + desktopFlyoutSubmenuWidthRem) * rootFontSize + desktopFlyoutPanelGapPx;
|
|
};
|
|
|
|
const syncDesktopFlyoutState = (triggerRect) => {
|
|
if (typeof window === "undefined") {
|
|
isDesktopFlyoutLayout.value = false;
|
|
return;
|
|
}
|
|
|
|
const canUseHover =
|
|
typeof window.matchMedia === "function"
|
|
? window.matchMedia("(hover: hover) and (pointer: fine)").matches
|
|
: false;
|
|
const hasSections = desktopFlyoutMenuSections.value.length > 0;
|
|
const hasSpaceForFlyout =
|
|
!!triggerRect && triggerRect.right - getDesktopFlyoutEstimatedWidth() >= desktopFlyoutPanelGapPx;
|
|
const nextDesktopFlyoutState =
|
|
canUseHover && hasSections && window.innerWidth >= desktopFlyoutMinViewportWidth && hasSpaceForFlyout;
|
|
|
|
isDesktopFlyoutLayout.value = nextDesktopFlyoutState;
|
|
|
|
if (!nextDesktopFlyoutState) {
|
|
activeDesktopFlyoutSectionKey.value = null;
|
|
return;
|
|
}
|
|
|
|
const currentSectionStillExists = desktopFlyoutMenuSections.value.some(
|
|
(section) => section.key === activeDesktopFlyoutSectionKey.value
|
|
);
|
|
|
|
if (!currentSectionStillExists) {
|
|
activeDesktopFlyoutSectionKey.value = desktopFlyoutMenuSections.value[0]?.key ?? null;
|
|
}
|
|
};
|
|
|
|
const setActiveDesktopFlyoutSection = (sectionKey) => {
|
|
activeDesktopFlyoutSectionKey.value = sectionKey;
|
|
|
|
if (sectionKey !== "attachments") {
|
|
activeAttachmentId.value = null;
|
|
}
|
|
};
|
|
|
|
const isOverflowClippingStyle = (style) =>
|
|
["overflow", "overflowX", "overflowY"].some((property) => {
|
|
const value = style[property];
|
|
return value && value !== "visible";
|
|
});
|
|
|
|
const isDropdownClippedByAncestor = (dropdownRect, triggerRect) => {
|
|
let el = dropdownRoot.value?.parentElement;
|
|
|
|
while (el && el !== document.body) {
|
|
const style = window.getComputedStyle(el);
|
|
if (isOverflowClippingStyle(style)) {
|
|
const ancestorRect = el.getBoundingClientRect();
|
|
const wouldClipCurrentDropdown =
|
|
dropdownRect.top < ancestorRect.top ||
|
|
dropdownRect.right > ancestorRect.right ||
|
|
dropdownRect.bottom > ancestorRect.bottom ||
|
|
dropdownRect.left < ancestorRect.left;
|
|
const wouldClipBelow = triggerRect.bottom + dropdownRect.height > ancestorRect.bottom;
|
|
const wouldClipAbove = triggerRect.top - dropdownRect.height < ancestorRect.top;
|
|
|
|
if (wouldClipCurrentDropdown || (wouldClipBelow && wouldClipAbove)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
el = el.parentElement;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// Smart re-positioning of the flyout menu when either it's partially or fully hidden
|
|
const syncDesktopFlyoutPosition = () => {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
const dropdownContentEl = dropdownContent.value;
|
|
const triggerEl = dropdownRoot.value?.querySelector(".dropdown-trigger") ?? dropdownRoot.value;
|
|
if (!dropdownContentEl || !triggerEl) {
|
|
return;
|
|
}
|
|
|
|
const triggerRect = triggerEl.getBoundingClientRect();
|
|
const contentHeight = dropdownContentEl.scrollHeight;
|
|
const viewportWidth = window.innerWidth;
|
|
const contentRect = dropdownContentEl.getBoundingClientRect();
|
|
|
|
let needsFixed = isDropdownClippedByAncestor(contentRect, triggerRect);
|
|
if (isDesktopFlyoutLayout.value) {
|
|
let el = dropdownRoot.value.parentElement;
|
|
while (el && el !== document.body && el !== null) {
|
|
const style = window.getComputedStyle(el);
|
|
if (style.overflowY !== "visible") {
|
|
if (el.clientHeight < contentHeight + 10) {
|
|
needsFixed = true;
|
|
break;
|
|
}
|
|
}
|
|
el = el.parentElement;
|
|
}
|
|
}
|
|
|
|
if (isFixedPosition.value !== needsFixed) {
|
|
isFixedPosition.value = needsFixed;
|
|
}
|
|
|
|
if (needsFixed) {
|
|
if (dropdownContentEl.style.top !== "") {
|
|
dropdownContentEl.style.top = "";
|
|
}
|
|
|
|
const viewportInsets = getViewportInsets();
|
|
const viewportTop = viewportInsets.top;
|
|
const effectiveContentHeight = dropdownMaxHeight.value
|
|
? Math.min(contentHeight, dropdownMaxHeight.value)
|
|
: contentHeight;
|
|
const top =
|
|
dropdownPlacement.value === "top"
|
|
? Math.max(viewportTop, triggerRect.top - effectiveContentHeight)
|
|
: triggerRect.bottom;
|
|
|
|
const nextFixedStyles = {
|
|
top: `${top}px`,
|
|
right: `${viewportWidth - triggerRect.right}px`,
|
|
width: "max-content",
|
|
};
|
|
|
|
if (
|
|
fixedPositionStyles.value.top !== nextFixedStyles.top ||
|
|
fixedPositionStyles.value.right !== nextFixedStyles.right ||
|
|
fixedPositionStyles.value.width !== nextFixedStyles.width
|
|
) {
|
|
fixedPositionStyles.value = nextFixedStyles;
|
|
}
|
|
} else {
|
|
if (dropdownContentEl.style.top !== "") {
|
|
dropdownContentEl.style.top = "";
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<template v-if="displayActionsDirectly">
|
|
<!-- Error -->
|
|
<template v-if="attachmentsFromOrderError">
|
|
<div class="message is-danger">
|
|
<div class="message-body">
|
|
{{ attachmentsFromOrderError }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<slot v-if="hasCustomActionsSlot" name="actions"></slot>
|
|
<template v-for="section in directBuiltInMenuSections" :key="section.key">
|
|
<ActionSettingsWheelItemLabel :label="section.label" />
|
|
<template v-for="item in section.items" :key="item.key">
|
|
<ActionSettingsWheelToggleItem
|
|
v-if="isToggleMenuItem(item)"
|
|
:checked="item.value"
|
|
:click-action="item.clickAction"
|
|
:description="item.description"
|
|
:disabled="item.disabled"
|
|
:label="item.label"
|
|
:test-id="item.testId"
|
|
/>
|
|
<ActionSettingsWheelItem
|
|
v-else
|
|
:icon="item.icon"
|
|
:label="item.label"
|
|
:template="item.template"
|
|
:click-action="item.clickAction"
|
|
:disabled="item.disabled"
|
|
/>
|
|
</template>
|
|
</template>
|
|
<ActionSettingsWheelItem
|
|
v-for="item in standaloneMenuActions"
|
|
:key="item.key"
|
|
:icon="item.icon"
|
|
:label="item.label"
|
|
:template="item.template"
|
|
:click-action="item.clickAction"
|
|
:disabled="item.disabled"
|
|
/>
|
|
</template>
|
|
<div class="dropdown is-right" :class="dropdownClass" v-else ref="dropdownRoot">
|
|
<div class="dropdown-trigger">
|
|
<button
|
|
type="button"
|
|
:class="getTriggerButtonClass"
|
|
class="is-inverted"
|
|
aria-haspopup="true"
|
|
aria-controls="dropdown-menu"
|
|
:aria-expanded="isDropdownOpen ? 'true' : 'false'"
|
|
@click.stop="toggleDropdown"
|
|
>
|
|
<span class="icon">
|
|
<i :class="props.icon"></i>
|
|
</span>
|
|
<span v-if="props.label.length > 0" class="ml-2">{{ props.label }}</span>
|
|
</button>
|
|
</div>
|
|
<div class="dropdown-menu" id="dropdown-menu" role="menu" :style="dropdownMenuStyle">
|
|
<div
|
|
ref="dropdownContent"
|
|
class="dropdown-content"
|
|
:class="dropdownContentClass"
|
|
:style="dropdownContentStyle"
|
|
@dropdown-action-selected="onActionSelected"
|
|
>
|
|
<template v-if="hasDropdownContent">
|
|
<template v-if="isDesktopFlyoutLayout && desktopFlyoutMenuSections.length > 0">
|
|
<div class="action-settings-wheel-flyout" data-testid="action-settings-wheel-flyout">
|
|
<div class="action-settings-wheel-flyout__submenu-stack">
|
|
<div
|
|
v-for="section in desktopFlyoutMenuSections"
|
|
:key="section.key"
|
|
class="action-settings-wheel-flyout__submenu"
|
|
:class="{
|
|
'is-active': activeDesktopFlyoutSection?.key === section.key,
|
|
'action-settings-wheel-flyout__submenu--attachments': section.key === 'attachments',
|
|
'action-settings-wheel-flyout__submenu--rules': section.key === 'rules',
|
|
}"
|
|
:data-testid="`action-settings-wheel-submenu-${section.key}`"
|
|
>
|
|
<p class="action-settings-wheel-flyout__submenu-title">
|
|
{{ SessionUser.functions.ucFirst(section.label) }}
|
|
</p>
|
|
<div class="action-settings-wheel-flyout__submenu-items">
|
|
<template v-if="section.key === 'attachments'">
|
|
<button
|
|
v-for="attachment in section.attachments || []"
|
|
:key="`attachment-row-${attachment.id}`"
|
|
type="button"
|
|
class="action-settings-wheel-section-trigger action-settings-wheel-section-trigger--attachment"
|
|
:class="{ 'is-active': activeAttachmentId === attachment.id }"
|
|
:data-testid="`action-settings-wheel-attachment-row-${attachment.id}`"
|
|
:title="getAttachmentLabel(attachment)"
|
|
@mouseenter="setActiveAttachment(attachment)"
|
|
@focus="setActiveAttachment(attachment)"
|
|
@click.stop.prevent="setActiveAttachment(attachment)"
|
|
>
|
|
<span class="action-settings-wheel-section-trigger__arrow">
|
|
<i class="fas fa-chevron-left" aria-hidden="true"></i>
|
|
</span>
|
|
<span class="action-settings-wheel-section-trigger__icon">
|
|
<i class="fas fa-paperclip" aria-hidden="true"></i>
|
|
</span>
|
|
<span class="action-settings-wheel-section-trigger__label">
|
|
{{ getAttachmentLabel(attachment) }}
|
|
</span>
|
|
</button>
|
|
</template>
|
|
<template v-else>
|
|
<template v-for="item in section.items" :key="item.key">
|
|
<ActionSettingsWheelToggleItem
|
|
v-if="isToggleMenuItem(item)"
|
|
:checked="item.value"
|
|
:click-action="item.clickAction"
|
|
:description="item.description"
|
|
:disabled="item.disabled"
|
|
:label="item.label"
|
|
:test-id="item.testId"
|
|
/>
|
|
<ActionSettingsWheelItem
|
|
v-else
|
|
:icon="item.icon"
|
|
:label="item.label"
|
|
:template="item.template"
|
|
:click-action="item.clickAction"
|
|
:disabled="item.disabled"
|
|
/>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
<div
|
|
v-if="section.key === 'attachments' && activeDesktopFlyoutSection?.key === 'attachments' && activeAttachment"
|
|
class="action-settings-wheel-attachment-panel"
|
|
data-testid="action-settings-wheel-attachment-panel"
|
|
>
|
|
<p class="action-settings-wheel-attachment-panel__title" :title="getAttachmentLabel(activeAttachment)">
|
|
{{ getAttachmentLabel(activeAttachment) }}
|
|
</p>
|
|
<div class="action-settings-wheel-attachment-panel__preview" data-testid="action-settings-wheel-attachment-preview">
|
|
<span
|
|
v-if="previewLoadingAttachmentId === activeAttachment.id"
|
|
class="action-settings-wheel-attachment-panel__text"
|
|
>
|
|
{{ t("global.loading") }}
|
|
</span>
|
|
<img
|
|
v-else-if="activeAttachmentPreviewKind === 'image' && activeAttachmentPreviewSource"
|
|
:src="activeAttachmentPreviewSource"
|
|
alt=""
|
|
class="action-settings-wheel-attachment-panel__image"
|
|
/>
|
|
<iframe
|
|
v-else-if="activeAttachmentPreviewKind === 'document' && activeAttachmentPreviewSource"
|
|
:src="activeAttachmentPreviewSource"
|
|
title="Attachment preview"
|
|
class="action-settings-wheel-attachment-panel__document"
|
|
></iframe>
|
|
<a
|
|
v-else-if="activeAttachmentPreviewKind === 'link'"
|
|
:href="activeAttachment.content?.other"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="action-settings-wheel-attachment-panel__text action-settings-wheel-attachment-panel__link"
|
|
>
|
|
{{ activeAttachment.content?.other }}
|
|
</a>
|
|
<div
|
|
v-else-if="['image', 'document'].includes(activeAttachmentPreviewKind)"
|
|
class="action-settings-wheel-attachment-panel__placeholder"
|
|
>
|
|
<span class="action-settings-wheel-attachment-panel__placeholder-icon">
|
|
<i :class="getAttachmentPreviewPlaceholderIcon(activeAttachment)" aria-hidden="true"></i>
|
|
</span>
|
|
<span class="action-settings-wheel-attachment-panel__placeholder-label">
|
|
{{ getAttachmentPreviewPlaceholderLabel(activeAttachment) }}
|
|
</span>
|
|
</div>
|
|
<span v-else-if="activeAttachmentPreviewKind === 'office'" class="action-settings-wheel-attachment-panel__text">
|
|
{{ t("admin.pos.attachments_office_preview_unavailable") }}
|
|
</span>
|
|
<span v-else-if="activeAttachmentPreviewKind === 'text'" class="action-settings-wheel-attachment-panel__text">
|
|
{{ formatAttachmentText(activeAttachment) }}
|
|
</span>
|
|
<span v-else class="action-settings-wheel-attachment-panel__text">
|
|
{{ t("admin.pos.attachments_no_preview") }}
|
|
</span>
|
|
</div>
|
|
<div class="action-settings-wheel-attachment-panel__actions">
|
|
<button
|
|
type="button"
|
|
class="action-settings-wheel-attachment-action"
|
|
:data-testid="`action-settings-wheel-attachment-action-preview-${activeAttachment.id}`"
|
|
@click.stop.prevent="previewAttachment(activeAttachment)"
|
|
>
|
|
<span class="action-settings-wheel-attachment-action__icon">
|
|
<i class="fas fa-eye" aria-hidden="true"></i>
|
|
</span>
|
|
<span>{{ t("global.preview") }}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="action-settings-wheel-attachment-action"
|
|
:data-testid="`action-settings-wheel-attachment-action-download-${activeAttachment.id}`"
|
|
@click.stop.prevent="downloadAttachment(activeAttachment)"
|
|
>
|
|
<span class="action-settings-wheel-attachment-action__icon">
|
|
<i class="fas fa-download" aria-hidden="true"></i>
|
|
</span>
|
|
<span>{{ t("common.download") }}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="action-settings-wheel-attachment-action"
|
|
:data-testid="`action-settings-wheel-attachment-action-print-${activeAttachment.id}`"
|
|
@click.stop.prevent="printAttachment(activeAttachment)"
|
|
>
|
|
<span class="action-settings-wheel-attachment-action__icon">
|
|
<i class="fas fa-print" aria-hidden="true"></i>
|
|
</span>
|
|
<span>{{ t("global.print") }}</span>
|
|
</button>
|
|
<button
|
|
v-if="SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()"
|
|
type="button"
|
|
class="action-settings-wheel-attachment-action action-settings-wheel-attachment-action--danger"
|
|
:disabled="deletingAttachmentId === activeAttachment.id"
|
|
:data-testid="`action-settings-wheel-attachment-action-delete-${activeAttachment.id}`"
|
|
@click.stop.prevent="removeAttachment(activeAttachment)"
|
|
>
|
|
<span class="action-settings-wheel-attachment-action__icon">
|
|
<i class="fas fa-trash-alt" aria-hidden="true"></i>
|
|
</span>
|
|
<span>{{ t("global.delete") }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="action-settings-wheel-flyout__sections" data-testid="action-settings-wheel-sections">
|
|
<slot v-if="hasCustomActionsSlot" name="actions"></slot>
|
|
<button
|
|
v-for="section in desktopFlyoutMenuSections"
|
|
:key="section.key"
|
|
type="button"
|
|
class="action-settings-wheel-section-trigger"
|
|
:class="{ 'is-active': activeDesktopFlyoutSection?.key === section.key }"
|
|
:data-testid="`action-settings-wheel-section-${section.key}`"
|
|
@mouseenter="setActiveDesktopFlyoutSection(section.key)"
|
|
@focus="setActiveDesktopFlyoutSection(section.key)"
|
|
@click.stop.prevent="setActiveDesktopFlyoutSection(section.key)"
|
|
>
|
|
<span class="action-settings-wheel-section-trigger__arrow">
|
|
<i class="fas fa-chevron-left" aria-hidden="true"></i>
|
|
</span>
|
|
<span class="action-settings-wheel-section-trigger__label">
|
|
{{ SessionUser.functions.ucFirst(section.label) }}
|
|
</span>
|
|
</button>
|
|
<div v-if="standaloneMenuActions.length > 0" class="action-settings-wheel-flyout__standalone-actions">
|
|
<ActionSettingsWheelItem
|
|
v-for="item in standaloneMenuActions"
|
|
:key="item.key"
|
|
:icon="item.icon"
|
|
:label="item.label"
|
|
:template="item.template"
|
|
:click-action="item.clickAction"
|
|
:disabled="item.disabled"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div data-testid="action-settings-wheel-sections">
|
|
<slot v-if="hasCustomActionsSlot" name="actions"></slot>
|
|
<template v-for="section in flatBuiltInMenuSections" :key="section.key">
|
|
<ActionSettingsWheelItemLabel :label="section.label" :data-testid="`action-settings-wheel-section-${section.key}`" />
|
|
<template v-for="item in section.items" :key="item.key">
|
|
<ActionSettingsWheelToggleItem
|
|
v-if="isToggleMenuItem(item)"
|
|
:checked="item.value"
|
|
:click-action="item.clickAction"
|
|
:description="item.description"
|
|
:disabled="item.disabled"
|
|
:label="item.label"
|
|
:test-id="item.testId"
|
|
/>
|
|
<ActionSettingsWheelItem
|
|
v-else
|
|
:icon="item.icon"
|
|
:label="item.label"
|
|
:template="item.template"
|
|
:click-action="item.clickAction"
|
|
:disabled="item.disabled"
|
|
/>
|
|
</template>
|
|
</template>
|
|
<ActionSettingsWheelItem
|
|
v-for="item in standaloneMenuActions"
|
|
:key="item.key"
|
|
:icon="item.icon"
|
|
:label="item.label"
|
|
:template="item.template"
|
|
:click-action="item.clickAction"
|
|
:disabled="item.disabled"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<template v-else>
|
|
<div class="dropdown-item">
|
|
<p class="has-text-centered">{{ t("admin.pos.settings_wheel.no_actions_defined") }}</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<CustomerModal
|
|
v-if="customerModalVisible"
|
|
:user_id="userIdFromCustomerNumber"
|
|
@closeModal="customerModalVisible = false"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.action-settings-wheel-trigger {
|
|
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
|
|
}
|
|
|
|
.action-settings-wheel-trigger--active:not(.action-settings-wheel-trigger--text) {
|
|
border-color: #25344d;
|
|
box-shadow: 0 0 0 3px rgba(37, 52, 77, 0.28);
|
|
}
|
|
|
|
.action-settings-wheel-trigger--text {
|
|
background: transparent;
|
|
border: 0;
|
|
box-shadow: none;
|
|
color: #25344d;
|
|
padding-inline: 0.35rem;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.action-settings-wheel-trigger--text:hover,
|
|
.action-settings-wheel-trigger--text:focus,
|
|
.action-settings-wheel-trigger--text[aria-expanded="true"] {
|
|
background: rgba(37, 52, 77, 0.1);
|
|
color: #132339;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.action-settings-wheel-trigger--text .icon,
|
|
.action-settings-wheel-trigger--text .icon:hover,
|
|
.action-settings-wheel-trigger--text .icon:focus {
|
|
text-decoration: none;
|
|
}
|
|
|
|
.action-settings-wheel-trigger--icon-only {
|
|
min-width: auto;
|
|
}
|
|
|
|
.dropdown {
|
|
position: relative;
|
|
}
|
|
|
|
.dropdown-menu {
|
|
min-width: 15rem;
|
|
z-index: 4001;
|
|
}
|
|
|
|
.dropdown-content {
|
|
position: relative;
|
|
background: #ffffff;
|
|
border: 1px solid #cfd8e3;
|
|
border-radius: 0.8rem;
|
|
box-shadow: 0 18px 36px rgba(19, 35, 57, 0.14);
|
|
padding: 0.35rem;
|
|
z-index: 4002;
|
|
}
|
|
|
|
.action-settings-wheel-dropdown-content--desktop-flyout {
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.action-settings-wheel-flyout {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: flex-start;
|
|
gap: 0.75rem;
|
|
min-width: 33rem;
|
|
z-index: 4003;
|
|
}
|
|
/* When the flyout is partly out of it's parent element, it can get cut off by overflow:hidden, so we need to allow it to overflow, but attempt to keep it visible within the viewport with smart positioning logic in the component */
|
|
.action-settings-wheel-flyout {
|
|
overflow: visible;
|
|
}
|
|
|
|
|
|
.action-settings-wheel-flyout__submenu-stack {
|
|
display: grid;
|
|
min-width: 17rem;
|
|
}
|
|
|
|
.action-settings-wheel-flyout__sections,
|
|
.action-settings-wheel-flyout__submenu {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 15rem;
|
|
}
|
|
|
|
.action-settings-wheel-flyout__submenu {
|
|
grid-area: 1 / 1;
|
|
position: relative;
|
|
background: linear-gradient(180deg, #f9fbfd 0%, #f3f7fb 100%);
|
|
border: 1px solid #d9e4ef;
|
|
border-radius: 0.8rem;
|
|
padding: 0.35rem;
|
|
visibility: hidden;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.12s ease;
|
|
}
|
|
|
|
.action-settings-wheel-flyout__submenu.is-active {
|
|
visibility: visible;
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.action-settings-wheel-flyout__submenu-title {
|
|
margin: 0;
|
|
padding: 0.55rem 0.75rem 0.35rem;
|
|
color: #6a7890;
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.05em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.action-settings-wheel-flyout__submenu-items,
|
|
.action-settings-wheel-flyout__standalone-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.15rem;
|
|
}
|
|
|
|
.action-settings-wheel-flyout__submenu--attachments {
|
|
overflow: visible;
|
|
}
|
|
|
|
.action-settings-wheel-flyout__submenu--rules {
|
|
min-width: 18.75rem;
|
|
}
|
|
|
|
.action-settings-wheel-flyout__standalone-actions {
|
|
margin-top: 0.35rem;
|
|
padding-top: 0.35rem;
|
|
border-top: 1px solid #e3eaf2;
|
|
}
|
|
|
|
.action-settings-wheel-section-trigger {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.65rem;
|
|
padding: 0.8rem 0.9rem;
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
border-radius: 0.7rem;
|
|
color: #25344d;
|
|
cursor: pointer;
|
|
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
}
|
|
|
|
.action-settings-wheel-section-trigger:hover,
|
|
.action-settings-wheel-section-trigger:focus,
|
|
.action-settings-wheel-section-trigger.is-active {
|
|
background: #f2f6fb;
|
|
border-color: #d8e2ed;
|
|
color: #132339;
|
|
outline: none;
|
|
}
|
|
|
|
.action-settings-wheel-section-trigger__arrow {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 0.9rem;
|
|
color: #708097;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.action-settings-wheel-section-trigger__icon {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 0.95rem;
|
|
color: #708097;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.action-settings-wheel-section-trigger--attachment {
|
|
padding-right: 1rem;
|
|
}
|
|
|
|
.action-settings-wheel-section-trigger__label {
|
|
flex: 1;
|
|
min-width: 0;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-panel {
|
|
position: absolute;
|
|
top: 0;
|
|
right: calc(100% + 0.75rem);
|
|
width: min(20rem, 48vw);
|
|
min-height: 20rem;
|
|
background: #ffffff;
|
|
border: 1px solid #cfd8e3;
|
|
border-radius: 0.9rem;
|
|
box-shadow: 0 18px 36px rgba(19, 35, 57, 0.14);
|
|
padding: 0.75rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
z-index: 2;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-panel__title {
|
|
margin: 0;
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
color: #132339;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-panel__preview {
|
|
min-height: 12.5rem;
|
|
border: 1px solid #e7edf5;
|
|
border-radius: 0.75rem;
|
|
background: linear-gradient(180deg, #fbfcfe 0%, #f3f6fa 100%);
|
|
overflow: hidden;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-panel__image,
|
|
.action-settings-wheel-attachment-panel__document {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: 0;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-panel__image {
|
|
object-fit: contain;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-panel__text {
|
|
padding: 1rem;
|
|
text-align: center;
|
|
color: #4a5568;
|
|
overflow-wrap: anywhere;
|
|
white-space: pre-line;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-panel__link {
|
|
text-decoration: none;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-panel__placeholder {
|
|
width: 100%;
|
|
padding: 1.5rem 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.75rem;
|
|
color: #5c6980;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-panel__placeholder-icon {
|
|
width: 3rem;
|
|
height: 3rem;
|
|
border-radius: 999px;
|
|
background: #eaf1f8;
|
|
color: #42526b;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-panel__placeholder-label {
|
|
padding: 0.2rem 0.55rem;
|
|
border-radius: 999px;
|
|
background: #eef3f8;
|
|
color: #42526b;
|
|
font-size: 0.74rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-panel__actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.3rem;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-action {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.65rem;
|
|
padding: 0.72rem 0.85rem;
|
|
background: #f8fafc;
|
|
border: 1px solid #dde6f0;
|
|
border-radius: 0.7rem;
|
|
color: #25344d;
|
|
cursor: pointer;
|
|
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-action:hover,
|
|
.action-settings-wheel-attachment-action:focus {
|
|
background: #f2f6fb;
|
|
border-color: #d2deea;
|
|
color: #132339;
|
|
outline: none;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-action:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-action--danger {
|
|
color: #8d2637;
|
|
border-color: #f0d4db;
|
|
background: #fff7f8;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-action--danger:hover,
|
|
.action-settings-wheel-attachment-action--danger:focus {
|
|
background: #fff0f2;
|
|
border-color: #e7bcc7;
|
|
color: #6f1728;
|
|
}
|
|
|
|
.action-settings-wheel-attachment-action__icon {
|
|
width: 0.95rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.dropdown-content :deep(.dropdown-item-action),
|
|
.dropdown-content :deep(.dropdown-item-label) {
|
|
border-radius: 0.55rem;
|
|
}
|
|
|
|
@media (hover: none) {
|
|
.action-settings-wheel-attachment-panel {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|