Add related items support and improve POS UI/UX

Introduced support for related items in orders by enabling nested relationships through the `related_item_id` parameter. Enhanced the POS interface with better UI elements, such as reusable components (`ProductBox`, `WhiteBox`) and dynamic labels for buttons. Debugging tools and query parameter support for `order_id`, `step`, and `customer_id` were also added to streamline the process flow.
This commit is contained in:
Jepp9350
2025-02-25 17:57:08 +01:00
parent 333898469d
commit 800253699c
13 changed files with 529 additions and 59 deletions
+10
View File
@@ -3,4 +3,14 @@
}
textarea.has-sharp-edges {
border-radius: 0;
}
.pos-addon-popper {
padding: 0.5rem;
background-color: #fbfcfe;
opacity: 0.9;
border: 3px solid #68696b;
min-width: 250px;
min-height: 100px;
text-align: center;
}
+17
View File
@@ -66,6 +66,23 @@ export const popperBox = (title, content, data_key = null) => {
</div>`;
};
export const popperBoxProductAddonHtml = (id, name, price, description, classes = '') => {
let html = `<div class="popper-box ${classes}" data-key="product-addon-${id}-popper">`;
html += `<p class="title is-6">${name}&nbsp; / &nbsp;${price} Kr.</p>`;
html += `<p class="subtitle is-6 mb-1 has-text-link">No. ${id}</p>`;
// If the description is set (and isn't empty), show it
if (description && description !== '' && description !== ' ') {
html += `<p class="subtitle is-6">${description}</p>`;
}
// Otherwise, show a placeholder
else {
html += `<p class="subtitle is-6">No description available</p>`;
}
html += `</div>`;
return html;
};
export const showPopperWithContent = (title, content) => {
showPopper(popperBox(title, content));
};
@@ -0,0 +1,136 @@
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
import { Colors } from "@/ThemeConfig.vue";
import { showPopper, popperBoxProductAddonHtml, removePopperIfOpen } from "@/components/displays/PopperDefault.vue";
const emit = defineEmits(['add-to-cart']);
const props = defineProps({
id: Number,
name: String,
description: String,
price: Number,
image: String,
addons: Array,
isSelected: Boolean,
selectedAddons: Array,
toggleProductAddon: Function,
});
const isAddonSelected = (productId, addonId) => {
return props.selectedAddons.some((addon) => addon.product_id === productId && addon.addon_id === addonId && addon.status === true);
};
const getAddonPrice = (addonId) => {
const addon = props.addons.find((addon) => addon.id === addonId);
return addon.product.price;
};
const showAddonPopper = (addon, element) => {
showPopper(popperBoxProductAddonHtml(
addon.id,
addon.name,
addon.product.price,
addon.product.description,
'pos-addon-popper'
), element);
};
const hideAddonPopper = (addon) => {
removePopperIfOpen();
};
const emitAddToCart = (id) => {
emit('add-to-cart', id);
};
</script>
<template>
<div class="pt-2 mb-2 pr-6">
<div class="columns is-multiline pos-product-box" :class="{ 'is-selected': isSelected }">
<!-- Product image -->
<div class="column is-4-desktop">
<img :src="image" alt="Product image" class="image pos-product-image pl-5 mt-4" />
</div>
<!-- Product details -->
<div class="column is-8-desktop">
<p class="title is-6">{{ name }}</p>
<p class="subtitle is-6 mb-0" :style="{ 'color': Colors.global.primaryColor }">
No. {{ id }}</p>
<p class="subtitle is-6">{{ description }}</p>
<!-- White space, to accommodate the design requirements -->
<p>&nbsp;</p>
<p class="subtitle is-6 has-text-black">{{ price }} Kr.</p>
</div>
<!-- Expandable product details -->
<div v-if="isSelected" class="column is-12-desktop">
<!-- Addons -->
<div class="buttons is-centered mt-3 mb-0 pl-6" v-if="addons.length > 0">
<button
class="button is-small is-fullwidth"
v-for="addon in addons"
:key="addon.id"
:class="{ 'is-link': isAddonSelected(props.id, addon.option_id), 'is-light': !isAddonSelected(props.id, addon.option_id) }"
@click="toggleProductAddon(props.id, addon.option_id)"
@mouseenter="showAddonPopper(addon, $event.target)"
@mouseleave="hideAddonPopper(addon, $event.target)"
>
<!-- Addon ( + ) -->
<span class="icon is-small" style="margin-right: auto; margin-left: 1rem;">
<i class="fas fa-plus fa-xs"></i>
</span>
<!-- Addon name -->
<span style="font-size: 0.7rem;">
{{ addon.name }}
</span>
<!-- Price separator -->
<span style="font-size: 0.7rem;">
&nbsp;/&nbsp;
</span>
<!-- Addon price -->
<span style="font-size: 0.7rem;">
{{ getAddonPrice(addon.id) }} Kr.
</span>
<!-- Addon ( - ) -->
<span class="icon is-small" style="margin-left: auto; margin-right: 1rem;">
<i class="fas fa-minus fa-xs"></i>
</span>
</button>
</div>
<!-- White space, to accommodate the design requirements -->
<p>&nbsp;</p>
<!-- Add to cart -->
<div class="buttons is-centered mt-2 mb-3 pl-6">
<button
class="button is-link is-small is-fullwidth"
@click="emitAddToCart(id)"
>
<span class="icon is-small">
<i class="fas fa-cart-plus"></i>
</span>
<span>Tilføj til kurv</span>
</button>
</div>
</div>
<!-- Expander -->
<div class="column is-12-desktop">
<p class="has-text-centered pl-6">
<span class="icon is-small" style="color: #c7c7c7;">
<i
class="fas fa-angle-down fa-lg"
:class="{ 'fa-rotate-180': isSelected }"
aria-hidden="true"
></i>
</span>
</p>
</div>
</div>
</div>
</template>
<style scoped>
</style>
@@ -1,12 +1,41 @@
<script setup>
import { ref } from 'vue';
import { getCurrentStep, setDepartment, getOrderId } from "@/components/shop/POSDepartmentProcess.vue";
import { getCurrentStep, setDepartment, getOrderId, setStep, setOrderId, searchAndSelectCustomer, loadOrderItems } from "@/components/shop/POSDepartmentProcess.vue";
import PosDepartmentStep1 from "@/components/displays/department/pos/steps/PosDepartmentStep1.vue";
import PosDepartmentStep2 from "@/components/displays/department/pos/steps/PosDepartmentStep2.vue";
import PosDepartmentStep3 from "@/components/displays/department/pos/steps/PosDepartmentStep3.vue";
setDepartment();
// Debug:
// If the &id= query parameter is set, then set the order_id to the value of the query parameter
// If the &step= query parameter is set, then set the current_step to the value of the query parameter
// If the &customer_id= query parameter is set, then set the customer_id to the value of the query parameter
if (window.location.search.includes("id=")) {
const urlParams = new URLSearchParams(window.location.search);
const orderId = urlParams.get("id");
if (orderId !== null) {
setOrderId(parseInt(orderId));
}
loadOrderItems();
}
if (window.location.search.includes("step=")) {
const urlParams = new URLSearchParams(window.location.search);
const step = urlParams.get("step");
if (step !== null) {
setStep(parseInt(step));
}
}
if (window.location.search.includes("customer_id=")) {
const urlParams = new URLSearchParams(window.location.search);
const customerId = urlParams.get("customer_id");
if (customerId !== null) {
searchAndSelectCustomer(parseInt(customerId));
}
}
/** Define the createOrder function */
</script>
@@ -8,6 +8,8 @@ import {ref, watch, defineProps, defineSlots, useSlots, renderSlot } from 'vue';
import { createPopper } from '@popperjs/core';
import { showPopper, popper, showPopperWithContent, removePopperIfOpen, popperBox} from "@/components/displays/PopperDefault.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
// Tabs
@@ -47,13 +49,28 @@ const hideOrderItemWhileRemoving = (id) => {
document.getElementById('order-item-' + id).style.display = 'none';
};
const getItemRelatedItems = (orderItems, orderItemId) => {
// Return all items with the item.related_item_id equal to the orderItemId
return orderItems.filter((orderItem) => orderItem.related_item_id === orderItemId);
};
const isLastRelatedItem = (orderItems, orderItem) => {
// Get all related items
const relatedItems = getItemRelatedItems(orderItems, orderItem.related_item_id);
// If there are no related items, return true
if (relatedItems.length === 0) {
return true;
}
// If the last related item is the current item, return true
return relatedItems[relatedItems.length - 1].id === orderItem.id;
};
</script>
<template>
<div>
<article class="panel is-dark">
<p class="panel-heading"><span>Transaktion #{{ orderId }}</span><slot name="headingcenter"></slot><span class="is-float-right"><slot></slot></span></p>
<p class="panel-tabs">
<article class="">
<p><span class="is-size-4">Transaktion #{{ orderId }}</span><slot name="headingcenter"></slot><span class="is-float-right"><slot></slot></span></p>
<p class="panel-tabs" v-if="panel_tabs.length > 1">
<a v-for="(tab, index) in panel_tabs" :key="index" :class="{'is-active': tab.active}" @click="panel_tabs.forEach(tab => tab.active = false); tab.active = true" >
<!-- If the tab has an icon, show it -->
<span v-if="tab.icon" class="icon is-small">
@@ -69,15 +86,20 @@ const hideOrderItemWhileRemoving = (id) => {
<thead>
<tr>
<th>Produkt</th>
<th style="width: 20%; text-align: right;">Pris</th>
<th style="width: 20%; text-align: right;">Pris (DKK)</th>
<th v-if="editPossible" style="width: 15%;"></th>
</tr>
</thead>
<tbody>
<!-- Slot -->
<slot name="beforeItems"></slot>
<tr v-for="orderItem in orderItems" :key="orderItem.id" :id="'order-item-' + orderItem.id">
<template v-for="orderItem in orderItems" :key="orderItem.id">
<tr
:id="'order-item-' + orderItem.id"
v-if="!orderItem.related_item_id"
:style="{ 'border-bottom-style': getItemRelatedItems(orderItems, orderItem.id).length > 0 ? 'hidden' : '' }"
style="border-top: 1px solid #f5f5f5;"
>
<td>
<span>{{ orderItem.product.name }}</span>
<!-- Notes (If there are any) -->
@@ -137,7 +159,7 @@ const hideOrderItemWhileRemoving = (id) => {
$event.target
)"
@mouseleave="removePopperIfOpen()">
<span style="text-decoration: line-through;" class="has-text-dark">{{ orderItem.product.price * orderItem.quantity}}</span> <span>{{ orderItem.price * orderItem.quantity }} DKK</span>
<span style="text-decoration: line-through;" class="has-text-dark">{{ orderItem.product.price * orderItem.quantity}}</span> <span>{{ orderItem.price * orderItem.quantity }}</span>
</span>
</td>
<td style="width: 20%; text-align: right;" v-else>
@@ -150,16 +172,136 @@ const hideOrderItemWhileRemoving = (id) => {
$event.target
)"
@mouseleave="removePopperIfOpen()">
{{ orderItem.price * orderItem.quantity }} DKK
{{ orderItem.price * orderItem.quantity }}
</span>
</td>
<td v-if="editPossible" style="width: 15%;">
<div class="buttons is-narrow">
<button class="button is-small is-dark" @click="hideOrderItemWhileRemoving(orderItem.id);removeOrderItem(orderItem.id).then(() => loadOrderItems())"><span class="icon has-text-danger"><i class="fas fa-trash"></i></span></button>
<button class="button is-small is-dark" :disabled="!SessionUser.hasPermission('edit_order_items')" @click="showEditOrderItemForm(orderItem.id, orderItem.product.name, orderItem.price, orderItem.notes, orderItem.reference, orderItem.quantity, editPossible).then(() => loadOrderItems())"><span class="icon"><i class="fas fa-edit"></i></span></button>
<ActionSettingsWheelButton>
<template #actions>
<ActionSettingsWheelItem
label="Slet"
icon="fas fa-trash"
template="danger"
@click="hideOrderItemWhileRemoving(orderItem.id);removeOrderItem(orderItem.id).then(() => loadOrderItems())"
/>
<ActionSettingsWheelItem
label="Rediger"
icon="fas fa-edit"
template="warning"
:disabled="!SessionUser.hasPermission('edit_order_items')"
@click="showEditOrderItemForm(orderItem.id, orderItem.product.name, orderItem.price, orderItem.notes, orderItem.reference, orderItem.quantity, editPossible).then(() => loadOrderItems())"
/>
</template>
</ActionSettingsWheelButton>
</div>
</td>
</tr>
<!-- Related items -->
<template v-for="relatedItem in getItemRelatedItems(orderItems, orderItem.id)" :key="relatedItem.id">
<tr
:id="'order-item-' + relatedItem.id"
:style="{ 'border-bottom-style': isLastRelatedItem(orderItems, relatedItem) ? '' : 'hidden' }"
>
<td>
<span class="related-item-text">+ {{ relatedItem.product.name }}</span>
<!-- Notes (If there are any) -->
<span v-if="relatedItem.notes"
@mouseover="showPopper(
popperBox(
'Note',
relatedItem.notes
),
$event.target
)"
@mouseleave="removePopperIfOpen()"
@click="showEditOrderItemForm(relatedItem.id, relatedItem.product.name, relatedItem.price, relatedItem.notes, relatedItem.reference, relatedItem.quantity, editPossible).then(() => loadOrderItems())"
class="ml-2 is-size-7 has-text-grey is-clickable">
<i class="fas fa-comment"></i>
Se note
</span>
<!-- Reference (If there are any) -->
<span v-if="relatedItem.reference"
@mouseover="showPopper(
popperBox(
'Reference',
relatedItem.reference
),
$event.target
)"
@mouseleave="removePopperIfOpen()"
@click="showEditOrderItemForm(relatedItem.id, relatedItem.product.name, relatedItem.price, relatedItem.notes, relatedItem.reference, relatedItem.quantity, editPossible).then(() => loadOrderItems())"
class="ml-2 is-size-7 has-text-grey is-clickable">
<i class="fas fa-link"></i>
Se reference
</span>
<!-- Quantity (If it is more than 1) -->
<span v-if="relatedItem.quantity > 1"
@mouseover="showPopper(
popperBox(
'Antal',
relatedItem.quantity
),
$event.target
)"
@mouseleave="removePopperIfOpen()"
@click="showEditOrderItemForm(relatedItem.id, relatedItem.product.name, relatedItem.price, relatedItem.notes, relatedItem.reference, relatedItem.quantity, editPossible).then(() => loadOrderItems())"
class="ml-2 is-size-7 has-text-grey is-clickable">
<i class="fas fa-box"></i>
</span>
</td>
<!-- If the price has been changed, show the new price (With a strike-through) and the old price -->
<td style="width: 20%; text-align: right;" v-if="relatedItem.price !== relatedItem.product.price">
<span
@mouseover="showPopper(
popperBox(
'Pris',
'Pris ændret fra ' + relatedItem.product.price + ' DKK til ' + relatedItem.price + ' DKK / Enhed (' + relatedItem.price * relatedItem.quantity + ' DKK i alt)'
),
$event.target
)"
@mouseleave="removePopperIfOpen()">
<span style="text-decoration: line-through;" class="has-text-dark related-item-text">{{ relatedItem.product.price * relatedItem.quantity}}</span> <span>{{ relatedItem.price * relatedItem.quantity }} DKK</span>
</span>
</td>
<td style="width: 20%; text-align: right;" v-else>
<span
class="related-item-text"
@mouseover="showPopper(
popperBox(
'Pris',
relatedItem.price + ' DKK / Enhed (' + relatedItem.price * relatedItem.quantity + ' DKK i alt)'
),
$event.target
)"
@mouseleave="removePopperIfOpen()">
{{ relatedItem.price * relatedItem.quantity }}
</span>
</td>
<td v-if="editPossible" style="width: 15%;">
<div class="buttons is-narrow">
<ActionSettingsWheelButton>
<template #actions>
<ActionSettingsWheelItem
label="Slet"
icon="fas fa-trash"
template="danger"
@click="hideOrderItemWhileRemoving(relatedItem.id);removeOrderItem(relatedItem.id).then(() => loadOrderItems())"
/>
<ActionSettingsWheelItem
label="Rediger"
icon="fas fa-edit"
template="warning"
:disabled="!SessionUser.hasPermission('edit_order_items')"
@click="showEditOrderItemForm(relatedItem.id, relatedItem.product.name, relatedItem.price, relatedItem.notes, relatedItem.reference, relatedItem.quantity, editPossible).then(() => loadOrderItems())"
/>
</template>
</ActionSettingsWheelButton>
</div>
</td>
</tr>
</template>
</template>
<slot name="bottom-order-items"></slot>
</tbody>
<tfoot>
@@ -206,5 +348,8 @@ const hideOrderItemWhileRemoving = (id) => {
</template>
<style scoped>
.related-item-text {
color: #4a4a4a;
font-size: 0.9rem;
}
</style>
@@ -145,11 +145,12 @@ loadCustomerAttributes();
<article class="">
<div class="panel-block">
<div class="columns is-vcentered">
<div class="column">
<span class="is-size-6 title">{{ customer_name }}</span>
<div class="column is-4">
<span class="is-size-6 title"
>{{ customer_name }}</span>
</div>
<div class="column is-narrow">
<div class="tabs is-right">
<div class="column">
<div class="tabs is-right is-small is-float-right">
<ul>
<li v-for="(tab, index) in panel_tabs" :key="index" :class="{'is-active': tab.active}" @click="panel_tabs.forEach(tab => tab.active = false); tab.active = true">
<RequiresPermission :permission="tab.permission" v-if="tab.permission">
@@ -10,6 +10,7 @@ import PosOrderItemsCurrent from "@/components/displays/department/pos/PosOrderI
import { order_items, order_id, loadOrderItems, setProductsCategory, notes } from "@/components/shop/POSDepartmentProcess.vue";
import Cancel from "@/components/forms/department/pos/buttons/Cancel.vue";
import PosNotes from "@/components/displays/department/pos/PosNotes.vue";
import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
// Set the category to products
setProductsCategory(null);
@@ -19,18 +20,24 @@ setProductsCategory(null);
<div>
<div class="columns">
<!-- Choose products -->
<div class="column is-8-desktop">
<p>Choose products</p>
<div class="column is-8-desktop pl-0 pr-3">
<SelectProductsFormPOS class="mt-3" category="null" />
<PosNotes :notes="notes" class="mt-3" :isAddFormVisible="false" :isOldNotesVisible="true"/>
</div>
<!-- Last provided orders -->
<div class="column is-4-desktop keep-in-viewport">
<PosOrderItemsCurrent :orderItems="order_items" :orderId="order_id" :loadOrderItems="loadOrderItems" :editPossible="true" />
<PosSelectedCustomer class="mt-3"/>
<!-- Current order items -->
<div class="column is-4-desktop keep-in-viewport pl-3 pr-0">
<WhiteBox style="min-height: 40%;">
<template #default>
<PosOrderItemsCurrent :orderItems="order_items" :orderId="order_id" :loadOrderItems="loadOrderItems" :editPossible="true" />
</template>
</WhiteBox>
<!--<PosSelectedCustomer class="mt-3"/> -->
<NextStepError class="is-fullwidth" />
<div class="buttons">
<NextStep class="is-fullwidth" />
<NextStep
class="is-fullwidth"
label="Overblik"
/>
<Cancel class="is-fullwidth" />
</div>
</div>
@@ -5,10 +5,12 @@ import PosLastScannedLicensePlates from "@/components/displays/department/pos/Po
import NextStep from "@/components/forms/department/pos/buttons/NextStep.vue";
import SelectProductsFormPOS from "@/components/forms/department/pos/SelectProductsFormPOS.vue";
import NextStepError from "@/components/forms/department/pos/error/NextStepError.vue";
import { order_items, order_id, loadOrderItems } from "@/components/shop/POSDepartmentProcess.vue";
import { order_items, order_id, loadOrderItems, isCustomerSelected } from "@/components/shop/POSDepartmentProcess.vue";
import PosOrderItemsCurrent from "@/components/displays/department/pos/PosOrderItemsCurrent.vue";
import PosSelectedCustomer from "@/components/displays/department/pos/PosSelectedCustomer.vue";
import Cancel from "@/components/forms/department/pos/buttons/Cancel.vue";
import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
import ButtonsBox from "@/components/displays/boxes/ButtonsBox.vue";
</script>
@@ -16,19 +18,36 @@ import Cancel from "@/components/forms/department/pos/buttons/Cancel.vue";
<div>
<div class="columns">
<!-- Choose add-ons -->
<div class="column is-8-desktop">
<p>Choose add-ons</p>
<SelectProductsFormPOS class="mt-3" category="4" />
<div class="column is-8 pl-0 pr-3">
<WhiteBox>
<template #default>
<PosOrderItemsCurrent :orderItems="order_items" :orderId="order_id" :loadOrderItems="loadOrderItems" :editPossible="true" />
</template>
</WhiteBox>
<NextStepError class="is-fullwidth" />
<ButtonsBox>
<div class="buttons">
<Cancel
style="width: 25%;"
tabindex="2"
class="is-fullwidth"
label="Annuller"
/>
<NextStep
style="width: 25%;"
class="is-fullwidth"
label="Bekræft"
/>
</div>
</ButtonsBox>
</div>
<!-- Last provided orders -->
<div class="column is-4-desktop">
<PosOrderItemsCurrent :orderItems="order_items" :orderId="order_id" :loadOrderItems="loadOrderItems" :editPossible="true" />
<PosSelectedCustomer />
<NextStepError class="is-fullwidth" />
<div class="buttons">
<NextStep class="is-fullwidth" />
<Cancel class="is-fullwidth" />
</div>
<div class="column is-4 pl-3 pr-0">
<WhiteBox>
<template #default v-if="isCustomerSelected()">
<PosSelectedCustomer />
</template>
</WhiteBox>
</div>
</div>
</div>
@@ -30,6 +30,8 @@ import {SessionUser} from "@/components/session/token/SessionUser.vue";
import { useRoute } from 'vue-router';
import Swal from "sweetalert2";
import ConfigurationCategory from "@/components/displays/superuser/configuration/ConfigurationCategory.vue";
import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
import ProductBox from "@/components/displays/boxes/ProductBox.vue";
// Define the products
const products = ref([]);
@@ -196,19 +198,40 @@ const setProductAddon = (productId, addonId, status) => {
}
};
const addAddonsToOrderMiddleware = async (product_id, quantity = 1) => {
// Toggle product addon
const toggleProductAddon = (productId, addonId) => {
console.log(productId, addonId);
setProductAddon(productId, addonId, !isProductAddonTrue(productId, addonId));
};
const addAddonsToOrderMiddleware = async (product_id, quantity = 1, related_item_id = null) => {
// If the product addons are set, add them to the order (If the product is added)
var product_addons = productAddons.value.filter((productAddon) => productAddon.product_id === product_id && productAddon.status === true);
for (let i = 0; i < product_addons.length; i++) {
// Get the products addon, and show the fake create order item
showFakeCreateOrderItem(product_addons[i].addon_id, quantity, 0);
showFakeCreateOrderItem(product_addons[i].addon_id, quantity, 0, related_item_id);
}
// Add the addons to the order
for (let i = 0; i < product_addons.length; i++) {
await createOrderItem(getOrderId(), product_addons[i].addon_id, quantity);
await createOrderItem(getOrderId(), product_addons[i].addon_id, quantity, related_item_id);
}
};
const addProductWithAddonsToOrder = async (product_id) => {
console.log(product_id);
let quantity = 1;
// Show the fake create order item
showFakeCreateOrderItem(product_id, quantity, 0);
// Create the order item
await createOrderItem(getOrderId(), product_id, quantity).then(async (result) => {
let order_item_id = result.data.data.id;
// Add the addons to the order
await addAddonsToOrderMiddleware(product_id, quantity, order_item_id).then(() => {
loadOrderItems();
});
});
};
const recommended_product_ids = ref([]);
const recommended_last_orders = ref([]);
@@ -246,32 +269,66 @@ const showRecommendedProducts = () => {
const hideRecommendedProducts = () => {
isShowingRecommended.value = false;
};
const selectedProduct = ref(null);
const isProductSelected = (productId) => {
return selectedProduct.valueOf() === parseInt(productId);
};
const selectProduct = (productId) => {
if (selectedProduct.valueOf() === parseInt(productId)) {
selectedProduct.value = null;
} else {
selectedProduct.value = parseInt(productId);
}
};
const getAddonPrice = (productId, addonId) => {
return products.value.find((product) => product.id === productId).addons.find((addon) => addon.option_id === addonId).price;
};
</script>
<template>
<div>
<div class="columns is-multiline">
<div class="column is-12">
<div class="column is-12 is-flex is-justify-content-space-between">
<!-- Categories -->
<div class="buttons">
<!-- Recommended products -->
<button class="button has-text-success" @click="showRecommendedProducts()">
<span class="icon">
<i class="fas fa-history"></i>
</span>
<span>
Anbefalede produkter
</span>
</button>
<!-- Category buttons -->
<button class="button" v-for="category in categories" :key="category.identifier" @click="hideRecommendedProducts(); setProductsCategory(category.identifier); getProductCategory(category.identifier, department_id.valueOf()).then((response) => { products = response.data.data; });" :disabled="category.identifier === getProductsCategory() && !isShowingRecommended">
{{ category.name }}
</button>
</div>
<WhiteBox style="padding-bottom: 0; padding-top: 0.6rem;" class="px-0">
<template #default>
<!-- Tabs -->
<div class="tabs" style="overflow-x: auto;">
<ul>
<li
v-for="category in categories"
:key="category.identifier"
:class="{
'is-active': category.identifier === getProductsCategory() && !isShowingRecommended
}">
<a
@click="hideRecommendedProducts();
setProductsCategory(category.identifier);
getProductCategory(category.identifier, department_id.valueOf())
.then((response) => {
products = response.data.data;
});"
:style="{ 'border-bottom-color': category.identifier === getProductsCategory() ? '#3273dc' : '#fff' }"
style="
border-bottom-width: 0.6rem;
min-width: 4rem;
font-size: 0.8rem;
">
{{ category.name }}
</a>
</li>
</ul>
</div>
</template>
</WhiteBox>
</div>
</div>
<div class="tw-scroll-minimal" style="overflow-y: auto; overflow-x: clip; max-height: 100vh;">
<div class="columns is-multiline is-vcentered is-mobile">
<div class="columns is-multiline is-mobile">
<!-- Recommended products -->
<div class="column is-12" v-if="isShowingRecommended">
<!-- Same as last time -->
@@ -372,6 +429,30 @@ const hideRecommendedProducts = () => {
<pre>{{ recommended_product_ids }}</pre>
</configurationCategory>
</div>
<!-- New product display -->
<template v-for="product in products" :key="product.id" v-if="!isShowingRecommended">
<div class="column is-6-desktop">
<WhiteBox
class="p-0 pos-product-box"
:class="{ 'is-selected': selectedProduct === product.id }"
>
<ProductBox
:id="product.id"
:name="product.name"
:description="product.description"
:price="product.price"
:image="getPicture(product.piktogram)"
:addons="product.addons"
:isSelected="selectedProduct === product.id"
:selectedAddons="productAddons"
:toggleProductAddon="toggleProductAddon"
@click="selectProduct(parseInt(product.id))"
@addToCart="addProductWithAddonsToOrder(product.id)"
/>
</WhiteBox>
</div>
</template>
<!-- Products -->
<div class="column is-4-desktop" v-for="product in products" :key="product.id" v-if="!isShowingRecommended">
<div class="card" :style="{ 'border': getProductBorder(product) }">
@@ -438,4 +519,11 @@ const hideRecommendedProducts = () => {
scrollbar-color: #4A5568 transparent;
scrollbar-width: thin;
}
.pos-product-box {
border: 0.5rem solid transparent;
cursor: pointer;
}
.pos-product-box.is-selected {
border: 0.5rem solid #3273dc;
}
</style>
@@ -1,6 +1,10 @@
<script setup>
import { nextStep, nextStepDelay } from "@/components/shop/POSDepartmentProcess.vue";
import { Colors } from "@/ThemeConfig.vue";
import { defineProps } from 'vue';
const props = defineProps(['label']);
const label = props.label || 'Næste';
</script>
<template>
@@ -9,7 +13,7 @@ import { Colors } from "@/ThemeConfig.vue";
@click="nextStep" v-if="nextStepDelay === 0"
:style="{ 'background-color': Colors.buttons.success.backgroundColor, 'color': Colors.buttons.success.textColor }"
>
Næste</button>
{{ label }}</button>
<button class="button is-loading" v-else>Please wait... {{ nextStepDelay }}</button>
</template>
+1 -1
View File
@@ -148,7 +148,7 @@ if (props.menu_items.length > 0) {
:style="{ 'background-color': Colors.menus.parentBackgroundColor, 'color': Colors.menus.parentTextColor }"
>
<!-- Image -->
<div class="menu-image" style="padding: 1rem;">
<div class="menu-image has-text-centered py-4 px-6">
<img src="@/assets/branding/truckwash-banner-white-compressed.png" alt="Truck Wash Logo" />
</div>
<p class="menu-label"
+3 -2
View File
@@ -15,7 +15,7 @@ export const getOrderItems = (order_id) => {
});
};
export const createOrderItem = (order_id, product_id, quantity) => {
export const createOrderItem = (order_id, product_id, quantity, related_item_id = null) => {
const token = localStorage.getItem('token');
if (!token) {
return null;
@@ -23,7 +23,8 @@ export const createOrderItem = (order_id, product_id, quantity) => {
return axios.post(API_URL + '/order/items', {
order_id,
product_id,
quantity
quantity,
related_item_id
}, {
headers: {
Authorization: `Bearer ${token}`
@@ -370,6 +370,19 @@ export const getCustomerName = async (customerNumber) => {
return '';
};
/** Search, then select customer by customer number */
export const searchAndSelectCustomer = async (customerNumber) => {
// Get the customer data
await authenticatedRequest(
`/users/customer?customer_number=${customerNumber}`,
'GET').then((response) => {
console.log(response);
selectCustomer(response.data.data.economic_customer);
}).catch((error) => {
console.error(error);
});
};
/** Show selected scan dialog */
export const showSelectedScanDialog = (scan) => {
// Show a loading swal