Add additional items handling and customizable product display in POS Mobile flow

- Introduced `PosDepartmentStepMobile2AdditionalItems.vue` for managing additional items in transactions.
- Added `additionalItemSelection` view to `PosDepartmentStepMobileFlow.vue` for enhanced item selection.
- Enhanced `PosDepartmentStepMobile2Products.vue` with support for displaying products as addons.
- Implemented reusable addon conversion logic for managing products as additional items.
- Updated `WhiteBoxCard.vue` to emit toggle events for improved interaction.
- Refactored button logic in `PosDepartmentStepMobileButtonNextStep.vue` to handle custom actions and disable states.
- Adjusted prop handling and event logic in multiple components for seamless addon integration.
This commit is contained in:
Jeppe Bundgaard
2025-09-23 10:50:59 +02:00
parent d4ba7937af
commit 4ef4f927bc
8 changed files with 396 additions and 18 deletions
+17 -3
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { defineProps } from 'vue';
import { defineProps, defineEmits, onMounted, ref } from 'vue';
import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
const props = defineProps({
@@ -20,7 +21,13 @@ const props = defineProps({
default: false
}
});
import { onMounted, ref } from 'vue';
/**
* Emits:
* - toggle (boolean): Emitted when the card is toggled open or closed.
*/
const emit = defineEmits<{
(e: 'toggle', isOpen: boolean): void;
}>();
const isOpen = ref(props.defaultOpen);
onMounted(() => {
@@ -28,6 +35,13 @@ onMounted(() => {
isOpen.value = props.defaultOpen;
}
});
/** Function to emit toggle event when isOpen changes */
const toggle = () => {
if (!props.toggleable) return;
isOpen.value = !isOpen.value;
emit('toggle', isOpen.value);
};
</script>
<template>
@@ -42,7 +56,7 @@ onMounted(() => {
</div>
</div>
<!-- Default slot for header -->
<div class="card-header" @click="props.toggleable ? isOpen = !isOpen : null" :class="{ 'is-clickable': props.toggleable }" v-if="$slots.header && !props.hideHeader">
<div class="card-header" @click="toggle" :class="{ 'is-clickable': props.toggleable }" v-if="$slots.header && !props.hideHeader">
<slot name="header"></slot>
</div>
<!-- Default slot for any content -->
@@ -13,7 +13,7 @@ import ControlFieldInputLabel from "@/components/viewport/elements/controls/fiel
import ControlField from "@/components/viewport/elements/controls/fields/ControlField.vue";
import PosDepartmentStepMobile2RegistrationNumbers
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2RegistrationNumbers.vue";
import { vehicleSelection, transactionItems, notes, reference, vehicles, lastOrders, metadata } from "./objects/PosDepartmentStepMobileFlow.vue";
import { vehicleSelection, transactionItems, notes, reference, vehicles, lastOrders, metadata, additionalItemSelection } from "./objects/PosDepartmentStepMobileFlow.vue";
import PosDepartmentStep2MobileVehicleSelection
from "@/components/displays/department/pos/steps/mobile/views/PosDepartmentStep2MobileVehicleSelection.vue";
import PosDepartmentStepMobileButtonNextStep
@@ -27,6 +27,8 @@ import PosDepartmentStepMobileButtonClearAll
import PosDepartmentStepMobileFixedBottomControl
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileFixedBottomControl.vue";
import {PosOrderItem} from "@/components/displays/department/pos/steps/mobile/objects/PosOrderItem.vue";
import PosDepartmentStepMobile2AdditionalItems
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2AdditionalItems.vue";
onMounted(() => {
// Set the header to be transparent
@@ -313,6 +315,9 @@ watch(() => vehicles.vehicle_1.value.reference, (newValue, oldValue) => {
<template v-if="vehicleSelection">
<PosDepartmentStep2MobileVehicleSelection @close="vehicleSelection = false"/>
</template>
<template v-else-if="additionalItemSelection">
<PosDepartmentStepMobile2AdditionalItems/>
</template>
<template v-else>
<div class="is-flex is-flex-direction-column is-justify-content-space-between is-gap-2">
<!-- Registration numbers -->
@@ -325,6 +330,8 @@ watch(() => vehicles.vehicle_1.value.reference, (newValue, oldValue) => {
:classes="layout.classes"
:product="transactionItems.primaryItem.value"
/>
<!-- Additional items -->
<PosDepartmentStepMobile2AdditionalItems/>
<!-- Last order details -->
<PosDepartmentStepMobile2LastOrder
v-show="lastOrders.get(1)"
@@ -0,0 +1,201 @@
<script setup lang="ts">
import { defineProps, ref, computed, watch } from "vue";
import { PosOrder } from "../objects/PosOrder.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
import { pos } from "../objects/PosDepartmentStepMobileFlow.vue";
import {PosProduct} from "@/components/displays/department/pos/steps/mobile/objects/PosProduct.vue";
import PosDepartmentStepMobile2CategoryProduct
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2CategoryProduct.vue";
import PosDepartmentStepMobile2Addons
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Addons.vue";
import {Addon} from "@/components/displays/department/pos/steps/mobile/objects/PosAddon.vue";
import PosDepartmentStepMobileFixedBottomControl
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileFixedBottomControl.vue";
import PosDepartmentStepMobileButtonNextStep
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileButtonNextStep.vue";
import {
transactionItems
} from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
import PosDepartmentStep2MobileVehicleSelection
from "@/components/displays/department/pos/steps/mobile/views/PosDepartmentStep2MobileVehicleSelection.vue";
const props = defineProps({
label: {
type: String,
default: "Additional items",
required: true
},
subtitle: {
type: String,
default: "Click to modify your additional items",
required: true
},
defaultChecked: {
type: Boolean,
default: false
},
lastOrder: {
type: Object as () => PosOrder | null,
default: null,
required: false
},
});
const checked = ref(props.defaultChecked);
// Function to generate a summary from the last order
function generateSummary(order: PosOrder): string {
if (!order || !order.items || order.items.length === 0) {
return "Denne ordre har ingen varer.";
}
const itemNames = order.items.map(item => item?.product?.name || "Ukendt vare");
const uniqueItems = Array.from(new Set(itemNames));
return uniqueItems.length > 1
? `${uniqueItems.length} varer: ${uniqueItems.slice(0, 2).join(", ")}${uniqueItems.length > 2 ? " og flere" : ""}`
: uniqueItems[0];
}
// Functions to determine the display values
const displayLabel = computed(() => {
return props.lastOrder
? `Vasket d. ${SessionUser.functions.date.toLocal(new Date(props?.lastOrder?.created_at))}`
: props.label;
});
const displaySubtitle = computed(() => {
return props.lastOrder
? generateSummary(props.lastOrder)
: props.subtitle;
});
// Emit event on toggle
function onToggle(isOpen: boolean) {
checked.value = isOpen;
}
/** Additional item management logic */
const exampleProducts = ref<PosProduct[]>([
{
id: 1,
name: "Extra Towel",
price: 5.00,
description: "A soft extra towel",
subscription_allowed: false,
},
{
id: 2,
name: "Premium Soap",
price: 3.50,
description: "A premium quality soap",
subscription_allowed: true,
}
]);
const convertProductToAddon = (product: PosProduct, options: {quantity?: number, min?: number, max?: number} = {}): Addon => {
console.warn("Converting product to addon:", product, options);
return {
id: product.id,
name: product.name,
price: product.price,
product: product,
quantity: options.quantity || 0,
min: options.min || -1,
max: options.max || -1,
}
}
const getAvailableAdditionalItems = () => {
const tmp = transactionItems.additionalItems.value || [];
if (!tmp || tmp.length === 0) return [];
return <Addon[]>tmp.map(p => convertProductToAddon(p, {
quantity: p?.quantity || 0,
min: -1,
max: -1,
}));
}
const availableAdditionalItems = ref<Addon[]>(getAvailableAdditionalItems());
const onClickAddOtherProduct = () => {
pos.views.additionalItemSelection.value = !pos.views.additionalItemSelection.value;
}
// Watch for changes in available additional items, to update the pos.transactionItems.additionalItems
watch(availableAdditionalItems, (newVal) => {
newVal.forEach(addon => {
if (addon.quantity && addon.quantity > 0 && addon.product) {
addon.product.quantity = addon.quantity; // Ensure product has correct quantity
}
pos.transactionItems.setAdditionalItems(newVal.filter(a => a.quantity && a.quantity > 0).map(a => a.product!).filter((p): p is PosProduct => !!p) );
});
}, { deep: true });
// Watch for changes in pos.transactionItems.additionalItems to remove items with quantity 0
watch(() => pos.transactionItems.additionalItems.value, (newVal) => {
if (!newVal) return;
if (newVal.length === 0) {
availableAdditionalItems.value = [];
return;
}
// Prevent recursive loop by checking if availableAdditionalItems already matches newVal
const currentProductIds = availableAdditionalItems.value.map(a => a.id);
const newProductIds = newVal.map(p => p.id);
const isSame = currentProductIds.length === newProductIds.length && currentProductIds.every(id => newProductIds.includes(id));
if (isSame) return;
availableAdditionalItems.value = getAvailableAdditionalItems();
}, { deep: true });
</script>
<template>
<div>
<!-- Minimal view, when not set as fullscreen view -->
<WhiteBoxCard :toggleable="true" @toggle="onToggle" v-if="!pos.views.additionalItemSelection.value">
<!-- Header -->
<template #header>
<div class="card-header-title">{{ displayLabel }}</div>
<div class="card-header-icon">
<!-- Toggle Switch -->
<label class="switch">
<input type="checkbox" v-model="checked" />
<span class="slider round"></span>
</label>
</div>
</template>
<!-- Content -->
<template #content>
<!-- Suggested items -->
<div>
<PosDepartmentStepMobile2Addons :label="''" :addons="availableAdditionalItems" />
</div>
</template>
<!-- Footer -->
<template #footer>
<!-- Select other product button -->
<a class="card-footer-item" @click="onClickAddOtherProduct">Vælg andre produkter</a>
</template>
</WhiteBoxCard>
<!-- Fullscreen view, when selecting other products -->
<template v-else>
<!-- Shortcuts / recommendations -->
<WhiteBoxCard :toggleable="true">
<!-- Header -->
<template #header>
<div class="card-header-title">{{ SessionUser.objects.global.language.recommended }}</div>
<div class="card-header-icon">
<span class="icon">
<!-- Arrow right icon -->
<i class="fas fa-arrow-right"></i>
</span>
</div>
</template>
</WhiteBoxCard>
<!-- Categories of products -->
<PosDepartmentStep2MobileVehicleSelection :onAddProduct="pos.transactionItems.addAdditionalItem" :onSearchClick="() => console.warn('AdditionalItem Search Clicked')"/><!-- :asAddons="true" :addons="availableAdditionalItems" @update:addons="availableAdditionalItems = $event"/>-->
<!--{{ availableAdditionalItems }}-->
<!-- Products in category -->
<!-- Back button -->
<!-- Buttons -->
<PosDepartmentStepMobileFixedBottomControl>
<!-- Complete button -->
<PosDepartmentStepMobileButtonNextStep :customAction="onClickAddOtherProduct"/>
</PosDepartmentStepMobileFixedBottomControl>
</template>
</div>
</template>
<style scoped>
</style>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { watch, defineEmits } from "vue";
import {watch, defineEmits, defineProps, ref} from "vue";
import { pos } from "../objects/PosDepartmentStepMobileFlow.vue";
import PosDepartmentStepMobile2CategoryProduct
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2CategoryProduct.vue";
@@ -7,8 +7,25 @@ import {PosCategory} from "@/components/displays/department/pos/steps/mobile/obj
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {PosProduct} from "@/components/displays/department/pos/steps/mobile/objects/PosProduct.vue";
import { department_id, customer_id } from "@/components/shop/POSDepartmentProcess.vue";
import PosDepartmentStepMobile2Addons
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Addons.vue";
import {Addon} from "@/components/displays/department/pos/steps/mobile/objects/PosAddon.vue";
const emits = defineEmits(["addProduct"]);
const props = defineProps({
// To be able to count the products, providing the "addonList" object, paired with the "asAddons" boolean
// This should be the list of addons, not the complete product list
addons: {
type: Object as () => Addon[],
required: false,
},
// Display products as addons (To be able to modify the amount of the products)
asAddons: {
type: Boolean,
required: false,
default: false,
},
});
// Function to fetch categories based on the department ID
// This function will be called when the component is mounted
@@ -54,17 +71,51 @@ watch(() => pos.categories.selection(), (newCategory) => {
}
}
});
/** As Addons **/
const mergedAddons = ref<Addon[]>([]);
// Function to merge the count of addons with the products
const mergeCountAddons = (products: PosProduct[], addons: Addon[] | undefined) => {
const tmpAddons = <Addon[]>[]; // Temporary array to hold the merged addons
// Convert all products to addons
for (let product of products) {
let tmpAddon = pos.transactionItems.convertProductToAddon(product); // Convert product to addon (count = 0)
tmpAddons.push(tmpAddon);
}
// Loop through the addons and update the count if the addon exists in the tmpAddons array
if (addons) {
for (let addon of addons) {
let index = tmpAddons.findIndex(a => a.product.id === addon.product.id);
if (index !== -1) {
tmpAddons[index].quantity = addon.quantity; // Update the count
}
}
}
mergedAddons.value = tmpAddons;
return mergedAddons.value;
};
// Recalculate the merged addons when the props.addons change
watch(() => props.addons, (newAddons) => {
mergeCountAddons(pos.productList.get(), newAddons);
}, { immediate: true });
</script>
<template>
<template v-if="pos.categories.isSelected()">
<template v-for="product in pos.productList.get()" :key="product.id">
<PosDepartmentStepMobile2CategoryProduct
@addProduct="emits('addProduct', product)"
:price="product.price"
:label="product.name"
:piktogram="product.piktogram"
/>
<template v-if="!props.asAddons">
<!-- Display of products, without a "basket" -->
<template v-for="product in pos.productList.get()" :key="product.id">
<PosDepartmentStepMobile2CategoryProduct
@addProduct="emits('addProduct', product)"
:price="product.price"
:label="product.name"
:piktogram="product.piktogram"
/>
</template>
</template>
<template v-else>
<!-- Display of products, with a "basket" -->
<PosDepartmentStepMobile2Addons :label="''" :addons="mergedAddons"/>
</template>
</template>
</template>
@@ -10,14 +10,26 @@ import Swal from "sweetalert2";
const selectCustomerString = `${SessionUser.objects.global.language.select} ${SessionUser.objects.global.language.customer.toLowerCase()}`;
const props = defineProps({
// If true, the button will have a white background. Default is true.
isWhite: {
type: Boolean,
default: true,
},
// Function that returns a Promise, to be executed before proceeding to the next step.
onBeforeStep: {
type: Function,
default: () => Promise.resolve(),
},
// Custom action will override the default onClick behavior, if provided.
customAction: {
type: Function,
default: null,
},
// Custom is the button disabled state. If provided, this will override the default disabled logic.
customDisabled: {
type: Boolean,
default: null,
},
});
const updateReference = () => {
@@ -114,6 +126,9 @@ const step1 = () => {
reg_1.value = vehicles.vehicle_1.value?.reg || "";
reg_2.value = vehicles.vehicle_2.value?.reg || "";
reg_3.value = vehicles.vehicle_3.value?.reg || "";
// Merge references and notes
mergeReferences();
mergeNotes();
// We only need to check if the customer requires reference, if the field is empty.
if (!isNonEmptyString(reference.value)) {
validateReferenceRequirements().then((isValid) => {
@@ -129,16 +144,29 @@ const step1 = () => {
finalizeStep1();
}
};
const finalizeStep1 = () => {
const mergeReferences = () => {
if (metadata.getReference() && metadata.getReference().length >= 1) {
// If there is no reference, set it to the registration number of vehicle 1
reference.value = metadata.getReference();
updateReference();
} else if (reference.value && reference.value.length >= 1) {
// If there is a reference in the field, set it to the metadata
metadata.setReference(reference.value);
updateReference();
}
}
const mergeNotes = () => {
if (metadata.getNotes() && metadata.getNotes().length >= 1) {
order_notes.value = metadata.getNotes();
updateNotes();
} else if (order_notes.value && order_notes.value.length >= 1) {
metadata.setNotes(order_notes.value);
updateNotes();
}
}
const finalizeStep1 = () => {
mergeReferences();
mergeNotes();
// Upload the attachments if there are any (And the order id has been created)
if (attachments.getBase64().length > 0 && order_id.value > 0) {
for (const attachment of attachments.getBase64()) {
@@ -204,6 +232,11 @@ const step3 = () => {
};
const onClick = () => {
// Check if a custom action is provided
if (props.customAction) {
props.customAction();
return;
}
if (isCustomerSelected() && getCustomerId() > 0) {
props.onBeforeStep().then(() => {
// Proceed to the next step
@@ -257,6 +290,10 @@ const isRegistrationNumberFilled = () => {
}
// Is the requirements for clicking the button met?
const isRequirementsForClickMet = () => {
// If a custom disabled state is provided, use that.
if (props.customDisabled !== null) {
return !props.customDisabled;
}
/**
* States, and their requirements:
* If any popup is open, the button cannot be clicked.
@@ -15,12 +15,14 @@ import type { PosActionButton } from './PosActionButton.vue';
const manualInput = ref(false);
// Step 2
const vehicleSelection = ref(false);
const additionalItemSelection = ref(false);
const views = {
// Step 1: Manual Input of registration numbers.
manualInput,
// Step 2: Vehicle Selection from a list.
vehicleSelection,
vehicleSelection, // Used to select the "Primary" product for the transaction.
additionalItemSelection, // Used to select additional items for the transaction.
};
/** Sound effects */
const soundEffects = ref<PosSoundEffect[]>([]);
@@ -388,6 +390,10 @@ const removeAdditionalItem = (item: PosProduct) => {
const clearAdditionalItems = () => {
additionalItems.value = [];
};
// Function to set the additional items for the transaction (replaces the current list)
const setAdditionalItems = (items: PosProduct[]) => {
additionalItems.value = items;
};
// Function to get the total price of all additional items
const getAdditionalItemsTotal = () => {
let total = 0;
@@ -401,6 +407,18 @@ const getAdditionalItemsTotal = () => {
});
return total;
};
// Function to convert a product to an addon:
const convertProductToAddon = (product: PosProduct, options: {quantity?: number, min?: number, max?: number} = {}): Addon => {
return {
id: product.id,
name: product.name,
price: product.price,
product: product,
quantity: options.quantity || 0,
min: options.min || -1,
max: options.max || -1,
}
}
// Function to set the primary item for the transaction
const setPrimaryItem = (item: PosProduct | null) => {
console.warn('Setting primary item:', item);
@@ -494,6 +512,7 @@ const transactionItems = {
addAdditionalItem,
removeAdditionalItem,
clearAdditionalItems,
setAdditionalItems,
// Primary item related functions
primaryItem,
setPrimaryItem,
@@ -504,6 +523,8 @@ const transactionItems = {
getTransactionTotal,
// Update product prices if needed (e.g., if an item is already in the cart, update its price)
updateTransactionPrices,
// Convert a product to an addon
convertProductToAddon,
};
/** Categories */
@@ -945,6 +966,7 @@ const resetVehicles = () => {
const resetViews = () => {
views.manualInput.value = false;
views.vehicleSelection.value = false;
views.additionalItemSelection.value = false;
}
// Function to reset attachments
const resetAttachments = () => {
@@ -1111,7 +1133,8 @@ const buildSnapshot = () => ({
},
views: {
manualInput: views.manualInput.value,
vehicleSelection: views.vehicleSelection.value
vehicleSelection: views.vehicleSelection.value,
additionalItemSelection: views.additionalItemSelection.value
},
transactionItems: {
primaryItem: transactionItems.primaryItem.value,
@@ -1178,6 +1201,7 @@ const retrievePos = () => {
if (parsedData.views) {
views.manualInput.value = !!parsedData.views.manualInput;
views.vehicleSelection.value = !!parsedData.views.vehicleSelection;
views.additionalItemSelection.value = !!parsedData.views.additionalItemSelection;
}
// Restore transaction items
@@ -1234,7 +1258,7 @@ if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {
watch(() => [vehicles.vehicle_1.value, vehicles.vehicle_2.value, vehicles.vehicle_3.value, vehicles.activeVehicleIndex.value], savePos);
// Watch views state
watch(() => [views.manualInput.value, views.vehicleSelection.value], savePos);
watch(() => [views.manualInput.value, views.vehicleSelection.value, views.additionalItemSelection.value], savePos);
// Watch transaction items
watch(() => [transactionItems.primaryItem.value, transactionItems.additionalItems.value], savePos);
@@ -1305,6 +1329,10 @@ export default defineComponent({
popups,
reset,
hasRetrievedPos,
attachments,
locations,
sounds,
search,
};
}
});
@@ -1420,6 +1448,7 @@ export const Meta = {
export const UI = {
manualInput,
vehicleSelection,
additionalItemSelection,
};
export const Locations = {
@@ -1587,6 +1616,7 @@ export {
// UI State
manualInput,
vehicleSelection,
additionalItemSelection,
// Search
search,
@@ -1,5 +1,5 @@
<script setup lang="ts">
import {defineEmits} from "vue";
import {defineEmits, defineProps} from "vue";
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
import { pos } from "../objects/PosDepartmentStepMobileFlow.vue";
import GenericLabelSearch from "@/components/viewport/page/templates/generic/graphics/GenericLabelSearch.vue";
@@ -10,17 +10,54 @@ import PosDepartmentStepMobile2Products
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Products.vue";
import PosDepartmentStepMobile2ProductRecommendations
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2ProductRecommendations.vue";
import {Addon} from "@/components/displays/department/pos/steps/mobile/objects/PosAddon.vue";
// Define the close event to emit when the component is closed
const emit = defineEmits(["close"]);
// Define props
// Custom onAddProduct function to handle adding a product
const props = defineProps({
// Set a custom function to handle adding a product, if not provided, default will be the local onAddProduct function
onAddProduct: {
type: Function,
required: false,
default: null
},
// Set a custom onSearchClick function to handle search button click, if not provided, default will be the local onSearchClick function
onSearchClick: {
type: Function,
required: false,
default: null
},
asAddons: { // If true, products will be shown as addons, depends on the props.addons being set
type: Boolean,
required: false,
default: false
},
addons: {
type: Object as () => Addon[],
required: false,
},
});
// Function to handle adding a product
const onAddProduct = (product: any) => {
// If a custom onAddProduct function is provided, use it
if (props.onAddProduct) {
product.quantity = product.quantity || 1; // Ensure quantity is set
props.onAddProduct(product); // Call the custom function if provided
return;
}
// Default behavior: add the product to the transaction and close the component
pos.transactionItems.setPrimaryItem(product); // Set the primary item in the transaction
emit("close"); // Emit the close event to notify the parent component
};
// Function to handle search button click
const onSearchClick = () => {
if (props.onSearchClick) {
props.onSearchClick(); // Call the custom function if provided
return;
}
console.log("Search clicked");
};
</script>
@@ -36,7 +73,7 @@ const onSearchClick = () => {
<!-- Product Recommendations -->
<!--<PosDepartmentStepMobile2ProductRecommendations/>-->
<!-- Products -->
<PosDepartmentStepMobile2Products @addProduct="onAddProduct"/>
<PosDepartmentStepMobile2Products @addProduct="onAddProduct" :asAddons="props.asAddons" :addons="props.addons"/>
<!-- Next button -->
<GenericButton class="p-5" @click="emit('close')">
Next
@@ -27,6 +27,7 @@ export const ObjectsGlobal = {
showing_of_separator: "af",
send_as_is: "Send som den er",
ignore_customer_arrangements: "Ignorer kundeaftaler",
recommended: "Anbefalet",
unlink: "Fjern tilknytning",
link: "Tilknyt",
include_all_items_on_invoice: "Inkluder alle varer på faktura",