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:
@@ -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;
|
||||
}
|
||||
@@ -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} / ${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> </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;">
|
||||
/
|
||||
</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> </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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user