Revise QR code generation library, enhance POS flow, and update Edge Gateway management views and tests
This commit is contained in:
+16338
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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 ({
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -222,14 +222,11 @@ export default defineConfig(({ mode }) => {
|
||||
ignored: ['**/output/playwright/**']
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
disabled: 'dev',
|
||||
...(isPlaywrightRuntime
|
||||
? {
|
||||
entries: ['index.html']
|
||||
}
|
||||
: {})
|
||||
}
|
||||
optimizeDeps: isPlaywrightRuntime
|
||||
? {
|
||||
entries: ['index.html']
|
||||
}
|
||||
: undefined
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user