Add employee fixtures, mobile POS customer note popup, and alignment updates:
- Introduced employee fixtures in `mobilePos.js` for testing workflows involving employee data. - Enhanced `PosDepartmentStep1MobileCustomerNotes.vue` with translated labels and data-test attributes. - Added e2e tests for customer notes popup and translated label validation. - Improved alignment and responsiveness for `PaginationDisplayGeneralSearchReload.vue` and related e2e tests. - Updated button logic and styles in `GenericButton.vue` and other mobile POS components for consistent UI behavior.
This commit is contained in:
@@ -116,8 +116,12 @@ onBeforeUnmount(() => {
|
|||||||
@click="onFieldClicked()"
|
@click="onFieldClicked()"
|
||||||
>
|
>
|
||||||
<span v-if="previewValue" class="pos-order-field__preview">{{ previewValue }}</span>
|
<span v-if="previewValue" class="pos-order-field__preview">{{ previewValue }}</span>
|
||||||
<span v-else class="pos-order-note-editor__empty" data-testid="pos-order-note-empty-state">
|
<span
|
||||||
<span class="pos-order-field__empty-pill">{{ t('pos.add_note') }}</span>
|
v-else
|
||||||
|
class="pos-order-field__empty-state pos-order-note-editor__empty"
|
||||||
|
data-testid="pos-order-note-empty-state"
|
||||||
|
>
|
||||||
|
<span class="pos-order-field__empty-pill">+ {{ t('pos.add_note') }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1215,7 +1215,16 @@ const formatCashierName = (order) => {
|
|||||||
<!-- Orders as cards -->
|
<!-- Orders as cards -->
|
||||||
<template v-for="order in getVisibleOrders" :key="order.id">
|
<template v-for="order in getVisibleOrders" :key="order.id">
|
||||||
<div class="column is-full">
|
<div class="column is-full">
|
||||||
<WhiteBoxCard :forceStateFooter="true" :forceState="false" :defaultOpen="false" :toggleable="true" class="is-clickable" :has-hover-effect="true" :hasSelectionStyle="false">
|
<WhiteBoxCard
|
||||||
|
:forceStateFooter="true"
|
||||||
|
:forceState="false"
|
||||||
|
:defaultOpen="false"
|
||||||
|
:toggleable="true"
|
||||||
|
class="is-clickable pos-orders-mobile-card"
|
||||||
|
:has-hover-effect="true"
|
||||||
|
:hasSelectionStyle="false"
|
||||||
|
:data-testid="`pos-order-list-card-${order.id}`"
|
||||||
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header-title is-flex is-justify-content-space-between is-align-items-center" style="width: 100%;">
|
<div class="card-header-title is-flex is-justify-content-space-between is-align-items-center" style="width: 100%;">
|
||||||
<div>
|
<div>
|
||||||
@@ -1541,4 +1550,30 @@ const formatCashierName = (order) => {
|
|||||||
.status-bar.has-text-success {
|
.status-bar.has-text-success {
|
||||||
border-color: #48c774;
|
border-color: #48c774;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.pos-orders-mobile-card {
|
||||||
|
border: 1px solid #d7dee8;
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
box-shadow: 0 10px 24px rgba(19, 35, 57, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-orders-mobile-card :deep(.card) {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-orders-mobile-card :deep(.card-header) {
|
||||||
|
border-bottom: 1px solid #e3e9f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-orders-mobile-card :deep(.card-content) {
|
||||||
|
border-top: 1px solid #eef2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-orders-mobile-card :deep(.card-footer) {
|
||||||
|
border-top: 1px solid #e3e9f2;
|
||||||
|
background: #fbfcfe;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -244,6 +244,10 @@ const applyPendingBookingFromSelection = async () => {
|
|||||||
const preparedAddons = addonProducts
|
const preparedAddons = addonProducts
|
||||||
.filter(p => !!p)
|
.filter(p => !!p)
|
||||||
.map(p => transactionItems.convertProductToAddon(p as any, { quantity: (p as any)?.quantity || 1 }));
|
.map(p => transactionItems.convertProductToAddon(p as any, { quantity: (p as any)?.quantity || 1 }));
|
||||||
|
const bookingIncludesWashCertificate = preparedAddons.some((addon: any) =>
|
||||||
|
addon?.id === 41 || addon?.product?.id === 41
|
||||||
|
);
|
||||||
|
attachments.setWashCertificate(bookingIncludesWashCertificate);
|
||||||
|
|
||||||
let effectivePrimaryProduct = primaryProduct;
|
let effectivePrimaryProduct = primaryProduct;
|
||||||
if (!primaryProduct.is_wash) {
|
if (!primaryProduct.is_wash) {
|
||||||
@@ -568,8 +572,16 @@ watch(() => vehicles?.vehicle_1?.value?.reg, () => {
|
|||||||
// Utility: map source addons and carry over quantities from previous addons by product.id
|
// Utility: map source addons and carry over quantities from previous addons by product.id
|
||||||
const mapAddonsWithQuantity = (sourceAddons = [], previousAddons = []) =>
|
const mapAddonsWithQuantity = (sourceAddons = [], previousAddons = []) =>
|
||||||
sourceAddons.map(addon => {
|
sourceAddons.map(addon => {
|
||||||
const carriedQty = previousAddons.find(a => a.product.id === addon.product.id)?.quantity ?? 0;
|
const previousAddon = previousAddons.find(a => a.product.id === addon.product.id);
|
||||||
return { ...addon, quantity: carriedQty };
|
const nextQuantity = previousAddon?.quantity ?? addon?.quantity ?? addon?.product?.quantity ?? 0;
|
||||||
|
return {
|
||||||
|
...addon,
|
||||||
|
quantity: nextQuantity,
|
||||||
|
product: addon?.product ? {
|
||||||
|
...addon.product,
|
||||||
|
quantity: nextQuantity,
|
||||||
|
} : addon?.product,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
+1
-1
@@ -104,7 +104,7 @@ watch(() => metadata.getCustomerId(), (newCustomerId) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Reference -->
|
<!-- Reference -->
|
||||||
<div class="mt-2 mb-2 has-text-centered custom-button-secondary px-2" style="min-width: 174px;" :class="{'opacity-invisible': customerNotes.length === 0, 'has-background-warning': customerNotes.length > 0}" @click="popups.select('customer_notes')" v-if="customerNotes.length > 0">
|
<div class="mt-2 mb-2 has-text-centered custom-button-secondary px-2" style="min-width: 174px;" :class="{'opacity-invisible': customerNotes.length === 0, 'has-background-warning': customerNotes.length > 0}" @click="popups.select('customer_notes')" v-if="customerNotes.length > 0" data-testid="pos-mobile-customer-notes-trigger">
|
||||||
<p>
|
<p>
|
||||||
<!-- If there are notes, show a warning icon -->
|
<!-- If there are notes, show a warning icon -->
|
||||||
<span v-if="customerNotes.length > 0" class="icon is-small has-text-dark pr-1">
|
<span v-if="customerNotes.length > 0" class="icon is-small has-text-dark pr-1">
|
||||||
|
|||||||
+23
-20
@@ -7,7 +7,7 @@ import { errors } from "@/components/request/HandleGlobalError.vue";
|
|||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
|
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
|
||||||
import ContinueArrow from "@/components/viewport/elements/icons/ContinueArrow.vue";
|
import ContinueArrow from "@/components/viewport/elements/icons/ContinueArrow.vue";
|
||||||
import { vehicles, metadata, getCustomerId, popups, resetPos, attachments, transactionHistory } from "../objects/PosDepartmentStepMobileFlow.vue";
|
import { vehicles, metadata, getCustomerId, popups, resetPos, attachments, transactionHistory, transactionItems } from "../objects/PosDepartmentStepMobileFlow.vue";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import LongPressListener from "@/components/viewport/elements/wrappers/LongPressListener.vue";
|
import LongPressListener from "@/components/viewport/elements/wrappers/LongPressListener.vue";
|
||||||
import {views} from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
import {views} from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
||||||
@@ -247,29 +247,13 @@ const step2 = () => {
|
|||||||
// Set the order bookings id to the metadata
|
// Set the order bookings id to the metadata
|
||||||
if (metadata.getBookingId() && metadata.getBookingId() > 0) {
|
if (metadata.getBookingId() && metadata.getBookingId() > 0) {
|
||||||
SessionUser.objects.order_bookings.set.order_id(metadata.getBookingId(), order_id.value).then(() => {
|
SessionUser.objects.order_bookings.set.order_id(metadata.getBookingId(), order_id.value).then(() => {
|
||||||
// Check if order requires safety seal popup (product id 41 present as item or addon)
|
|
||||||
try {
|
try {
|
||||||
const posStr = localStorage.getItem('pos');
|
const hasSeal = transactionItems.containsWashCertificate();
|
||||||
let hasSeal = false;
|
|
||||||
if (posStr) {
|
|
||||||
try {
|
|
||||||
const posData = JSON.parse(posStr);
|
|
||||||
const primaryItem = posData?.transactionItems?.primaryItem;
|
|
||||||
const additionalItems = posData?.transactionItems?.additionalItems || [];
|
|
||||||
const allItems = [primaryItem, ...additionalItems].filter(Boolean);
|
|
||||||
hasSeal = allItems.some((item: any) =>
|
|
||||||
item?.product?.id === 41 ||
|
|
||||||
(item?.addons && item.addons.some((addon: any) => addon?.product?.id === 41))
|
|
||||||
);
|
|
||||||
} catch (parseErr) {
|
|
||||||
console.error('Failed to parse pos data:', parseErr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hasSeal) {
|
if (!hasSeal) {
|
||||||
metadata.setLoadingState(true);
|
metadata.setLoadingState(true);
|
||||||
metadata.setLoadingMessage(t('admin.pos.waiting_for_booking'));
|
metadata.setLoadingMessage(t('admin.pos.waiting_for_booking'));
|
||||||
SessionUser.objects.order_bookings.functions.complete(metadata.getBookingId(), null, (response: any) => {
|
SessionUser.objects.order_bookings.functions.complete(metadata.getBookingId(), null, (response: any) => {
|
||||||
console.log('Auto-completed booking without safety seal prompt (mobile POS - localStorage)');
|
console.log('Auto-completed booking without safety seal prompt (mobile POS transaction state)');
|
||||||
completeOrder();
|
completeOrder();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -279,7 +263,7 @@ const step2 = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error checking safety seal via localStorage:', err);
|
console.error('Error checking safety seal from transaction state:', err);
|
||||||
popups.select('complete_booking', {});
|
popups.select('complete_booking', {});
|
||||||
awaitBookingCompletion(() => {
|
awaitBookingCompletion(() => {
|
||||||
completeOrder();
|
completeOrder();
|
||||||
@@ -549,12 +533,19 @@ const defaultLongPressBehavior = () => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pos-mobile-cta-label {
|
.pos-mobile-cta-label {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
display: -webkit-box;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
line-height: 1.2;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pos-mobile-cta-value {
|
.pos-mobile-cta-value {
|
||||||
@@ -574,12 +565,19 @@ const defaultLongPressBehavior = () => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.pos-mobile-cta-label) {
|
:deep(.pos-mobile-cta-label) {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
display: -webkit-box;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
line-height: 1.2;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.pos-mobile-cta-value) {
|
:deep(.pos-mobile-cta-value) {
|
||||||
@@ -588,6 +586,11 @@ const defaultLongPressBehavior = () => {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.generic-button__content) {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
/* Loading state */
|
/* Loading state */
|
||||||
.is-loading {
|
.is-loading {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
+5
-2
@@ -147,7 +147,10 @@ watch(() => props.showInput, (newVal) => {
|
|||||||
<br />
|
<br />
|
||||||
<small>{{ SessionUser.functions.ucFirst(SessionUser.functions.date.toWordsWithTime(note?.created_at || new Date())) }}</small>
|
<small>{{ SessionUser.functions.ucFirst(SessionUser.functions.date.toWordsWithTime(note?.created_at || new Date())) }}</small>
|
||||||
<br />
|
<br />
|
||||||
<small v-if="note?.cashier_id">{{ t('pos.created_by') }} <strong>{{ cashierNamesMap.find(cashier => cashier.id === parseInt(note.cashier_id))?.display_name || t('pos.unknown') }}</strong></small>
|
<small
|
||||||
|
v-if="note?.cashier_id"
|
||||||
|
:data-testid="`pos-mobile-customer-note-created-by-${note.id}`"
|
||||||
|
>{{ t('admin.pos.created_by') }} <strong>{{ cashierNamesMap.find(cashier => cashier.id === parseInt(note.cashier_id))?.display_name || t('admin.pos.unknown') }}</strong></small>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,7 +161,7 @@ watch(() => props.showInput, (newVal) => {
|
|||||||
<input
|
<input
|
||||||
class="input has-sharp-edges"
|
class="input has-sharp-edges"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="t('pos.add_note_placeholder')"
|
:placeholder="t('admin.pos.add_note_placeholder')"
|
||||||
v-model="input"
|
v-model="input"
|
||||||
id="input_field"
|
id="input_field"
|
||||||
@keyup.enter="createNewNote(input); input=''"
|
@keyup.enter="createNewNote(input); input=''"
|
||||||
|
|||||||
@@ -64,9 +64,10 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Top row: Reload, Search -->
|
<!-- Top row: Reload, Search -->
|
||||||
<div class="columns is-vcentered is-multiline">
|
<div class="columns is-vcentered is-multiline pagination-general-search-reload">
|
||||||
<div class="column">
|
<div class="column pagination-general-search-reload__search-column">
|
||||||
<input
|
<input
|
||||||
|
data-testid="pagination-search-input"
|
||||||
class="input"
|
class="input"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('global.search_placeholder')"
|
:placeholder="$t('global.search_placeholder')"
|
||||||
@@ -74,12 +75,17 @@ onBeforeUnmount(() => {
|
|||||||
@input="search($event.target.value)"
|
@input="search($event.target.value)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow" v-if="$slots.buttons">
|
<div class="column is-narrow pagination-general-search-reload__buttons-column" v-if="$slots.buttons">
|
||||||
<!-- Custom buttons slot, passing loadList for convenience -->
|
<!-- Custom buttons slot, passing loadList for convenience -->
|
||||||
<slot name="buttons" :loadList="loadList"></slot>
|
<slot name="buttons" :loadList="loadList"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow pl-0">
|
<div class="column is-narrow pl-0 pagination-general-search-reload__action-column">
|
||||||
<div ref="actionDropdownRef" class="dropdown is-right" :class="{ 'is-active': isActionDropdownOpen }">
|
<div
|
||||||
|
ref="actionDropdownRef"
|
||||||
|
class="dropdown is-right pagination-general-search-reload__dropdown"
|
||||||
|
:class="{ 'is-active': isActionDropdownOpen }"
|
||||||
|
data-testid="pagination-reload-actions"
|
||||||
|
>
|
||||||
<div class="dropdown-trigger">
|
<div class="dropdown-trigger">
|
||||||
<div class="buttons has-addons action-dropdown-trigger">
|
<div class="buttons has-addons action-dropdown-trigger">
|
||||||
<button
|
<button
|
||||||
@@ -134,13 +140,92 @@ onBeforeUnmount(() => {
|
|||||||
.action-dropdown-trigger {
|
.action-dropdown-trigger {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-general-search-reload__dropdown {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.action-dropdown-item {
|
.action-dropdown-item {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-item.is-disabled {
|
.dropdown-item.is-disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.pagination-general-search-reload {
|
||||||
|
align-items: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-general-search-reload__search-column,
|
||||||
|
.pagination-general-search-reload__buttons-column,
|
||||||
|
.pagination-general-search-reload__action-column {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-general-search-reload__search-column {
|
||||||
|
flex: 1 1 0;
|
||||||
|
max-width: none;
|
||||||
|
min-width: 0;
|
||||||
|
order: 1;
|
||||||
|
padding-right: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-general-search-reload__search-column :deep(.input) {
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-general-search-reload__buttons-column {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
order: 3;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-general-search-reload__action-column {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-top: 0;
|
||||||
|
max-width: none;
|
||||||
|
order: 2;
|
||||||
|
padding-left: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-general-search-reload__dropdown {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-trigger {
|
||||||
|
display: flex;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-main,
|
||||||
|
.action-dropdown-toggle {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-main {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
justify-content: center;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-toggle {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button class="generic-button">
|
<button class="generic-button">
|
||||||
<span class="is-flex-wrap-nowrap" style="width: 90%;">
|
<span class="generic-button__content">
|
||||||
<slot />
|
<slot />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -33,10 +33,14 @@
|
|||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
/* Default text styles */
|
|
||||||
.generic-button span {
|
.generic-button__content {
|
||||||
/* Auto layout */
|
display: flex;
|
||||||
height: 18px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 100%;
|
||||||
font-family: 'Arial';
|
font-family: 'Arial';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -45,9 +49,6 @@
|
|||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
|
|
||||||
/* Inside auto layout */
|
/* Inside auto layout */
|
||||||
flex: none;
|
|
||||||
order: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
}
|
||||||
/* Disabled styles */
|
/* Disabled styles */
|
||||||
.generic-button:disabled {
|
.generic-button:disabled {
|
||||||
@@ -71,10 +72,9 @@
|
|||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
/* Disabled text styles */
|
|
||||||
.generic-button:disabled span {
|
.generic-button:disabled .generic-button__content {
|
||||||
/* Auto layout */
|
min-height: 100%;
|
||||||
height: 18px;
|
|
||||||
font-family: 'Arial';
|
font-family: 'Arial';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -83,9 +83,6 @@
|
|||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
|
|
||||||
/* Inside auto layout */
|
/* Inside auto layout */
|
||||||
flex: none;
|
|
||||||
order: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
}
|
||||||
/** White Button Styles */
|
/** White Button Styles */
|
||||||
.generic-button.white {
|
.generic-button.white {
|
||||||
@@ -109,10 +106,9 @@
|
|||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
/* White text styles */
|
|
||||||
.generic-button.white span {
|
.generic-button.white .generic-button__content {
|
||||||
/* Auto layout */
|
min-height: 100%;
|
||||||
height: 18px;
|
|
||||||
font-family: 'Arial';
|
font-family: 'Arial';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -121,9 +117,6 @@
|
|||||||
color: #000000;
|
color: #000000;
|
||||||
|
|
||||||
/* Inside auto layout */
|
/* Inside auto layout */
|
||||||
flex: none;
|
|
||||||
order: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
}
|
||||||
.generic-button.white:disabled {
|
.generic-button.white:disabled {
|
||||||
/* Category */
|
/* Category */
|
||||||
@@ -152,12 +145,12 @@
|
|||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
/* Disabled white text styles */
|
/* Disabled white text styles */
|
||||||
.generic-button.white:disabled span {
|
.generic-button.white:disabled .generic-button__content {
|
||||||
/* Uncategorized */
|
/* Uncategorized */
|
||||||
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 12px;
|
min-height: 100%;
|
||||||
|
|
||||||
/* sm-tx */
|
/* sm-tx */
|
||||||
font-family: 'Arial';
|
font-family: 'Arial';
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ test.describe('Admin POS Orders - desktop settings', () => {
|
|||||||
await expect(reg3Add).toBeVisible();
|
await expect(reg3Add).toBeVisible();
|
||||||
const noteEmptyState = page.getByTestId('pos-order-note-empty-state');
|
const noteEmptyState = page.getByTestId('pos-order-note-empty-state');
|
||||||
await expect(noteEmptyState).toBeVisible();
|
await expect(noteEmptyState).toBeVisible();
|
||||||
|
await expect(noteEmptyState.locator('.pos-order-field__empty-pill')).toHaveText(/^\+\s+\S+/);
|
||||||
await expect(noteEmptyState).not.toContainText(/Ingen data/i);
|
await expect(noteEmptyState).not.toContainText(/Ingen data/i);
|
||||||
await expect(noteEmptyState.locator('.pos-order-note-editor__empty-copy')).toHaveCount(0);
|
await expect(noteEmptyState.locator('.pos-order-note-editor__empty-copy')).toHaveCount(0);
|
||||||
|
|
||||||
@@ -714,6 +715,10 @@ test.describe('Admin POS Orders - mobile smoke', () => {
|
|||||||
test('keeps horizontal gutters around the mobile order tabs', async ({ page }) => {
|
test('keeps horizontal gutters around the mobile order tabs', async ({ page }) => {
|
||||||
await openOrderDetail(page);
|
await openOrderDetail(page);
|
||||||
|
|
||||||
|
const noteEmptyState = page.getByTestId('pos-order-note-empty-state');
|
||||||
|
await expect(noteEmptyState).toBeVisible();
|
||||||
|
await expect(noteEmptyState.locator('.pos-order-field__empty-pill')).toHaveText(/^\+\s+\S+/);
|
||||||
|
|
||||||
const tabsContainer = getVisibleTestId(page, 'pos-order-mobile-tabs');
|
const tabsContainer = getVisibleTestId(page, 'pos-order-mobile-tabs');
|
||||||
const cartTab = getVisibleTestId(page, 'pos-order-tab-cart');
|
const cartTab = getVisibleTestId(page, 'pos-order-tab-cart');
|
||||||
const containerBox = await tabsContainer.boundingBox();
|
const containerBox = await tabsContainer.boundingBox();
|
||||||
@@ -745,6 +750,35 @@ test.describe('Admin POS Orders - mobile smoke', () => {
|
|||||||
await expect(page.getByTestId('pos-order-item-delete-9101').first()).toBeVisible();
|
await expect(page.getByTestId('pos-order-item-delete-9101').first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('keeps the mobile search and reload controls inline and shows a clear card boundary', async ({ page }) => {
|
||||||
|
await page.goto('/admin/12/modules/pos/orders');
|
||||||
|
|
||||||
|
const searchInput = page.getByTestId('pagination-search-input');
|
||||||
|
const reloadActions = page.getByTestId('pagination-reload-actions');
|
||||||
|
const orderCard = page.getByTestId('pos-order-list-card-54518');
|
||||||
|
|
||||||
|
await expect(searchInput).toBeVisible();
|
||||||
|
await expect(reloadActions).toBeVisible();
|
||||||
|
await expect(orderCard).toBeVisible();
|
||||||
|
|
||||||
|
const searchBox = await searchInput.boundingBox();
|
||||||
|
const reloadBox = await reloadActions.boundingBox();
|
||||||
|
|
||||||
|
expect(searchBox).not.toBeNull();
|
||||||
|
expect(reloadBox).not.toBeNull();
|
||||||
|
const searchCenterY = (searchBox?.y ?? 0) + ((searchBox?.height ?? 0) / 2);
|
||||||
|
const reloadCenterY = (reloadBox?.y ?? 0) + ((reloadBox?.height ?? 0) / 2);
|
||||||
|
expect(Math.abs(searchCenterY - reloadCenterY)).toBeLessThanOrEqual(4);
|
||||||
|
expect((searchBox?.x ?? 0)).toBeLessThan((reloadBox?.x ?? 0));
|
||||||
|
expect(((searchBox?.x ?? 0) + (searchBox?.width ?? 0)) - (reloadBox?.x ?? 0)).toBeLessThanOrEqual(4);
|
||||||
|
expect((searchBox?.width ?? 0)).toBeGreaterThan(reloadBox?.width ?? 0);
|
||||||
|
|
||||||
|
await expect(orderCard).toHaveCSS('border-top-width', '1px');
|
||||||
|
await expect(orderCard).toHaveCSS('border-right-width', '1px');
|
||||||
|
await expect(orderCard).toHaveCSS('border-bottom-width', '1px');
|
||||||
|
await expect(orderCard).toHaveCSS('border-left-width', '1px');
|
||||||
|
});
|
||||||
|
|
||||||
test('stacks the add-items workspace on mobile and still lets the user add products', async ({ page }) => {
|
test('stacks the add-items workspace on mobile and still lets the user add products', async ({ page }) => {
|
||||||
await openOrderDetail(page);
|
await openOrderDetail(page);
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,54 @@ test.describe("POS mobile order flow", () => {
|
|||||||
await expect(page.getByTestId("pos-mobile-customer-popup")).toBeVisible({ timeout: 10_000 });
|
await expect(page.getByTestId("pos-mobile-customer-popup")).toBeVisible({ timeout: 10_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("customer notes popup shows the translated created-by label", async ({ page }) => {
|
||||||
|
const fixture = createMobilePosFixture({
|
||||||
|
customerNotesByNumber: {
|
||||||
|
[REGULAR_CUSTOMER_ID]: [
|
||||||
|
{
|
||||||
|
id: 401,
|
||||||
|
note: "test123",
|
||||||
|
created_at: "2026-04-09T06:17:00.000Z",
|
||||||
|
cashier_id: 7,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
employees: [
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
display_name: "Jeppe",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await setupMobilePosPage(page, fixture, {
|
||||||
|
token: "mobile-customer-notes-token",
|
||||||
|
seedState: {
|
||||||
|
customerId: REGULAR_CUSTOMER_ID,
|
||||||
|
reg: "AB12345",
|
||||||
|
reference: "NOTES-REF",
|
||||||
|
includePrimaryItem: false,
|
||||||
|
vehicleType: null,
|
||||||
|
lastOrderId: null,
|
||||||
|
},
|
||||||
|
route: {
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByTestId("pos-mobile-customer-notes-trigger")).toBeVisible({ timeout: 10_000 });
|
||||||
|
await page.getByTestId("pos-mobile-customer-notes-trigger").click();
|
||||||
|
|
||||||
|
const popup = page.locator('[data-testid="pos-mobile-popup"][data-popup-id="customer_notes"]');
|
||||||
|
await expect(popup).toBeVisible({ timeout: 10_000 });
|
||||||
|
await expect(page.getByTestId("pos-mobile-customer-note-created-by-401")).toContainText("Oprettet af Jeppe");
|
||||||
|
await expect(page.getByTestId("pos-mobile-customer-note-created-by-401")).not.toContainText("pos.created_by");
|
||||||
|
|
||||||
|
await popup.getByText("Tilføj note", { exact: true }).click();
|
||||||
|
await expect(page.locator("#input_field")).toBeVisible({ timeout: 10_000 });
|
||||||
|
await expect(page.locator("#input_field")).toHaveAttribute("placeholder", "Tilføj note...");
|
||||||
|
});
|
||||||
|
|
||||||
test("@smoke manual input happy path creates, completes, and resets", async ({ page }) => {
|
test("@smoke manual input happy path creates, completes, and resets", async ({ page }) => {
|
||||||
const fixture = createMobilePosFixture();
|
const fixture = createMobilePosFixture();
|
||||||
await createOrderFromStep1(page, fixture, {
|
await createOrderFromStep1(page, fixture, {
|
||||||
@@ -677,6 +725,70 @@ test.describe("POS mobile order flow", () => {
|
|||||||
await waitForStepReset(page);
|
await waitForStepReset(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("booking completion prompts when a safety seal is added to the basket", async ({ page }) => {
|
||||||
|
const orderId = 9409;
|
||||||
|
const fixture = createMobilePosFixture({
|
||||||
|
ordersById: {
|
||||||
|
[orderId]: buildRegularOrder(orderId, {
|
||||||
|
reg_1: "BOOK123",
|
||||||
|
reference: "",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
orderItemsByOrderId: {
|
||||||
|
[orderId]: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await setupMobilePosPage(page, fixture, {
|
||||||
|
token: "mobile-booking-basket-seal-token",
|
||||||
|
seedState: {
|
||||||
|
customerId: REGULAR_CUSTOMER_ID,
|
||||||
|
reg: "BOOK123",
|
||||||
|
reference: "",
|
||||||
|
includePrimaryItem: false,
|
||||||
|
vehicleType: null,
|
||||||
|
bookingId: DEFAULT_BOOKING_ID,
|
||||||
|
vehicleStatus: "booked",
|
||||||
|
lastOrderId: null,
|
||||||
|
},
|
||||||
|
route: {
|
||||||
|
step: 2,
|
||||||
|
orderId,
|
||||||
|
customerId: REGULAR_CUSTOMER_ID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
||||||
|
await waitForBookingHydration(page, {
|
||||||
|
primaryId: 53,
|
||||||
|
addonProductIds: [71],
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("pos-mobile-additional-items-open").click();
|
||||||
|
await expect(page.getByTestId("pos-mobile-additional-items-selection")).toBeVisible({ timeout: 10_000 });
|
||||||
|
await page.getByTestId("pos-mobile-category-8").click();
|
||||||
|
await page.waitForTimeout(2_000);
|
||||||
|
await page.getByTestId("pos-mobile-product-41").click();
|
||||||
|
await expect.poll(async () => {
|
||||||
|
const snapshot = await getStoredPosSnapshot(page);
|
||||||
|
return (snapshot?.transactionItems?.additionalItems || []).map((item) => Number(item?.id)).sort((left, right) => left - right);
|
||||||
|
}, { timeout: 10_000 }).toEqual([41]);
|
||||||
|
|
||||||
|
await page.getByTestId("pos-mobile-next-step").click();
|
||||||
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await page.getByTestId("pos-mobile-next-step").click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId("pos-mobile-popup")).toBeVisible({ timeout: 10_000 });
|
||||||
|
await page.getByTestId("pos-mobile-booking-safety-seal-input").fill("9090");
|
||||||
|
await page.getByTestId("pos-mobile-booking-complete-with-certificate").click();
|
||||||
|
|
||||||
|
await expect.poll(() => fixture.requestCounters.bookingComplete, { timeout: 10_000 }).toBe(1);
|
||||||
|
await expect.poll(() => fixture.bookingsById[DEFAULT_BOOKING_ID]?.status ?? "").toBe("completed");
|
||||||
|
await expect.poll(() => String(fixture.bookingsById[DEFAULT_BOOKING_ID]?.safety_seal ?? "")).toBe("9090");
|
||||||
|
await waitForStepReset(page);
|
||||||
|
});
|
||||||
|
|
||||||
test("booking completion with safety seal popup submits the explicit seal", async ({ page }) => {
|
test("booking completion with safety seal popup submits the explicit seal", async ({ page }) => {
|
||||||
const orderId = 9408;
|
const orderId = 9408;
|
||||||
const fixture = createMobilePosFixture({
|
const fixture = createMobilePosFixture({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
import { createPosFixture, mockApi, seedAuthenticatedState } from "./support/network.js";
|
import { createPosFixture, mockApi, seedAuthenticatedState } from "./support/network.js";
|
||||||
|
import { createMobilePosFixture, REGULAR_CUSTOMER_ID, setupMobilePosPage } from "./support/mobilePos.js";
|
||||||
|
|
||||||
const POS_PERMISSIONS = [
|
const POS_PERMISSIONS = [
|
||||||
"admin",
|
"admin",
|
||||||
@@ -475,6 +476,51 @@ test.describe("POS visuals", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("mobile step 1 selected customer CTA snapshot", async ({ page }, testInfo) => {
|
||||||
|
test.skip(!testInfo.project.name.includes("mobile"), "Mobile only");
|
||||||
|
|
||||||
|
const longCustomerName = "(TEST) Pleno Vognmandsforretning";
|
||||||
|
const fixture = createMobilePosFixture({
|
||||||
|
customersByNumber: {
|
||||||
|
[REGULAR_CUSTOMER_ID]: {
|
||||||
|
id: REGULAR_CUSTOMER_ID,
|
||||||
|
customerNumber: REGULAR_CUSTOMER_ID,
|
||||||
|
name: longCustomerName,
|
||||||
|
address: "Demo Street 1",
|
||||||
|
zip: "2630",
|
||||||
|
city: "Taastrup",
|
||||||
|
mobilePhone: "12345678",
|
||||||
|
email: "pos-mobile@example.com",
|
||||||
|
corporateIdentificationNumber: "00012345",
|
||||||
|
economic_customer: REGULAR_CUSTOMER_ID,
|
||||||
|
barred: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await setupMobilePosPage(page, fixture, {
|
||||||
|
token: "pos-visual-mobile-step-1-customer-token",
|
||||||
|
permissions: ["admin", "department_access_1"],
|
||||||
|
seedState: {
|
||||||
|
customerId: REGULAR_CUSTOMER_ID,
|
||||||
|
reg: "EC21235",
|
||||||
|
reference: "EC21233 - Test Ref. / Intern nummer",
|
||||||
|
includePrimaryItem: false,
|
||||||
|
vehicleType: null,
|
||||||
|
lastOrderId: null,
|
||||||
|
},
|
||||||
|
route: {
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByTestId("pos-mobile-step-1")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("pos-mobile-next-step")).toContainText(longCustomerName);
|
||||||
|
await expect(page.getByTestId("pos-mobile-fixed-actions")).toHaveScreenshot("pos-mobile-step-1-fixed-actions.png", {
|
||||||
|
maxDiffPixels: 300,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("mobile order detail add-items workspace snapshot", async ({ page }, testInfo) => {
|
test("mobile order detail add-items workspace snapshot", async ({ page }, testInfo) => {
|
||||||
test.skip(!testInfo.project.name.includes("mobile"), "Mobile only");
|
test.skip(!testInfo.project.name.includes("mobile"), "Mobile only");
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
@@ -408,6 +408,12 @@ function buildDefaultFixture() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
employees: [
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
display_name: "Jeppe",
|
||||||
|
},
|
||||||
|
],
|
||||||
products,
|
products,
|
||||||
departmentCategories: [
|
departmentCategories: [
|
||||||
{
|
{
|
||||||
@@ -563,6 +569,7 @@ function normalizeFixture(fixture) {
|
|||||||
fixture.vehicles = Array.isArray(fixture.vehicles) ? fixture.vehicles : [];
|
fixture.vehicles = Array.isArray(fixture.vehicles) ? fixture.vehicles : [];
|
||||||
fixture.departments = Array.isArray(fixture.departments) ? fixture.departments : [];
|
fixture.departments = Array.isArray(fixture.departments) ? fixture.departments : [];
|
||||||
fixture.departmentCategories = Array.isArray(fixture.departmentCategories) ? fixture.departmentCategories : [];
|
fixture.departmentCategories = Array.isArray(fixture.departmentCategories) ? fixture.departmentCategories : [];
|
||||||
|
fixture.employees = Array.isArray(fixture.employees) ? fixture.employees : [];
|
||||||
fixture.requestCounters = createRequestCounters(fixture.requestCounters);
|
fixture.requestCounters = createRequestCounters(fixture.requestCounters);
|
||||||
fixture.requestLog = createRequestLog(fixture.requestLog);
|
fixture.requestLog = createRequestLog(fixture.requestLog);
|
||||||
fixture.failureBudget = createFailureBudget(fixture.failureBudget);
|
fixture.failureBudget = createFailureBudget(fixture.failureBudget);
|
||||||
@@ -600,6 +607,7 @@ export function createMobilePosFixture(overrides = {}) {
|
|||||||
departments: overrides.departments ? clone(overrides.departments) : clone(base.departments),
|
departments: overrides.departments ? clone(overrides.departments) : clone(base.departments),
|
||||||
products: overrides.products ? clone(overrides.products) : clone(base.products),
|
products: overrides.products ? clone(overrides.products) : clone(base.products),
|
||||||
departmentCategories: overrides.departmentCategories ? clone(overrides.departmentCategories) : clone(base.departmentCategories),
|
departmentCategories: overrides.departmentCategories ? clone(overrides.departmentCategories) : clone(base.departmentCategories),
|
||||||
|
employees: overrides.employees ? clone(overrides.employees) : clone(base.employees),
|
||||||
vehicles: overrides.vehicles ? clone(overrides.vehicles) : clone(base.vehicles),
|
vehicles: overrides.vehicles ? clone(overrides.vehicles) : clone(base.vehicles),
|
||||||
unknownVehicles: overrides.unknownVehicles ? clone(overrides.unknownVehicles) : clone(base.unknownVehicles),
|
unknownVehicles: overrides.unknownVehicles ? clone(overrides.unknownVehicles) : clone(base.unknownVehicles),
|
||||||
failureBudget: createFailureBudget(overrides.failureBudget),
|
failureBudget: createFailureBudget(overrides.failureBudget),
|
||||||
@@ -1203,6 +1211,14 @@ export async function mockMobilePosApi(page, fixture) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pathname.endsWith("/public/employees") && method === "GET") {
|
||||||
|
await route.fulfill(json({
|
||||||
|
success: true,
|
||||||
|
data: clone(fixture.employees || []),
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (pathname.endsWith("/customer/attributes") && method === "GET") {
|
if (pathname.endsWith("/customer/attributes") && method === "GET") {
|
||||||
recordCounter(fixture, "customerAttributesGet");
|
recordCounter(fixture, "customerAttributesGet");
|
||||||
const customerNumber = toPositiveInteger(parsedUrl.searchParams.get("customer_number"))
|
const customerNumber = toPositiveInteger(parsedUrl.searchParams.get("customer_number"))
|
||||||
|
|||||||
Reference in New Issue
Block a user