Revise QR code generation library, enhance POS flow, and update Edge Gateway management views and tests

This commit is contained in:
Jeppe Bundgaard
2026-04-23 10:48:08 +02:00
parent fc26e12013
commit 50190da52f
14 changed files with 16884 additions and 68 deletions
+16338
View File
File diff suppressed because it is too large Load Diff
+234
View File
@@ -758,6 +758,236 @@
background: #fff0f0;
}
.pos-step-one-insights {
display: grid;
gap: 0.9rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
}
.pos-step-one-insight-card {
display: flex;
flex-direction: column;
gap: 0.95rem;
min-width: 0;
min-height: 100%;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 1rem;
background: linear-gradient(180deg, #ffffff 0%, #fafbfd 100%);
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
}
.pos-step-one-insight-card__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.85rem;
}
.pos-step-one-insight-card__heading {
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.pos-step-one-insight-card__eyebrow {
margin: 0;
color: #738397;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.pos-step-one-insight-card__title {
margin: 0;
color: #20344f;
font-size: 1.05rem;
font-weight: 700;
line-height: 1.2;
}
.pos-step-one-insight-card__subtitle {
margin: 0;
color: #66768b;
font-size: 0.9rem;
line-height: 1.35;
}
.pos-step-one-insight-card__pills {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.45rem;
}
.pos-step-one-insight-pill {
display: inline-flex;
align-items: center;
min-height: 2rem;
padding: 0.35rem 0.7rem;
border: 1px solid transparent;
border-radius: 999px;
font-size: 0.76rem;
font-weight: 700;
line-height: 1.2;
white-space: nowrap;
}
.pos-step-one-insight-pill--success {
border-color: #cae7d2;
background: #effbf2;
color: #1d6b36;
}
.pos-step-one-insight-pill--warning {
border-color: #ecd89e;
background: #fff8df;
color: #906100;
}
.pos-step-one-insight-pill--danger {
border-color: #edc9c9;
background: #fff1f1;
color: #a23636;
}
.pos-step-one-insight-pill--booking {
border-color: #c8d8f5;
background: #eef4ff;
color: #21446d;
}
.pos-step-one-insight-pill--muted {
border-color: #d9e1eb;
background: #f5f7fa;
color: #526274;
}
.pos-step-one-insight-meta {
display: grid;
gap: 0.65rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.pos-step-one-insight-meta--compact {
gap: 0.55rem;
}
.pos-step-one-insight-meta__item {
min-width: 0;
padding: 0.72rem 0.82rem;
border: 1px solid #e7edf5;
border-radius: 0.85rem;
background: #ffffff;
}
.pos-step-one-insight-meta__label {
margin: 0 0 0.3rem;
color: #738397;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.pos-step-one-insight-meta__value {
margin: 0;
color: #243347;
font-size: 0.92rem;
font-weight: 600;
line-height: 1.35;
word-break: break-word;
}
.pos-step-one-insight-card__section {
display: flex;
flex-direction: column;
gap: 0.55rem;
min-width: 0;
}
.pos-step-one-insight-card__section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.pos-step-one-insight-card__section-title {
margin: 0;
color: #34455c;
font-size: 0.82rem;
font-weight: 700;
}
.pos-step-one-insight-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 0;
padding: 0;
list-style: none;
}
.pos-step-one-insight-list__item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.72rem 0.82rem;
border: 1px solid #e7edf5;
border-radius: 0.85rem;
background: #ffffff;
}
.pos-step-one-insight-list__item--addon {
background: #fbfcfe;
}
.pos-step-one-insight-list__label {
min-width: 0;
color: #243347;
font-size: 0.92rem;
font-weight: 600;
line-height: 1.35;
word-break: break-word;
}
.pos-step-one-insight-list__prefix {
margin-right: 0.35rem;
color: #738397;
}
.pos-step-one-insight-list__quantity {
flex: 0 0 auto;
color: #5b6c81;
font-size: 0.82rem;
font-weight: 700;
}
.pos-step-one-empty-state,
.pos-step-one-loading-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 4rem;
padding: 0.85rem;
border: 1px dashed #d8e1eb;
border-radius: 0.85rem;
background: #fbfcfe;
color: #63758a;
font-size: 0.9rem;
line-height: 1.4;
text-align: center;
}
.pos-step-one-copy-action {
align-self: flex-start;
margin-top: auto;
}
@media screen and (max-width: 1080px) {
.pos-order-items--order-detail .pos-order-metadata-grid {
grid-template-columns: minmax(0, 1fr);
@@ -787,4 +1017,8 @@
.pos-registration-grid {
grid-template-columns: minmax(0, 1fr);
}
.pos-step-one-insight-meta {
grid-template-columns: minmax(0, 1fr);
}
}
@@ -35,6 +35,8 @@ import ExpandableContentBox from "@/components/displays/boxes/ExpandableContentB
import OrderContentTable from "@/components/displays/superuser/tables/OrderContentTable.vue";
import VehicleCustomerSuggestionsPos from "@/components/forms/department/pos/input/vehicleCustomerSuggestionsPos.vue";
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import PosDesktopVehicleSummaryCard from "@/components/displays/department/pos/steps/elements/PosDesktopVehicleSummaryCard.vue";
import PosDesktopLastWashCard from "@/components/displays/department/pos/steps/elements/PosDesktopLastWashCard.vue";
const { t } = useI18n();
const emit = defineEmits(["update:desktopStep1Context", "commit:desktopStep1"]);
@@ -752,8 +754,11 @@ const showExistingNotes = computed(() => Array.isArray(notes.value) && notes.val
const hasVehicleCustomer = computed(() => Boolean(vehicleObject.value?.customer_id));
const shouldShowInlineCustomerSelector = computed(() => reg_1.value.length > 0 && !hasVehicleCustomer.value);
const shouldShowExpandedCustomerSelector = computed(() => hasVehicleCustomer.value);
const hasSecondaryContent = computed(() => {
const hasDesktopInsights = computed(() => Boolean(vehicleObject.value));
const hasDesktopSecondaryContent = computed(() => {
return Boolean(shouldShowExpandedCustomerSelector.value || isCustomerSelected());
});
const hasLegacyMobileExpandableContent = computed(() => {
return Boolean(vehicleObject.value || isCustomerSelected());
});
@@ -1096,9 +1101,67 @@ defineExpose({
:isOldNotesVisible="true"
class="pos-vehicle-form__notes"
/>
<div
v-if="hasDesktopInsights"
class="pos-vehicle-form__desktop-insights is-hidden-touch"
>
<div class="pos-step-one-insights">
<PosDesktopVehicleSummaryCard :vehicle="vehicleObject" />
<PosDesktopLastWashCard
v-if="vehicleObject?.last_order_id"
:order-id="vehicleObject.last_order_id"
/>
</div>
</div>
<section
v-if="hasDesktopSecondaryContent"
class="pos-vehicle-form__secondary-block is-hidden-touch"
data-testid="pos-desktop-secondary-block"
>
<div class="pos-vehicle-form__secondary-header">
<div class="pos-vehicle-form__secondary-heading">
<p class="pos-vehicle-form__secondary-eyebrow">{{ SessionUser.objects.global.language.other }}</p>
<h3 class="pos-vehicle-form__secondary-title">{{ SessionUser.objects.global.language.other }}</h3>
<p class="pos-vehicle-form__secondary-subtitle">Kundevalg og noter</p>
</div>
<button
type="button"
class="button is-small is-light pos-vehicle-form__secondary-toggle"
@click="isOtherExpanded = !isOtherExpanded"
>
<span>{{ isOtherExpanded ? SessionUser.objects.global.language.hide : SessionUser.objects.global.language.show }}</span>
<span class="icon is-small">
<i
class="fas fa-angle-down"
:class="{ 'fa-rotate-180': isOtherExpanded }"
aria-hidden="true"
></i>
</span>
</button>
</div>
<div v-if="isOtherExpanded" class="pos-vehicle-form__secondary-content">
<CustomerSearchFieldPos
v-if="shouldShowExpandedCustomerSelector"
@onCustomerSelected="handleManualCustomerSelection"
/>
<PosNotes
v-if="isCustomerSelected()"
:notes="notes"
:is-loading="false"
:isAddFormVisible="SessionUser.adminUser"
:isOldNotesVisible="false"
:is-label-visible="true"
class="mt-3"
/>
</div>
</section>
<ExpandableContentBox
v-if="hasSecondaryContent"
class="pos-vehicle-form__expandable"
v-if="hasLegacyMobileExpandableContent"
class="pos-vehicle-form__expandable is-hidden-desktop"
:expanded="isOtherExpanded"
@update:expanded="isOtherExpanded = $event"
>
@@ -1278,6 +1341,66 @@ defineExpose({
margin-bottom: 0;
}
.pos-vehicle-form__desktop-insights {
margin-top: 0.25rem;
}
.pos-vehicle-form__secondary-block {
display: flex;
flex-direction: column;
gap: 0.9rem;
padding: 1rem;
border: 1px solid #e6edf7;
border-radius: 1rem;
background: #fbfcfe;
}
.pos-vehicle-form__secondary-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.85rem;
}
.pos-vehicle-form__secondary-heading {
min-width: 0;
}
.pos-vehicle-form__secondary-eyebrow {
margin: 0 0 0.2rem;
color: #738397;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.pos-vehicle-form__secondary-title {
margin: 0;
color: #243446;
font-size: 1rem;
font-weight: 700;
line-height: 1.2;
}
.pos-vehicle-form__secondary-subtitle {
margin: 0.25rem 0 0;
color: #66768b;
font-size: 0.9rem;
line-height: 1.35;
}
.pos-vehicle-form__secondary-toggle {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.pos-vehicle-form__secondary-content {
min-width: 0;
}
.pos-vehicle-form__expandable {
border-top: 1px solid #e6edf7;
padding-top: 0.25rem;
@@ -24,7 +24,7 @@ import { Shelly } from "@/components/session/token/superUser/modules/shelly/Shel
import { SelfServe } from "@/components/session/token/superUser/modules/selfserve/SelfServe.vue";
import { Bird } from "@/components/session/token/superUser/modules/bird/Bird.vue";
import { EdgeGateway } from "@/components/session/token/superUser/modules/edgegateway/EdgeGateway.vue";
import { useQRCode } from '@vueuse/integrations/useQRCode';
import * as QRCode from "qrcode";
/**
@@ -116,7 +116,9 @@ export const SuperUserObject = {
* @returns {Promise<string>} The QR code URL
*/
showImpersonationQRCode: async (link) => {
return useQRCode(link);
return {
value: await QRCode.toDataURL(link),
};
},
/**
* Go back from intimidating a user
@@ -175,7 +175,7 @@ const deleteGateway = () => {
</script>
<template>
<section class="edge-gateway-manage" data-testid="gateway-manage-page">
<section class="edge-gateway-manage" data-testid="gateway-settings-page">
<article class="edge-gateway-manage__panel">
<p class="edge-gateway-manage__eyebrow">Metadata</p>
<h3>Gateway identity</h3>
@@ -19,7 +19,7 @@ import {
import { normalizeEdgeGatewayError } from "@/features/edgeGateways/edgeGatewayErrors.js";
import EdgeGatewayOverviewPage from "@/features/edgeGateways/EdgeGatewayOverviewPage.vue";
import EdgeGatewayInventoryPage from "@/features/edgeGateways/EdgeGatewayInventoryPage.vue";
import EdgeGatewayOperationsPage from "@/features/edgeGateways/EdgeGatewayOperationsPage.vue";
import EdgeGatewayTasksPage from "@/features/edgeGateways/EdgeGatewayTasksPage.vue";
import EdgeGatewayManagePage from "@/features/edgeGateways/EdgeGatewayManagePage.vue";
const props = defineProps({
@@ -104,22 +104,33 @@ const loading = ref({
let autoRefreshHandle = null;
let lastFleetRefreshAt = 0;
const normalizeViewId = (value) => {
const normalized = String(value || "overview").toLowerCase();
if (normalized === "operations") {
return "tasks";
}
if (normalized === "manage") {
return "settings";
}
return normalized;
};
const tabs = computed(() =>
props.allowDestructive
? [
{ id: "overview", label: "Overview" },
{ id: "inventory", label: "Inventory" },
{ id: "operations", label: "Operations" },
{ id: "manage", label: "Manage" },
{ id: "tasks", label: "Tasks" },
{ id: "settings", label: "Settings" },
]
: [
{ id: "overview", label: "Overview" },
{ id: "inventory", label: "Inventory" },
{ id: "operations", label: "Operations" },
{ id: "tasks", label: "Tasks" },
]
);
const currentView = computed(() => (props.routeDriven ? props.activeView : localView.value));
const currentView = computed(() => normalizeViewId(props.routeDriven ? props.activeView : localView.value));
const hasGateway = computed(() => Boolean(selectedGateway.value));
const showFleetLanding = computed(() => !hasGateway.value && !unavailableGatewayId.value);
const installerCopyLabel = computed(() =>
@@ -661,10 +672,12 @@ const navigateGateway = (gatewayId, nextView = "overview") => {
return;
}
const normalizedView = normalizeViewId(nextView);
if (props.routeDriven) {
emit("navigate", { gatewayId: String(gatewayId), view: nextView });
emit("navigate", { gatewayId: String(gatewayId), view: normalizedView });
} else {
localView.value = nextView;
localView.value = normalizedView;
}
};
@@ -1002,11 +1015,12 @@ const removeGateway = async () => {
};
const setView = (viewId) => {
const normalizedView = normalizeViewId(viewId);
if (props.routeDriven && selectedGateway.value?.id) {
emit("navigate", { gatewayId: String(selectedGateway.value.id), view: viewId });
emit("navigate", { gatewayId: String(selectedGateway.value.id), view: normalizedView });
return;
}
localView.value = viewId;
localView.value = normalizedView;
};
const stopAutoRefresh = () => {
@@ -1030,7 +1044,7 @@ const autoRefreshTick = async () => {
if (selectedGateway.value?.id) {
const shouldRefreshDetail =
currentView.value === "overview" ||
currentView.value === "operations" ||
currentView.value === "tasks" ||
Boolean(selectedGateway.value.active_operation);
const refreshes = [];
if (shouldRefreshDetail) {
@@ -1077,7 +1091,7 @@ watch(
() => props.activeView,
(value) => {
if (!props.routeDriven && value) {
localView.value = value;
localView.value = normalizeViewId(value);
}
},
{ immediate: true }
@@ -1295,18 +1309,30 @@ onUnmounted(() => {
@save-bindings="saveBindingsAction"
/>
<EdgeGatewayOperationsPage
v-else-if="currentView === 'operations'"
<EdgeGatewayTasksPage
v-else-if="currentView === 'tasks'"
:gateway="selectedGatewayView"
:operations="selectedGatewayView.operations || []"
:recent-commands="selectedGatewayView.recent_commands || []"
:operation-summary="selectedGatewayView.recent_operations_summary || {}"
:selected-operation-id="selectedGatewayView.active_operation?.id || null"
:operation-events="selectedGatewayView.active_operation?.events || []"
:busy="loading.operation || loading.operationCancel"
:allow-destructive="allowDestructive"
:loading-create="loading.operation"
:loading-cancel="loading.operationCancel"
@run-operation="queueOperation"
:update-target-version="selectedGatewayView.metadata?.target_version || ''"
:stream-status="selectedGatewayView.channel_status?.broker?.connected ? 'open' : 'idle'"
:stream-error="selectedGatewayView.error_state?.message || ''"
@queue-discovery="queueDiscovery"
@queue-update="queueOperation({ type: 'UPDATE', request: {} })"
@queue-uninstall="queueOperation({ type: 'UNINSTALL', request: {} })"
@cancel-operation="cancelOperation"
@retry-operation="queueOperation({ type: selectedGatewayView.active_operation?.type || 'UPDATE', request: {} })"
@select-operation="() => {}"
@update-target-version="() => {}"
/>
<EdgeGatewayManagePage
v-else-if="currentView === 'manage' && allowDestructive"
v-else-if="currentView === 'settings' && allowDestructive"
:gateway="selectedGatewayView"
:loading-metadata="loading.metadata"
:loading-cutover="loading.cutover"
+1 -1
View File
@@ -263,7 +263,7 @@ onUnmounted(() => {
<template>
<div class="is-hidden-mobile">
<p>{{ $t('login_qr.mobile_only') }}</p>
<p data-testid="login-qr-desktop-message">{{ $t('login_qr.mobile_only') }}</p>
</div>
<div class="is-hidden-desktop">
<PageLoader v-show="isProcessingQRCode" />
+12 -4
View File
@@ -2349,10 +2349,18 @@ test.describe("Admin POS Orders - desktop action menu layout", () => {
const previewContainer = dropdownContent.getByTestId("action-settings-wheel-attachment-preview");
await expect(attachmentPanel).toBeVisible();
await expect(previewContainer.locator("img, iframe")).toBeVisible();
await expect(dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-preview-"]')).toBeVisible();
await expect(dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-download-"]')).toBeVisible();
await expect(dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-print-"]')).toBeVisible();
await expect(dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-delete-"]')).toBeVisible();
await expect(
dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-preview-"]')
).toBeVisible();
await expect(
dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-download-"]')
).toBeVisible();
await expect(
dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-print-"]')
).toBeVisible();
await expect(
dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-delete-"]')
).toBeVisible();
const [firstAttachmentRowBox, attachmentPanelBox] = await Promise.all([
firstAttachmentRow.boundingBox(),
+13
View File
@@ -17,4 +17,17 @@ test.describe("Auth entry smoke", () => {
await expect(page.getByTestId("login-password")).toBeVisible();
await expect(page.getByTestId("login-submit")).toBeVisible();
});
test("@smoke qr login route renders without QR bootstrap import errors", async ({ page }) => {
const pageErrors = [];
page.on("pageerror", (error) => {
pageErrors.push(error.message);
});
await mockApi(page, { authenticated: false });
await page.goto("/login/qr");
await expect(page.getByTestId("login-qr-desktop-message")).toBeVisible();
expect(pageErrors.join("\n")).not.toContain("does not provide an export named 'default'");
});
});
+60 -10
View File
@@ -1126,12 +1126,66 @@ test.describe("POS flow", () => {
await page.locator("#reg_1").fill("AB12345");
await expect(page.getByTestId("pos-step-1").getByText("Pleno Logistics").first()).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-desktop-vehicle-summary")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-desktop-vehicle-summary")).toContainText("Tankvogn med hænger");
await expect(page.getByText(/Sidste vask/i)).toHaveCount(0);
await expect(page.getByRole("button", { name: /Kopier sidste vask/i })).toHaveCount(0);
await expect(page.getByTestId("pos-desktop-last-wash")).toHaveCount(0);
await expect(page.getByTestId("pos-desktop-last-wash-copy")).toHaveCount(0);
});
test("desktop keeps customer selection mounted only once when an unlinked vehicle card is expanded", async ({
test("desktop renders the Sidste vask card when the selected vehicle has a last_order_id", async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== "chromium-desktop", "Desktop POS step 1 is validated on chromium-desktop.");
const fixture = createPosFixture();
fixture.vehicles = [
{
...fixture.vehicles[0],
reg: "AB12345",
last_order_id: 9201,
},
];
fixture.orderItemsByOrderId[9201] = [
{
id: 92011,
order_id: 9201,
product_id: fixture.products[0].id,
product: fixture.products[0],
quantity: 1,
notes: "",
reference: "",
related_item_id: null,
price: fixture.products[0].price,
},
{
id: 92012,
order_id: 9201,
product_id: fixture.products[1].id,
product: fixture.products[1],
quantity: 1,
notes: "",
reference: "",
related_item_id: 92011,
price: fixture.products[1].price,
},
];
await setupDesktopPosPage(page, fixture, { token: "pos-desktop-last-wash-card" });
await page.locator("#reg_1").fill("AB12345");
const vehicleSummary = page.getByTestId("pos-desktop-vehicle-summary");
const lastWashCard = page.getByTestId("pos-desktop-last-wash");
await expect(vehicleSummary).toBeVisible({ timeout: 10_000 });
await expect(lastWashCard).toBeVisible({ timeout: 10_000 });
await expect(lastWashCard).toContainText("Tankvogn med hænger", { timeout: 10_000 });
await expect(lastWashCard).toContainText("Dolly", { timeout: 10_000 });
await expect(page.getByTestId("pos-desktop-last-wash-copy")).toBeDisabled();
});
test("desktop keeps only one visible customer selector for an unlinked vehicle after the refactor", async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== "chromium-desktop", "Desktop POS step 1 is validated on chromium-desktop.");
@@ -1155,18 +1209,14 @@ test.describe("POS flow", () => {
const visibleCustomerSearchInputs = page.locator("#pos_select_customer_input:visible");
const cardPaymentButtons = page.locator("button.customer-quick-action--card");
const visibleCardPaymentButtons = page.locator("button.customer-quick-action--card:visible");
const secondaryBlock = page.getByTestId("pos-desktop-secondary-block");
await expect(customerSearchInputs).toHaveCount(1);
await expect(visibleCustomerSearchInputs).toHaveCount(1);
await expect(cardPaymentButtons).toHaveCount(1);
await expect(visibleCardPaymentButtons).toHaveCount(1);
await page.locator(".pos-vehicle-form__expandable .has-text-centered").click();
await expect(customerSearchInputs).toHaveCount(1);
await expect(visibleCustomerSearchInputs).toHaveCount(1);
await expect(cardPaymentButtons).toHaveCount(1);
await expect(visibleCardPaymentButtons).toHaveCount(1);
await expect(page.getByTestId("pos-desktop-vehicle-summary")).toBeVisible({ timeout: 10_000 });
await expect(secondaryBlock).toHaveCount(0);
});
test("desktop defaults the inline customer picker to customer invoice and keeps the selected action clear", async ({
+5
View File
@@ -373,6 +373,11 @@ test.describe("POS visuals", () => {
await expect(page.getByTestId("pos-step-1").locator(".pos-card-tabs > .tabs li.is-active")).toContainText("Kunde");
await expectPosCardTabsDividerSpacing(page.getByTestId("pos-step-1"));
await expectCustomerTabsDividerSpacing(page.getByTestId("pos-step-1"));
await expect(page.getByTestId("pos-desktop-vehicle-summary")).toBeVisible();
await expect(page.getByTestId("pos-desktop-last-wash")).toBeVisible();
await expect(page.getByTestId("pos-desktop-last-wash")).toContainText("Indvendig vask Forvogn");
await expect(page.getByTestId("pos-desktop-secondary-block")).toBeVisible();
await expect(page.getByTestId("pos-desktop-last-wash-copy")).toBeDisabled();
await expect(page.getByTestId("pos-step-1")).toHaveScreenshot("pos-step-1-customer-desktop.png", {
maxDiffPixels: 2000,
});
+37 -19
View File
@@ -3320,7 +3320,9 @@ function ensureEdgeGatewayHardwareFixture(edgeGatewayFixture) {
}
function buildEdgeGatewayHardwareBindingsIndex(edgeGatewayFixture, departmentId) {
const gateways = edgeGatewayFixture.gateways.filter((gateway) => Number(gateway.department_id) === Number(departmentId));
const gateways = edgeGatewayFixture.gateways.filter(
(gateway) => Number(gateway.department_id) === Number(departmentId)
);
const bindingsByRelayId = {};
gateways.forEach((gateway) => {
@@ -3420,8 +3422,7 @@ function buildEdgeGatewayHardwareDepartmentWorkspace(edgeGatewayFixture, departm
required: requiredRelayCount,
bound: boundRelayCount,
missing: Math.max(0, requiredRelayCount - boundRelayCount),
state:
requiredRelayCount === 0 ? "NOT_REQUIRED" : boundRelayCount === requiredRelayCount ? "READY" : "MISSING",
state: requiredRelayCount === 0 ? "NOT_REQUIRED" : boundRelayCount === requiredRelayCount ? "READY" : "MISSING",
},
links: {
legacy: `/superuser/department/lanes/${lane.id}`,
@@ -3478,7 +3479,10 @@ function buildEdgeGatewayHardwareDepartmentWorkspace(edgeGatewayFixture, departm
const scansByScannerId = {};
(hardware.scans || [])
.filter((scan) => Number(scan.department_id) === Number(departmentId))
.sort((left, right) => new Date(String(right.created_at || 0)).getTime() - new Date(String(left.created_at || 0)).getTime())
.sort(
(left, right) =>
new Date(String(right.created_at || 0)).getTime() - new Date(String(left.created_at || 0)).getTime()
)
.forEach((scan) => {
const scannerId = Number(scan.plate_scanner_id || 0);
if (!scansByScannerId[scannerId]) {
@@ -3514,11 +3518,19 @@ function buildEdgeGatewayHardwareDepartmentWorkspace(edgeGatewayFixture, departm
});
const issues = [];
const onlineGatewayCount = gatewayPayloads.filter((gateway) => String(gateway?.status || "").toUpperCase() === "ONLINE").length;
const transportMode = String(gatewayPayloads[0]?.department_transport_mode || gateways[0]?.department_transport_mode || "cloud");
const onlineGatewayCount = gatewayPayloads.filter(
(gateway) => String(gateway?.status || "").toUpperCase() === "ONLINE"
).length;
const transportMode = String(
gatewayPayloads[0]?.department_transport_mode || gateways[0]?.department_transport_mode || "cloud"
);
if (gatewayPayloads.length === 0) {
issues.push({ severity: "danger", code: "NO_GATEWAY", message: "No edge gateway has been claimed for this department." });
issues.push({
severity: "danger",
code: "NO_GATEWAY",
message: "No edge gateway has been claimed for this department.",
});
} else if (transportMode === "gateway" && onlineGatewayCount === 0) {
issues.push({
severity: "danger",
@@ -3663,17 +3675,17 @@ function buildEdgeGatewayHardwareDepartmentWorkspace(edgeGatewayFixture, departm
});
const primaryGateway = gatewayPayloads.find((gateway) => gateway?.is_primary) || gatewayPayloads[0] || null;
const recentScanAt = scanners
.map((scanner) => scanner?.recent_scan_at)
.filter(Boolean)
.sort((left, right) => new Date(String(right || 0)).getTime() - new Date(String(left || 0)).getTime())[0] || null;
const recentScanAt =
scanners
.map((scanner) => scanner?.recent_scan_at)
.filter(Boolean)
.sort((left, right) => new Date(String(right || 0)).getTime() - new Date(String(left || 0)).getTime())[0] || null;
const health =
issues.some((issue) => issue.severity === "danger")
? "AT_RISK"
: issues.length > 0
? "PARTIAL"
: "READY";
const health = issues.some((issue) => issue.severity === "danger")
? "AT_RISK"
: issues.length > 0
? "PARTIAL"
: "READY";
const summary = {
department_id: department.id,
@@ -3820,7 +3832,9 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
if (edgeGatewayWorkspaceDepartmentsPattern.test(pathname) && method === "GET") {
const hardware = ensureEdgeGatewayHardwareFixture(edgeGatewayFixture);
const summaries = (hardware?.departments || [])
.map((department) => buildEdgeGatewayHardwareDepartmentWorkspace(edgeGatewayFixture, department.id, false)?.summary)
.map(
(department) => buildEdgeGatewayHardwareDepartmentWorkspace(edgeGatewayFixture, department.id, false)?.summary
)
.filter(Boolean)
.sort((left, right) => Number(left?.order_priority || 0) - Number(right?.order_priority || 0));
await route.fulfill(json({ data: summaries }));
@@ -3837,7 +3851,11 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
if (numberPlateScannersPattern.test(pathname) && method === "GET") {
const hardware = ensureEdgeGatewayHardwareFixture(edgeGatewayFixture);
const rows = cloneJson(hardware?.scanners || []);
const paginated = paginateRows(rows, parsedUrl.searchParams.get("page") || 1, parsedUrl.searchParams.get("limit") || 10);
const paginated = paginateRows(
rows,
parsedUrl.searchParams.get("page") || 1,
parsedUrl.searchParams.get("limit") || 10
);
await route.fulfill(
json({
success: true,
+3 -1
View File
@@ -41,7 +41,9 @@ test("[BOOKINGS][User][Creation] should show interior wash products on the inter
await expect(page.getByText("Indvendig vask Forvogn")).toBeVisible();
});
test("[BOOKINGS][User][Creation] should show desktop loading states while categories and products resolve", async ({ page }, testInfo) => {
test("[BOOKINGS][User][Creation] should show desktop loading states while categories and products resolve", async ({
page,
}, testInfo) => {
test.skip(!testInfo.project.name.toLowerCase().includes("desktop"), "Desktop only");
await prepareBookingPage(page, {
+5 -8
View File
@@ -222,14 +222,11 @@ export default defineConfig(({ mode }) => {
ignored: ['**/output/playwright/**']
}
},
optimizeDeps: {
disabled: 'dev',
...(isPlaywrightRuntime
? {
entries: ['index.html']
}
: {})
}
optimizeDeps: isPlaywrightRuntime
? {
entries: ['index.html']
}
: undefined
}
})