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()"
|
||||
>
|
||||
<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 class="pos-order-field__empty-pill">{{ t('pos.add_note') }}</span>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1215,7 +1215,16 @@ const formatCashierName = (order) => {
|
||||
<!-- Orders as cards -->
|
||||
<template v-for="order in getVisibleOrders" :key="order.id">
|
||||
<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>
|
||||
<div class="card-header-title is-flex is-justify-content-space-between is-align-items-center" style="width: 100%;">
|
||||
<div>
|
||||
@@ -1541,4 +1550,30 @@ const formatCashierName = (order) => {
|
||||
.status-bar.has-text-success {
|
||||
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>
|
||||
|
||||
@@ -244,6 +244,10 @@ const applyPendingBookingFromSelection = async () => {
|
||||
const preparedAddons = addonProducts
|
||||
.filter(p => !!p)
|
||||
.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;
|
||||
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
|
||||
const mapAddonsWithQuantity = (sourceAddons = [], previousAddons = []) =>
|
||||
sourceAddons.map(addon => {
|
||||
const carriedQty = previousAddons.find(a => a.product.id === addon.product.id)?.quantity ?? 0;
|
||||
return { ...addon, quantity: carriedQty };
|
||||
const previousAddon = previousAddons.find(a => a.product.id === addon.product.id);
|
||||
const nextQuantity = previousAddon?.quantity ?? addon?.quantity ?? addon?.product?.quantity ?? 0;
|
||||
return {
|
||||
...addon,
|
||||
quantity: nextQuantity,
|
||||
product: addon?.product ? {
|
||||
...addon.product,
|
||||
quantity: nextQuantity,
|
||||
} : addon?.product,
|
||||
};
|
||||
});
|
||||
|
||||
watch(
|
||||
|
||||
+2
-2
@@ -104,7 +104,7 @@ watch(() => metadata.getCustomerId(), (newCustomerId) => {
|
||||
</p>
|
||||
</div>
|
||||
<!-- 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>
|
||||
<!-- If there are notes, show a warning icon -->
|
||||
<span v-if="customerNotes.length > 0" class="icon is-small has-text-dark pr-1">
|
||||
@@ -153,4 +153,4 @@ watch(() => metadata.getCustomerId(), (newCustomerId) => {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
+24
-21
@@ -7,7 +7,7 @@ import { errors } from "@/components/request/HandleGlobalError.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.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 LongPressListener from "@/components/viewport/elements/wrappers/LongPressListener.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
|
||||
if (metadata.getBookingId() && metadata.getBookingId() > 0) {
|
||||
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 {
|
||||
const posStr = localStorage.getItem('pos');
|
||||
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);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const hasSeal = transactionItems.containsWashCertificate();
|
||||
if (!hasSeal) {
|
||||
metadata.setLoadingState(true);
|
||||
metadata.setLoadingMessage(t('admin.pos.waiting_for_booking'));
|
||||
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();
|
||||
});
|
||||
} else {
|
||||
@@ -279,7 +263,7 @@ const step2 = () => {
|
||||
});
|
||||
}
|
||||
} 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', {});
|
||||
awaitBookingCompletion(() => {
|
||||
completeOrder();
|
||||
@@ -549,12 +533,19 @@ const defaultLongPressBehavior = () => {
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pos-mobile-cta-label {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: -webkit-box;
|
||||
text-align: left;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.pos-mobile-cta-value {
|
||||
@@ -574,12 +565,19 @@ const defaultLongPressBehavior = () => {
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.pos-mobile-cta-label) {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: -webkit-box;
|
||||
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) {
|
||||
@@ -588,6 +586,11 @@ const defaultLongPressBehavior = () => {
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.generic-button__content) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
/* Loading state */
|
||||
.is-loading {
|
||||
pointer-events: none;
|
||||
|
||||
+6
-3
@@ -147,7 +147,10 @@ watch(() => props.showInput, (newVal) => {
|
||||
<br />
|
||||
<small>{{ SessionUser.functions.ucFirst(SessionUser.functions.date.toWordsWithTime(note?.created_at || new Date())) }}</small>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,7 +161,7 @@ watch(() => props.showInput, (newVal) => {
|
||||
<input
|
||||
class="input has-sharp-edges"
|
||||
type="text"
|
||||
:placeholder="t('pos.add_note_placeholder')"
|
||||
:placeholder="t('admin.pos.add_note_placeholder')"
|
||||
v-model="input"
|
||||
id="input_field"
|
||||
@keyup.enter="createNewNote(input); input=''"
|
||||
@@ -226,4 +229,4 @@ watch(() => props.showInput, (newVal) => {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -64,9 +64,10 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<!-- Top row: Reload, Search -->
|
||||
<div class="columns is-vcentered is-multiline">
|
||||
<div class="column">
|
||||
<div class="columns is-vcentered is-multiline pagination-general-search-reload">
|
||||
<div class="column pagination-general-search-reload__search-column">
|
||||
<input
|
||||
data-testid="pagination-search-input"
|
||||
class="input"
|
||||
type="text"
|
||||
:placeholder="$t('global.search_placeholder')"
|
||||
@@ -74,12 +75,17 @@ onBeforeUnmount(() => {
|
||||
@input="search($event.target.value)"
|
||||
/>
|
||||
</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 -->
|
||||
<slot name="buttons" :loadList="loadList"></slot>
|
||||
</div>
|
||||
<div class="column is-narrow pl-0">
|
||||
<div ref="actionDropdownRef" class="dropdown is-right" :class="{ 'is-active': isActionDropdownOpen }">
|
||||
<div class="column is-narrow pl-0 pagination-general-search-reload__action-column">
|
||||
<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="buttons has-addons action-dropdown-trigger">
|
||||
<button
|
||||
@@ -134,13 +140,92 @@ onBeforeUnmount(() => {
|
||||
.action-dropdown-trigger {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pagination-general-search-reload__dropdown {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-dropdown-item {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item.is-disabled {
|
||||
opacity: 0.5;
|
||||
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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<template>
|
||||
<button class="generic-button">
|
||||
<span class="is-flex-wrap-nowrap" style="width: 90%;">
|
||||
<span class="generic-button__content">
|
||||
<slot />
|
||||
</span>
|
||||
</button>
|
||||
@@ -33,10 +33,14 @@
|
||||
align-self: stretch;
|
||||
flex-grow: 0;
|
||||
}
|
||||
/* Default text styles */
|
||||
.generic-button span {
|
||||
/* Auto layout */
|
||||
height: 18px;
|
||||
|
||||
.generic-button__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 100%;
|
||||
font-family: 'Arial';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
@@ -45,9 +49,6 @@
|
||||
color: #FFFFFF;
|
||||
|
||||
/* Inside auto layout */
|
||||
flex: none;
|
||||
order: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
/* Disabled styles */
|
||||
.generic-button:disabled {
|
||||
@@ -71,10 +72,9 @@
|
||||
align-self: stretch;
|
||||
flex-grow: 0;
|
||||
}
|
||||
/* Disabled text styles */
|
||||
.generic-button:disabled span {
|
||||
/* Auto layout */
|
||||
height: 18px;
|
||||
|
||||
.generic-button:disabled .generic-button__content {
|
||||
min-height: 100%;
|
||||
font-family: 'Arial';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
@@ -83,9 +83,6 @@
|
||||
color: #FFFFFF;
|
||||
|
||||
/* Inside auto layout */
|
||||
flex: none;
|
||||
order: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
/** White Button Styles */
|
||||
.generic-button.white {
|
||||
@@ -109,10 +106,9 @@
|
||||
align-self: stretch;
|
||||
flex-grow: 0;
|
||||
}
|
||||
/* White text styles */
|
||||
.generic-button.white span {
|
||||
/* Auto layout */
|
||||
height: 18px;
|
||||
|
||||
.generic-button.white .generic-button__content {
|
||||
min-height: 100%;
|
||||
font-family: 'Arial';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
@@ -121,9 +117,6 @@
|
||||
color: #000000;
|
||||
|
||||
/* Inside auto layout */
|
||||
flex: none;
|
||||
order: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.generic-button.white:disabled {
|
||||
/* Category */
|
||||
@@ -152,12 +145,12 @@
|
||||
background: none;
|
||||
}
|
||||
/* Disabled white text styles */
|
||||
.generic-button.white:disabled span {
|
||||
.generic-button.white:disabled .generic-button__content {
|
||||
/* Uncategorized */
|
||||
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
min-height: 100%;
|
||||
|
||||
/* sm-tx */
|
||||
font-family: 'Arial';
|
||||
@@ -176,4 +169,4 @@
|
||||
flex-grow: 0;
|
||||
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -252,6 +252,7 @@ test.describe('Admin POS Orders - desktop settings', () => {
|
||||
await expect(reg3Add).toBeVisible();
|
||||
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+/);
|
||||
await expect(noteEmptyState).not.toContainText(/Ingen data/i);
|
||||
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 }) => {
|
||||
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 cartTab = getVisibleTestId(page, 'pos-order-tab-cart');
|
||||
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();
|
||||
});
|
||||
|
||||
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 }) => {
|
||||
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 });
|
||||
});
|
||||
|
||||
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 }) => {
|
||||
const fixture = createMobilePosFixture();
|
||||
await createOrderFromStep1(page, fixture, {
|
||||
@@ -677,6 +725,70 @@ test.describe("POS mobile order flow", () => {
|
||||
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 }) => {
|
||||
const orderId = 9408;
|
||||
const fixture = createMobilePosFixture({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { createPosFixture, mockApi, seedAuthenticatedState } from "./support/network.js";
|
||||
import { createMobilePosFixture, REGULAR_CUSTOMER_ID, setupMobilePosPage } from "./support/mobilePos.js";
|
||||
|
||||
const POS_PERMISSIONS = [
|
||||
"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.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,
|
||||
departmentCategories: [
|
||||
{
|
||||
@@ -563,6 +569,7 @@ function normalizeFixture(fixture) {
|
||||
fixture.vehicles = Array.isArray(fixture.vehicles) ? fixture.vehicles : [];
|
||||
fixture.departments = Array.isArray(fixture.departments) ? fixture.departments : [];
|
||||
fixture.departmentCategories = Array.isArray(fixture.departmentCategories) ? fixture.departmentCategories : [];
|
||||
fixture.employees = Array.isArray(fixture.employees) ? fixture.employees : [];
|
||||
fixture.requestCounters = createRequestCounters(fixture.requestCounters);
|
||||
fixture.requestLog = createRequestLog(fixture.requestLog);
|
||||
fixture.failureBudget = createFailureBudget(fixture.failureBudget);
|
||||
@@ -600,6 +607,7 @@ export function createMobilePosFixture(overrides = {}) {
|
||||
departments: overrides.departments ? clone(overrides.departments) : clone(base.departments),
|
||||
products: overrides.products ? clone(overrides.products) : clone(base.products),
|
||||
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),
|
||||
unknownVehicles: overrides.unknownVehicles ? clone(overrides.unknownVehicles) : clone(base.unknownVehicles),
|
||||
failureBudget: createFailureBudget(overrides.failureBudget),
|
||||
@@ -1203,6 +1211,14 @@ export async function mockMobilePosApi(page, fixture) {
|
||||
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") {
|
||||
recordCounter(fixture, "customerAttributesGet");
|
||||
const customerNumber = toPositiveInteger(parsedUrl.searchParams.get("customer_number"))
|
||||
|
||||
Reference in New Issue
Block a user