Compare commits
9
Commits
ios-v1.1.0
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df7563c15d | ||
|
|
0b1df55728 | ||
|
|
398a0a146f | ||
|
|
dfaca1e8a5 | ||
|
|
a4a3b4adb1 | ||
|
|
b2f77b45e5 | ||
|
|
f0b3fc4675 | ||
|
|
1e7298245d | ||
|
|
0369664a96 |
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// AUT-1 smoke run — capture the before/after visual diff for the
|
||||||
|
// `primary-button hover state` change in `src/assets/main.css`.
|
||||||
|
//
|
||||||
|
// Renders tests/visual-previews/AUT-1/preview.html in a real Chromium
|
||||||
|
// against the project's bundled Bulma stylesheet, then takes two
|
||||||
|
// screenshots:
|
||||||
|
//
|
||||||
|
// desktop-before.png — Bulma defaults, button idle (no hover effect).
|
||||||
|
// desktop-after.png — With the AUT-1 CSS rules applied, button
|
||||||
|
// hovered (lift + brightness shift visible).
|
||||||
|
// mobile-before.png — Same as desktop-before, captured at the
|
||||||
|
// Pixel 5 viewport.
|
||||||
|
// mobile-after.png — Same as desktop-after, captured at the
|
||||||
|
// Pixel 5 viewport.
|
||||||
|
//
|
||||||
|
// Output is written to tests/visual-previews/AUT-1/. The script is
|
||||||
|
// idempotent: existing files are overwritten, not appended to.
|
||||||
|
//
|
||||||
|
// Invoke with: `node scripts/aut-1-capture-hover-preview.mjs`
|
||||||
|
// (Chromium must already be installed via `npm run test:e2e:install`.)
|
||||||
|
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { dirname } from "node:path";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const REPO_ROOT = path.resolve(__dirname, "..");
|
||||||
|
const PREVIEW_HTML = path.join(
|
||||||
|
REPO_ROOT,
|
||||||
|
"tests/visual-previews/AUT-1/preview.html",
|
||||||
|
);
|
||||||
|
const OUTPUT_DIR = path.join(REPO_ROOT, "tests/visual-previews/AUT-1");
|
||||||
|
|
||||||
|
// The AUT-1 hover rules — kept in lockstep with the diff in
|
||||||
|
// src/assets/main.css. We inject these via a <style> tag on the
|
||||||
|
// "after" pass and leave them off for the "before" pass so the
|
||||||
|
// screenshots show the same Bulma theme but with vs without the new
|
||||||
|
// hover effect.
|
||||||
|
const AUT1_HOVER_CSS = `
|
||||||
|
.button.is-primary {
|
||||||
|
transition:
|
||||||
|
filter 0.15s ease,
|
||||||
|
transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.button.is-primary:hover,
|
||||||
|
.button.is-primary.is-hovered {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.button.is-primary:active,
|
||||||
|
.button.is-primary.is-active {
|
||||||
|
filter: brightness(0.95);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
.button.is-primary:focus-visible {
|
||||||
|
filter: brightness(1.04);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function captureVariant({ browser, viewport, label, withHoverCss, hover }) {
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport,
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.goto(pathToFileUrl(PREVIEW_HTML));
|
||||||
|
if (withHoverCss) {
|
||||||
|
await page.addStyleTag({ content: AUT1_HOVER_CSS });
|
||||||
|
}
|
||||||
|
const target = page.locator(
|
||||||
|
hover
|
||||||
|
? '[data-testid="primary-button-hover"]'
|
||||||
|
: '[data-testid="primary-button-idle"]',
|
||||||
|
);
|
||||||
|
const stage = page.locator(
|
||||||
|
hover ? "#stage-hover" : "#stage-idle",
|
||||||
|
);
|
||||||
|
if (hover) {
|
||||||
|
await target.hover();
|
||||||
|
// Wait for the 0.15s transition to settle.
|
||||||
|
await page.waitForTimeout(220);
|
||||||
|
}
|
||||||
|
const filename = `${label}-${withHoverCss ? "after" : "before"}.png`;
|
||||||
|
const destination = path.join(OUTPUT_DIR, filename);
|
||||||
|
await stage.screenshot({ path: destination, type: "png" });
|
||||||
|
await context.close();
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathToFileUrl(filePath) {
|
||||||
|
// Playwright's `file://` URLs need absolute paths. On POSIX this is
|
||||||
|
// straightforward; on Windows this helper keeps the script cross-platform
|
||||||
|
// should it ever run there.
|
||||||
|
const absolute = path.resolve(filePath);
|
||||||
|
return absolute.startsWith("/") ? `file://${absolute}` : `file:///${absolute}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||||
|
const browser = await chromium.launch({ headless: true });
|
||||||
|
try {
|
||||||
|
const viewports = [
|
||||||
|
{ label: "desktop", viewport: { width: 1280, height: 720 } },
|
||||||
|
{
|
||||||
|
label: "mobile",
|
||||||
|
viewport: { width: 393, height: 851 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const { label, viewport } of viewports) {
|
||||||
|
for (const withHoverCss of [false, true]) {
|
||||||
|
for (const hover of [false, true]) {
|
||||||
|
// We only want idle on the "before" pass and hover on the
|
||||||
|
// "after" pass — skip the two redundant combinations.
|
||||||
|
const isIdleShot = !hover;
|
||||||
|
const isBeforeShot = !withHoverCss;
|
||||||
|
if (isIdleShot !== isBeforeShot) continue;
|
||||||
|
const file = await captureVariant({
|
||||||
|
browser,
|
||||||
|
viewport,
|
||||||
|
label,
|
||||||
|
withHoverCss,
|
||||||
|
hover,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("wrote", path.relative(REPO_ROOT, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("aut-1 capture failed:", err);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
@@ -122,6 +122,7 @@ export const ownedFilesByRole = {
|
|||||||
"superuser-users.spec.ts",
|
"superuser-users.spec.ts",
|
||||||
"superuser-vehicles.smoke.spec.js",
|
"superuser-vehicles.smoke.spec.js",
|
||||||
"workfeed-config.smoke.spec.js",
|
"workfeed-config.smoke.spec.js",
|
||||||
|
"xlvask-flag-to-selvvash-navigation.spec.ts",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -144,3 +144,30 @@ body:not(.pleno-large-table-headers) .table thead th {
|
|||||||
margin-top: 30px !important;
|
margin-top: 30px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AUT-1: enhanced primary-button hover state.
|
||||||
|
Bulma's default only nudges background lightness; this adds a small
|
||||||
|
lift + brightness shift so the hover is unambiguous. Transitions keep
|
||||||
|
it smooth so it doesn't feel jarring on click-heavy screens. */
|
||||||
|
.button.is-primary {
|
||||||
|
transition:
|
||||||
|
filter 0.15s ease,
|
||||||
|
transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.is-primary:hover,
|
||||||
|
.button.is-primary.is-hovered {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.is-primary:active,
|
||||||
|
.button.is-primary.is-active {
|
||||||
|
filter: brightness(0.95);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.is-primary:focus-visible {
|
||||||
|
filter: brightness(1.04);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|||||||
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
|
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
|
||||||
import { SELFWASH_PERIOD_ALL_LIMIT } from "@/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js";
|
import { SELFWASH_PERIOD_ALL_LIMIT } from "@/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js";
|
||||||
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
import { removeError } from "@/components/request/HandleGlobalError.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
autoLoad: {
|
autoLoad: {
|
||||||
@@ -37,6 +38,8 @@ const props = defineProps({
|
|||||||
type: Boolean, default: false
|
type: Boolean, default: false
|
||||||
}, highlightUsageLogId: {
|
}, highlightUsageLogId: {
|
||||||
type: Number, default: 0
|
type: Number, default: 0
|
||||||
|
}, departmentId: {
|
||||||
|
type: Number, default: 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,9 +63,11 @@ const {
|
|||||||
setOrder,
|
setOrder,
|
||||||
hideSearchField,
|
hideSearchField,
|
||||||
setHideSearchField,
|
setHideSearchField,
|
||||||
|
lastError,
|
||||||
} = paginatedList;
|
} = paginatedList;
|
||||||
|
|
||||||
setEndpoint("/modules/xlvask/services/usage/orders", false);
|
const XLVASK_USAGE_ORDERS_ENDPOINT = "/modules/xlvask/services/usage/orders";
|
||||||
|
setEndpoint(XLVASK_USAGE_ORDERS_ENDPOINT, false);
|
||||||
if (props.loadAllAtOnce) {
|
if (props.loadAllAtOnce) {
|
||||||
setMetaItemsPerPage(SELFWASH_PERIOD_ALL_LIMIT, false);
|
setMetaItemsPerPage(SELFWASH_PERIOD_ALL_LIMIT, false);
|
||||||
}
|
}
|
||||||
@@ -98,12 +103,23 @@ if (props.hideSearch) {
|
|||||||
setHideSearchField(false);
|
setHideSearchField(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (router.currentRoute.value.params.departmentId) {
|
const routeDepartmentId = Number.parseInt(
|
||||||
setOrder("StartTime", "desc");
|
String(router.currentRoute.value.params.departmentId ?? ""),
|
||||||
} else {
|
10
|
||||||
setOrder("StartTime", "desc");
|
);
|
||||||
|
const effectiveDepartmentId =
|
||||||
|
props.departmentId > 0
|
||||||
|
? props.departmentId
|
||||||
|
: Number.isInteger(routeDepartmentId) && routeDepartmentId > 0
|
||||||
|
? routeDepartmentId
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (effectiveDepartmentId > 0) {
|
||||||
|
setFilter("HallId", effectiveDepartmentId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setOrder("StartTime", "desc");
|
||||||
|
|
||||||
const extractResponseData = (response) => response?.data?.data ?? response?.data ?? {};
|
const extractResponseData = (response) => response?.data?.data ?? response?.data ?? {};
|
||||||
|
|
||||||
const summary = ref({});
|
const summary = ref({});
|
||||||
@@ -253,6 +269,20 @@ const visibleObjectsCount = computed(() => {
|
|||||||
}
|
}
|
||||||
return currentList.filter((object) => !isUsageOrderAttachedToOrder(object)).length;
|
return currentList.filter((object) => !isUsageOrderAttachedToOrder(object)).length;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 404 on the orders endpoint means the API surface has not been
|
||||||
|
// implemented yet. Show a friendly notice instead of the generic error
|
||||||
|
// popper the rest of the paginated surfaces surface.
|
||||||
|
const apiEndpointNotImplemented = computed(() => (
|
||||||
|
lastError.value?.status === 404
|
||||||
|
&& lastError.value?.endpoint === XLVASK_USAGE_ORDERS_ENDPOINT
|
||||||
|
));
|
||||||
|
|
||||||
|
watch(apiEndpointNotImplemented, (isNotImplemented) => {
|
||||||
|
if (isNotImplemented) {
|
||||||
|
removeError("paginatedGetRequest");
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -366,6 +396,13 @@ const visibleObjectsCount = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</PaginationDisplay>
|
</PaginationDisplay>
|
||||||
|
<div
|
||||||
|
v-else-if="apiEndpointNotImplemented"
|
||||||
|
class="notification is-info is-light mb-4"
|
||||||
|
data-testid="xlvask-api-not-implemented"
|
||||||
|
>
|
||||||
|
{{ t('invoicing_period.xlvask_review.errors.api_endpoint_not_implemented') }}
|
||||||
|
</div>
|
||||||
<ShowErrorField v-else error="paginatedGetRequest"/>
|
<ShowErrorField v-else error="paginatedGetRequest"/>
|
||||||
<XlvaskUsageOrdersTable
|
<XlvaskUsageOrdersTable
|
||||||
:objects="list"
|
:objects="list"
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export function usePaginatedList() {
|
|||||||
let activeRequestController = null;
|
let activeRequestController = null;
|
||||||
let searchDebounceTimeout = null;
|
let searchDebounceTimeout = null;
|
||||||
|
|
||||||
|
/** The last error produced by `paginatedGetRequest` (null on success). */
|
||||||
|
const lastError = ref(null);
|
||||||
|
|
||||||
/** Additional query parameters */
|
/** Additional query parameters */
|
||||||
const additionalQueryParameters = ref({});
|
const additionalQueryParameters = ref({});
|
||||||
|
|
||||||
@@ -170,6 +173,7 @@ export function usePaginatedList() {
|
|||||||
abortActiveRequest();
|
abortActiveRequest();
|
||||||
const requestController = typeof AbortController !== "undefined" ? new AbortController() : null;
|
const requestController = typeof AbortController !== "undefined" ? new AbortController() : null;
|
||||||
activeRequestController = requestController;
|
activeRequestController = requestController;
|
||||||
|
lastError.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(API_URL + endpoint.value, {
|
const response = await axios.get(API_URL + endpoint.value, {
|
||||||
@@ -193,10 +197,16 @@ export function usePaginatedList() {
|
|||||||
setLastUpdated();
|
setLastUpdated();
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!isCanceledRequest(error)) {
|
if (isCanceledRequest(error)) {
|
||||||
parseError(error, 'paginatedGetRequest');
|
return null;
|
||||||
console.log(error);
|
|
||||||
}
|
}
|
||||||
|
lastError.value = {
|
||||||
|
status: Number.parseInt(String(error?.response?.status ?? error?.status ?? ""), 10) || null,
|
||||||
|
endpoint: endpoint.value,
|
||||||
|
message: error?.response?.data?.data?.message ?? error?.response?.data?.message ?? error?.message ?? null,
|
||||||
|
};
|
||||||
|
parseError(error, 'paginatedGetRequest');
|
||||||
|
console.log(error);
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
if (activeRequestController === requestController) {
|
if (activeRequestController === requestController) {
|
||||||
@@ -487,6 +497,7 @@ export function usePaginatedList() {
|
|||||||
isLoading,
|
isLoading,
|
||||||
isExporting,
|
isExporting,
|
||||||
latestSearch,
|
latestSearch,
|
||||||
|
lastError,
|
||||||
additionalQueryParameters,
|
additionalQueryParameters,
|
||||||
exportTransform,
|
exportTransform,
|
||||||
setHideSearchField,
|
setHideSearchField,
|
||||||
@@ -542,6 +553,7 @@ export const hideSearchField = globalInstance.hideSearchField;
|
|||||||
export const isLoading = globalInstance.isLoading;
|
export const isLoading = globalInstance.isLoading;
|
||||||
export const isExporting = globalInstance.isExporting;
|
export const isExporting = globalInstance.isExporting;
|
||||||
export const latestSearch = globalInstance.latestSearch;
|
export const latestSearch = globalInstance.latestSearch;
|
||||||
|
export const lastError = globalInstance.lastError;
|
||||||
export const additionalQueryParameters = globalInstance.additionalQueryParameters;
|
export const additionalQueryParameters = globalInstance.additionalQueryParameters;
|
||||||
export const exportTransform = globalInstance.exportTransform;
|
export const exportTransform = globalInstance.exportTransform;
|
||||||
|
|
||||||
|
|||||||
@@ -4874,7 +4874,8 @@
|
|||||||
"create_order_item_failed": "Vaskelinjen kunne ikke føjes til ordren.",
|
"create_order_item_failed": "Vaskelinjen kunne ikke føjes til ordren.",
|
||||||
"redirect_order_failed": "Ordren kunne ikke åbnes.",
|
"redirect_order_failed": "Ordren kunne ikke åbnes.",
|
||||||
"load_customers_failed": "Kunderne kunne ikke hentes.",
|
"load_customers_failed": "Kunderne kunne ikke hentes.",
|
||||||
"load_usage_log_failed": "Forbrugsloggen kunne ikke hentes."
|
"load_usage_log_failed": "Forbrugsloggen kunne ikke hentes.",
|
||||||
|
"api_endpoint_not_implemented": "API-endepunkt ikke implementeret endnu"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4340,7 +4340,7 @@
|
|||||||
"vehicle_subscription_type_mismatch": "{product} @:{'words.generated.does'} @:{'words.generated.not'} match @:{'words.generated.the'} @:{'words.generated.vehicle'} @:{'words.generated.subscription'} @:{'words.generated.type'} {expected_product}.",
|
"vehicle_subscription_type_mismatch": "{product} @:{'words.generated.does'} @:{'words.generated.not'} match @:{'words.generated.the'} @:{'words.generated.vehicle'} @:{'words.generated.subscription'} @:{'words.generated.type'} {expected_product}.",
|
||||||
"wash_certificate_attached_without_item": "@:{'words.generated.waschzertifikat'} @:{'words.generated.ist'} @:{'words.generated.ohne'} @:{'words.generated.waschzertifikat'}-@.capitalize:{'words.generated.position'} angehaengt.",
|
"wash_certificate_attached_without_item": "@:{'words.generated.waschzertifikat'} @:{'words.generated.ist'} @:{'words.generated.ohne'} @:{'words.generated.waschzertifikat'}-@.capitalize:{'words.generated.position'} angehaengt.",
|
||||||
"wash_certificate_item_without_certificate": "@:{'words.generated.waschzertifikat'}-@.capitalize:{'words.generated.position'} @:{'words.generated.ist'} @:{'words.generated.ohne'} @:{'words.generated.waschzertifikat'} vorhanden.",
|
"wash_certificate_item_without_certificate": "@:{'words.generated.waschzertifikat'}-@.capitalize:{'words.generated.position'} @:{'words.generated.ist'} @:{'words.generated.ohne'} @:{'words.generated.waschzertifikat'} vorhanden.",
|
||||||
"xlvask_missing_order_link": "@:{'words.generated.xl'} @:{'words.generated.vask'} @:{'words.generated.wash'} @:{'words.generated.is'} neither ignored nor linked @:{'words.generated.to'} @:{'words.generated.an'} @:{'words.generated.order'} @:{'words.generated.in'} @:{'words.generated.the'} @:{'words.generated.selected'} @:{'words.generated.period'}."
|
"xlvask_missing_order_link": "@:{'words.generated.xl'} @.capitalize:{'words.generated.vask'}-@:{'words.generated.registrierung'} @:{'words.generated.ist'} weder ignoriert noch @:{'words.generated.mit'} einer @:{'words.generated.bestellung'} im ausgewaehlten @:{'words.generated.zeitraum'} verknuepft."
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"entities": {
|
"entities": {
|
||||||
@@ -4984,7 +4984,8 @@
|
|||||||
"create_order_item_failed": "Der Wäscheposten konnte nicht zum Auftrag hinzugefügt werden.",
|
"create_order_item_failed": "Der Wäscheposten konnte nicht zum Auftrag hinzugefügt werden.",
|
||||||
"redirect_order_failed": "Der Auftrag konnte nicht geöffnet werden.",
|
"redirect_order_failed": "Der Auftrag konnte nicht geöffnet werden.",
|
||||||
"load_customers_failed": "Die Kunden konnten nicht geladen werden.",
|
"load_customers_failed": "Die Kunden konnten nicht geladen werden.",
|
||||||
"load_usage_log_failed": "Das Verbrauchsprotokoll konnte nicht geladen werden."
|
"load_usage_log_failed": "Das Verbrauchsprotokoll konnte nicht geladen werden.",
|
||||||
|
"api_endpoint_not_implemented": "API-Endpunkt ist noch nicht implementiert"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4705,7 +4705,8 @@
|
|||||||
"create_order_item_failed": "The wash item could not be added to the order.",
|
"create_order_item_failed": "The wash item could not be added to the order.",
|
||||||
"redirect_order_failed": "The order could not be opened.",
|
"redirect_order_failed": "The order could not be opened.",
|
||||||
"load_customers_failed": "The customers could not be loaded.",
|
"load_customers_failed": "The customers could not be loaded.",
|
||||||
"load_usage_log_failed": "The usage log could not be loaded."
|
"load_usage_log_failed": "The usage log could not be loaded.",
|
||||||
|
"api_endpoint_not_implemented": "API endpoint not implemented yet"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3421,6 +3421,7 @@
|
|||||||
"expected_price": "@:{'templates.generated.compat.invoice_period.flags.preview.expected_price'}",
|
"expected_price": "@:{'templates.generated.compat.invoice_period.flags.preview.expected_price'}",
|
||||||
"no_order_items": "@:common.templates.no_entity_available",
|
"no_order_items": "@:common.templates.no_entity_available",
|
||||||
"no_xlvask_usage_log": "@:common.templates.no_entity_available",
|
"no_xlvask_usage_log": "@:common.templates.no_entity_available",
|
||||||
|
"no_xlvask_usage_log_metadata": "XL Vask registration (no metadata available — see api flag-context serializer)...",
|
||||||
"order_items": "@:{'templates.generated.compat.global_search.entity_types.order_items'}",
|
"order_items": "@:{'templates.generated.compat.global_search.entity_types.order_items'}",
|
||||||
"price": "@:common.price",
|
"price": "@:common.price",
|
||||||
"product": "@:common.product",
|
"product": "@:common.product",
|
||||||
@@ -4092,7 +4093,8 @@
|
|||||||
"create_order_item_failed": "@:{'templates.generated.compat.invoicing_period.xlvask_review.errors.create_order_item_failed'}",
|
"create_order_item_failed": "@:{'templates.generated.compat.invoicing_period.xlvask_review.errors.create_order_item_failed'}",
|
||||||
"redirect_order_failed": "@:{'templates.generated.compat.invoicing_period.xlvask_review.errors.redirect_order_failed'}",
|
"redirect_order_failed": "@:{'templates.generated.compat.invoicing_period.xlvask_review.errors.redirect_order_failed'}",
|
||||||
"load_customers_failed": "@:{'templates.generated.compat.invoicing_period.xlvask_review.errors.load_customers_failed'}",
|
"load_customers_failed": "@:{'templates.generated.compat.invoicing_period.xlvask_review.errors.load_customers_failed'}",
|
||||||
"load_usage_log_failed": "@:{'templates.generated.compat.invoicing_period.xlvask_review.errors.load_usage_log_failed'}"
|
"load_usage_log_failed": "@:{'templates.generated.compat.invoicing_period.xlvask_review.errors.load_usage_log_failed'}",
|
||||||
|
"api_endpoint_not_implemented": "@:{'templates.generated.compat.invoicing_period.xlvask_review.errors.api_endpoint_not_implemented'}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4343,7 +4343,7 @@
|
|||||||
"vehicle_subscription_type_mismatch": "{product} @:{'words.generated.does'} @:{'words.generated.not'} match @:{'words.generated.the'} @:{'words.generated.vehicle'} @:{'words.generated.subscription'} @:{'words.generated.type'} {expected_product}.",
|
"vehicle_subscription_type_mismatch": "{product} @:{'words.generated.does'} @:{'words.generated.not'} match @:{'words.generated.the'} @:{'words.generated.vehicle'} @:{'words.generated.subscription'} @:{'words.generated.type'} {expected_product}.",
|
||||||
"wash_certificate_attached_without_item": "@.capitalize:{'words.generated.vaskesertifikat'} @:{'words.generated.er'} @:{'words.generated.vedlagt'} @:{'words.generated.uten'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.vaskesertifikat'}-@:{'words.generated.linje'}.",
|
"wash_certificate_attached_without_item": "@.capitalize:{'words.generated.vaskesertifikat'} @:{'words.generated.er'} @:{'words.generated.vedlagt'} @:{'words.generated.uten'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.vaskesertifikat'}-@:{'words.generated.linje'}.",
|
||||||
"wash_certificate_item_without_certificate": "@.capitalize:{'words.generated.vaskesertifikat'}-@:{'words.generated.linjen'} @:{'words.generated.finnes'} @:{'words.generated.uten'} @:{'words.generated.et'} @:{'words.generated.vaskesertifikat'}.",
|
"wash_certificate_item_without_certificate": "@.capitalize:{'words.generated.vaskesertifikat'}-@:{'words.generated.linjen'} @:{'words.generated.finnes'} @:{'words.generated.uten'} @:{'words.generated.et'} @:{'words.generated.vaskesertifikat'}.",
|
||||||
"xlvask_missing_order_link": "@:{'words.generated.xl'} @.capitalize:{'words.generated.vask'} @:{'words.generated.wash'} @:{'words.generated.is'} neither ignored nor linked @:{'words.generated.to'} @:{'words.generated.an'} @:{'words.generated.order'} @:{'words.generated.in'} @:{'words.generated.the'} @:{'words.generated.selected'} @:{'words.generated.period'}."
|
"xlvask_missing_order_link": "@:{'words.generated.xl'} @.capitalize:{'words.generated.vask'}-registreringen @:{'words.generated.er'} verken ignorert @:{'words.generated.eller'} knyttet @:{'words.generated.til'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.ordre'} @:{'words.generated.i'} @:{'words.replication.article.host_mention'} @:{'words.generated.valgte'} @:{'words.generated.periode'}."
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"entities": {
|
"entities": {
|
||||||
@@ -4987,7 +4987,8 @@
|
|||||||
"create_order_item_failed": "Vaskelinjen kunne ikke legges til i ordren.",
|
"create_order_item_failed": "Vaskelinjen kunne ikke legges til i ordren.",
|
||||||
"redirect_order_failed": "Ordren kunne ikke åpnes.",
|
"redirect_order_failed": "Ordren kunne ikke åpnes.",
|
||||||
"load_customers_failed": "Kundene kunne ikke hentes.",
|
"load_customers_failed": "Kundene kunne ikke hentes.",
|
||||||
"load_usage_log_failed": "Forbruksloggen kunne ikke hentes."
|
"load_usage_log_failed": "Forbruksloggen kunne ikke hentes.",
|
||||||
|
"api_endpoint_not_implemented": "API-endepunkt er ikke implementert ennå"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4393,7 +4393,7 @@
|
|||||||
"vehicle_subscription_type_mismatch": "{product} @:{'words.generated.does'} @:{'words.generated.not'} @:{'words.generated.match'} @:{'words.generated.the'} @:{'words.generated.vehicle'} @:{'words.generated.subscription'} @:{'words.generated.type'} {expected_product}.",
|
"vehicle_subscription_type_mismatch": "{product} @:{'words.generated.does'} @:{'words.generated.not'} @:{'words.generated.match'} @:{'words.generated.the'} @:{'words.generated.vehicle'} @:{'words.generated.subscription'} @:{'words.generated.type'} {expected_product}.",
|
||||||
"wash_certificate_attached_without_item": "@.capitalize:{'words.generated.tvattcertifikat_2'} ar bifogat @:{'words.generated.utan'} @:{'words.replication.host_definite_suffix'} tvattcertifikatrad.",
|
"wash_certificate_attached_without_item": "@.capitalize:{'words.generated.tvattcertifikat_2'} ar bifogat @:{'words.generated.utan'} @:{'words.replication.host_definite_suffix'} tvattcertifikatrad.",
|
||||||
"wash_certificate_item_without_certificate": "@:{'words.generated.tvattcertifikatraden'} @:{'words.generated.finns'} @:{'words.generated.utan'} @:{'words.generated.ett'} @:{'words.generated.tvattcertifikat_2'}.",
|
"wash_certificate_item_without_certificate": "@:{'words.generated.tvattcertifikatraden'} @:{'words.generated.finns'} @:{'words.generated.utan'} @:{'words.generated.ett'} @:{'words.generated.tvattcertifikat_2'}.",
|
||||||
"xlvask_missing_order_link": "@:{'words.generated.xl'} @.capitalize:{'words.generated.vask'} @:{'words.generated.wash'} @:{'words.generated.is'} neither ignored nor @:{'words.generated.linked'} @:{'words.generated.to'} @:{'words.generated.an'} @:{'words.generated.order'} @:{'words.generated.in'} @:{'words.generated.the'} @:{'words.generated.selected'} @:{'words.generated.period'}."
|
"xlvask_missing_order_link": "@:{'words.generated.xl'} @.capitalize:{'words.generated.vask'}-registreringen @:{'words.generated.ar'} varken ignorerad @:{'words.generated.eller'} kopplad @:{'words.generated.till'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.order'} @:{'words.generated.i'} @:{'words.replication.article.host_mention'} @:{'words.generated.valda'} perioden."
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"entities": {
|
"entities": {
|
||||||
@@ -5037,7 +5037,8 @@
|
|||||||
"create_order_item_failed": "Tvättartikeln kunde inte läggas till i ordern.",
|
"create_order_item_failed": "Tvättartikeln kunde inte läggas till i ordern.",
|
||||||
"redirect_order_failed": "Ordern kunde inte öppnas.",
|
"redirect_order_failed": "Ordern kunde inte öppnas.",
|
||||||
"load_customers_failed": "Kunderna kunde inte hämtas.",
|
"load_customers_failed": "Kunderna kunde inte hämtas.",
|
||||||
"load_usage_log_failed": "Förbrukningsloggen kunde inte hämtas."
|
"load_usage_log_failed": "Förbrukningsloggen kunde inte hämtas.",
|
||||||
|
"api_endpoint_not_implemented": "API-slutpunkt är inte implementerad ännu"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -112,7 +112,8 @@
|
|||||||
"create_order_item_failed": "Vaskelinjen kunne ikke føjes til ordren.",
|
"create_order_item_failed": "Vaskelinjen kunne ikke føjes til ordren.",
|
||||||
"redirect_order_failed": "Ordren kunne ikke åbnes.",
|
"redirect_order_failed": "Ordren kunne ikke åbnes.",
|
||||||
"load_customers_failed": "Kunderne kunne ikke hentes.",
|
"load_customers_failed": "Kunderne kunne ikke hentes.",
|
||||||
"load_usage_log_failed": "Forbrugsloggen kunne ikke hentes."
|
"load_usage_log_failed": "Forbrugsloggen kunne ikke hentes.",
|
||||||
|
"api_endpoint_not_implemented": "API-endepunkt ikke implementeret endnu"
|
||||||
}
|
}
|
||||||
} } }
|
} } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"vehicle_subscription_type_mismatch": "{product} @:{'terms.glossary.does'} @:{'terms.glossary.not'} match @:{'terms.glossary.the'} @:{'terms.glossary.vehicle'} @:{'terms.glossary.subscription'} @:{'terms.glossary.type'} {expected_product}.",
|
"vehicle_subscription_type_mismatch": "{product} @:{'terms.glossary.does'} @:{'terms.glossary.not'} match @:{'terms.glossary.the'} @:{'terms.glossary.vehicle'} @:{'terms.glossary.subscription'} @:{'terms.glossary.type'} {expected_product}.",
|
||||||
"wash_certificate_attached_without_item": "@:{'terms.glossary.waschzertifikat'} @:{'terms.glossary.ist'} @:{'terms.glossary.ohne'} @:{'terms.glossary.waschzertifikat'}-@.capitalize:{'terms.glossary.position'} angehaengt.",
|
"wash_certificate_attached_without_item": "@:{'terms.glossary.waschzertifikat'} @:{'terms.glossary.ist'} @:{'terms.glossary.ohne'} @:{'terms.glossary.waschzertifikat'}-@.capitalize:{'terms.glossary.position'} angehaengt.",
|
||||||
"wash_certificate_item_without_certificate": "@:{'terms.glossary.waschzertifikat'}-@.capitalize:{'terms.glossary.position'} @:{'terms.glossary.ist'} @:{'terms.glossary.ohne'} @:{'terms.glossary.waschzertifikat'} vorhanden.",
|
"wash_certificate_item_without_certificate": "@:{'terms.glossary.waschzertifikat'}-@.capitalize:{'terms.glossary.position'} @:{'terms.glossary.ist'} @:{'terms.glossary.ohne'} @:{'terms.glossary.waschzertifikat'} vorhanden.",
|
||||||
"xlvask_missing_order_link": "@:{'terms.glossary.xl'} @:{'terms.glossary.vask'} @:{'terms.glossary.wash'} @:{'terms.glossary.is'} neither ignored nor linked @:{'terms.glossary.to'} @:{'terms.glossary.an'} @:{'terms.glossary.order'} @:{'terms.glossary.in'} @:{'terms.glossary.the'} @:{'terms.glossary.selected'} @:{'terms.glossary.period'}."
|
"xlvask_missing_order_link": "@:{'terms.glossary.xl'} @.capitalize:{'terms.glossary.vask'}-@:{'terms.glossary.registrierung'} @:{'terms.glossary.ist'} weder ignoriert noch @:{'terms.glossary.mit'} einer @:{'terms.glossary.bestellung'} im ausgewaehlten @:{'terms.glossary.zeitraum'} verknuepft."
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"entities": {
|
"entities": {
|
||||||
|
|||||||
@@ -191,7 +191,8 @@
|
|||||||
"create_order_item_failed": "Der Wäscheposten konnte nicht zum Auftrag hinzugefügt werden.",
|
"create_order_item_failed": "Der Wäscheposten konnte nicht zum Auftrag hinzugefügt werden.",
|
||||||
"redirect_order_failed": "Der Auftrag konnte nicht geöffnet werden.",
|
"redirect_order_failed": "Der Auftrag konnte nicht geöffnet werden.",
|
||||||
"load_customers_failed": "Die Kunden konnten nicht geladen werden.",
|
"load_customers_failed": "Die Kunden konnten nicht geladen werden.",
|
||||||
"load_usage_log_failed": "Das Verbrauchsprotokoll konnte nicht geladen werden."
|
"load_usage_log_failed": "Das Verbrauchsprotokoll konnte nicht geladen werden.",
|
||||||
|
"api_endpoint_not_implemented": "API-Endpunkt ist noch nicht implementiert"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,8 @@
|
|||||||
"create_order_item_failed": "The wash item could not be added to the order.",
|
"create_order_item_failed": "The wash item could not be added to the order.",
|
||||||
"redirect_order_failed": "The order could not be opened.",
|
"redirect_order_failed": "The order could not be opened.",
|
||||||
"load_customers_failed": "The customers could not be loaded.",
|
"load_customers_failed": "The customers could not be loaded.",
|
||||||
"load_usage_log_failed": "The usage log could not be loaded."
|
"load_usage_log_failed": "The usage log could not be loaded.",
|
||||||
|
"api_endpoint_not_implemented": "API endpoint not implemented yet"
|
||||||
}
|
}
|
||||||
} } }
|
} } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"expected_price": "@:{'phrases.compat.invoice_period.flags.preview.expected_price'}",
|
"expected_price": "@:{'phrases.compat.invoice_period.flags.preview.expected_price'}",
|
||||||
"no_order_items": "@:common.templates.no_entity_available",
|
"no_order_items": "@:common.templates.no_entity_available",
|
||||||
"no_xlvask_usage_log": "@:common.templates.no_entity_available",
|
"no_xlvask_usage_log": "@:common.templates.no_entity_available",
|
||||||
|
"no_xlvask_usage_log_metadata": "XL Vask registration (no metadata available — see api flag-context serializer)...",
|
||||||
"order_items": "@:{'phrases.compat.global_search.entity_types.order_items'}",
|
"order_items": "@:{'phrases.compat.global_search.entity_types.order_items'}",
|
||||||
"price": "@:common.price",
|
"price": "@:common.price",
|
||||||
"product": "@:common.product",
|
"product": "@:common.product",
|
||||||
|
|||||||
@@ -196,7 +196,8 @@
|
|||||||
"create_order_item_failed": "@:{'phrases.compat.invoicing_period.xlvask_review.errors.create_order_item_failed'}",
|
"create_order_item_failed": "@:{'phrases.compat.invoicing_period.xlvask_review.errors.create_order_item_failed'}",
|
||||||
"redirect_order_failed": "@:{'phrases.compat.invoicing_period.xlvask_review.errors.redirect_order_failed'}",
|
"redirect_order_failed": "@:{'phrases.compat.invoicing_period.xlvask_review.errors.redirect_order_failed'}",
|
||||||
"load_customers_failed": "@:{'phrases.compat.invoicing_period.xlvask_review.errors.load_customers_failed'}",
|
"load_customers_failed": "@:{'phrases.compat.invoicing_period.xlvask_review.errors.load_customers_failed'}",
|
||||||
"load_usage_log_failed": "@:{'phrases.compat.invoicing_period.xlvask_review.errors.load_usage_log_failed'}"
|
"load_usage_log_failed": "@:{'phrases.compat.invoicing_period.xlvask_review.errors.load_usage_log_failed'}",
|
||||||
|
"api_endpoint_not_implemented": "@:{'phrases.compat.invoicing_period.xlvask_review.errors.api_endpoint_not_implemented'}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"vehicle_subscription_type_mismatch": "{product} @:{'terms.glossary.does'} @:{'terms.glossary.not'} match @:{'terms.glossary.the'} @:{'terms.glossary.vehicle'} @:{'terms.glossary.subscription'} @:{'terms.glossary.type'} {expected_product}.",
|
"vehicle_subscription_type_mismatch": "{product} @:{'terms.glossary.does'} @:{'terms.glossary.not'} match @:{'terms.glossary.the'} @:{'terms.glossary.vehicle'} @:{'terms.glossary.subscription'} @:{'terms.glossary.type'} {expected_product}.",
|
||||||
"wash_certificate_attached_without_item": "@.capitalize:{'terms.glossary.vaskesertifikat'} @:{'terms.glossary.er'} @:{'terms.glossary.vedlagt'} @:{'terms.glossary.uten'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.vaskesertifikat'}-@:{'terms.glossary.linje'}.",
|
"wash_certificate_attached_without_item": "@.capitalize:{'terms.glossary.vaskesertifikat'} @:{'terms.glossary.er'} @:{'terms.glossary.vedlagt'} @:{'terms.glossary.uten'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.vaskesertifikat'}-@:{'terms.glossary.linje'}.",
|
||||||
"wash_certificate_item_without_certificate": "@.capitalize:{'terms.glossary.vaskesertifikat'}-@:{'terms.glossary.linjen'} @:{'terms.glossary.finnes'} @:{'terms.glossary.uten'} @:{'terms.glossary.et'} @:{'terms.glossary.vaskesertifikat'}.",
|
"wash_certificate_item_without_certificate": "@.capitalize:{'terms.glossary.vaskesertifikat'}-@:{'terms.glossary.linjen'} @:{'terms.glossary.finnes'} @:{'terms.glossary.uten'} @:{'terms.glossary.et'} @:{'terms.glossary.vaskesertifikat'}.",
|
||||||
"xlvask_missing_order_link": "@:{'terms.glossary.xl'} @.capitalize:{'terms.glossary.vask'} @:{'terms.glossary.wash'} @:{'terms.glossary.is'} neither ignored nor linked @:{'terms.glossary.to'} @:{'terms.glossary.an'} @:{'terms.glossary.order'} @:{'terms.glossary.in'} @:{'terms.glossary.the'} @:{'terms.glossary.selected'} @:{'terms.glossary.period'}."
|
"xlvask_missing_order_link": "@:{'terms.glossary.xl'} @.capitalize:{'terms.glossary.vask'}-registreringen @:{'terms.glossary.er'} verken ignorert @:{'terms.glossary.eller'} knyttet @:{'terms.glossary.til'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.ordre'} @:{'terms.glossary.i'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.valgte'} @:{'terms.glossary.periode'}."
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"entities": {
|
"entities": {
|
||||||
|
|||||||
@@ -191,7 +191,8 @@
|
|||||||
"create_order_item_failed": "Vaskelinjen kunne ikke legges til i ordren.",
|
"create_order_item_failed": "Vaskelinjen kunne ikke legges til i ordren.",
|
||||||
"redirect_order_failed": "Ordren kunne ikke åpnes.",
|
"redirect_order_failed": "Ordren kunne ikke åpnes.",
|
||||||
"load_customers_failed": "Kundene kunne ikke hentes.",
|
"load_customers_failed": "Kundene kunne ikke hentes.",
|
||||||
"load_usage_log_failed": "Forbruksloggen kunne ikke hentes."
|
"load_usage_log_failed": "Forbruksloggen kunne ikke hentes.",
|
||||||
|
"api_endpoint_not_implemented": "API-endepunkt er ikke implementert ennå"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"vehicle_subscription_type_mismatch": "{product} @:{'terms.glossary.does'} @:{'terms.glossary.not'} @:{'terms.glossary.match'} @:{'terms.glossary.the'} @:{'terms.glossary.vehicle'} @:{'terms.glossary.subscription'} @:{'terms.glossary.type'} {expected_product}.",
|
"vehicle_subscription_type_mismatch": "{product} @:{'terms.glossary.does'} @:{'terms.glossary.not'} @:{'terms.glossary.match'} @:{'terms.glossary.the'} @:{'terms.glossary.vehicle'} @:{'terms.glossary.subscription'} @:{'terms.glossary.type'} {expected_product}.",
|
||||||
"wash_certificate_attached_without_item": "@.capitalize:{'terms.glossary.tvattcertifikat_2'} ar bifogat @:{'terms.glossary.utan'} @:{'terms.replication.host_definite_suffix'} tvattcertifikatrad.",
|
"wash_certificate_attached_without_item": "@.capitalize:{'terms.glossary.tvattcertifikat_2'} ar bifogat @:{'terms.glossary.utan'} @:{'terms.replication.host_definite_suffix'} tvattcertifikatrad.",
|
||||||
"wash_certificate_item_without_certificate": "@:{'terms.glossary.tvattcertifikatraden'} @:{'terms.glossary.finns'} @:{'terms.glossary.utan'} @:{'terms.glossary.ett'} @:{'terms.glossary.tvattcertifikat_2'}.",
|
"wash_certificate_item_without_certificate": "@:{'terms.glossary.tvattcertifikatraden'} @:{'terms.glossary.finns'} @:{'terms.glossary.utan'} @:{'terms.glossary.ett'} @:{'terms.glossary.tvattcertifikat_2'}.",
|
||||||
"xlvask_missing_order_link": "@:{'terms.glossary.xl'} @.capitalize:{'terms.glossary.vask'} @:{'terms.glossary.wash'} @:{'terms.glossary.is'} neither ignored nor @:{'terms.glossary.linked'} @:{'terms.glossary.to'} @:{'terms.glossary.an'} @:{'terms.glossary.order'} @:{'terms.glossary.in'} @:{'terms.glossary.the'} @:{'terms.glossary.selected'} @:{'terms.glossary.period'}."
|
"xlvask_missing_order_link": "@:{'terms.glossary.xl'} @.capitalize:{'terms.glossary.vask'}-registreringen @:{'terms.glossary.ar'} varken ignorerad @:{'terms.glossary.eller'} kopplad @:{'terms.glossary.till'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.order'} @:{'terms.glossary.i'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.valda'} perioden."
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"entities": {
|
"entities": {
|
||||||
|
|||||||
@@ -191,7 +191,8 @@
|
|||||||
"create_order_item_failed": "Tvättartikeln kunde inte läggas till i ordern.",
|
"create_order_item_failed": "Tvättartikeln kunde inte läggas till i ordern.",
|
||||||
"redirect_order_failed": "Ordern kunde inte öppnas.",
|
"redirect_order_failed": "Ordern kunde inte öppnas.",
|
||||||
"load_customers_failed": "Kunderna kunde inte hämtas.",
|
"load_customers_failed": "Kunderna kunde inte hämtas.",
|
||||||
"load_usage_log_failed": "Förbrukningsloggen kunde inte hämtas."
|
"load_usage_log_failed": "Förbrukningsloggen kunde inte hämtas.",
|
||||||
|
"api_endpoint_not_implemented": "API-slutpunkt är inte implementerad ännu"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import XLVaskUsagePagination from "@/components/displays/pagination/models/Depar
|
|||||||
<template #default>
|
<template #default>
|
||||||
<DepartmentDashboardHero />
|
<DepartmentDashboardHero />
|
||||||
<!-- Syncronize Orders -->
|
<!-- Syncronize Orders -->
|
||||||
<XLVaskUsagePagination/>
|
<XLVaskUsagePagination :department-id="SessionUser.functions.getDepartmentIdFromUrl()"/>
|
||||||
</template>
|
</template>
|
||||||
</NotFoundFallBackPageWrapper>
|
</NotFoundFallBackPageWrapper>
|
||||||
</DepartmentDashboardPageWrapper>
|
</DepartmentDashboardPageWrapper>
|
||||||
|
|||||||
+18
-3
@@ -240,9 +240,10 @@ const xlvaskUsageLogHtml = (flag: any) => {
|
|||||||
].filter(([, value]) => String(value ?? "").trim() !== "");
|
].filter(([, value]) => String(value ?? "").trim() !== "");
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return escapeHtml(translate("invoice_period.flags.preview.no_xlvask_usage_log", "No XL Vask details available.", {
|
return escapeHtml(translate(
|
||||||
entity: translate("invoice_period.flags.preview.entities.xlvask_usage_log", "XL Vask details"),
|
"invoice_period.flags.preview.no_xlvask_usage_log_metadata",
|
||||||
}));
|
"XL Vask registration (no metadata available — see api flag-context serializer)..."
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<table class="table is-narrow is-fullwidth">
|
return `<table class="table is-narrow is-fullwidth">
|
||||||
@@ -285,8 +286,18 @@ const openOrderItem = (flag: any) => {
|
|||||||
SessionUser.functions.redirectTo.department(departmentId, `modules/pos/orders/${orderId}${suffix}`, true);
|
SessionUser.functions.redirectTo.department(departmentId, `modules/pos/orders/${orderId}${suffix}`, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractDateOnly = (value: any) => {
|
||||||
|
const rawValue = String(value ?? "").trim();
|
||||||
|
if (rawValue === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(rawValue);
|
||||||
|
return match ? `${match[1]}-${match[2]}-${match[3]}` : "";
|
||||||
|
};
|
||||||
|
|
||||||
const openXlVaskUsageLog = (flag: any) => {
|
const openXlVaskUsageLog = (flag: any) => {
|
||||||
const usageLogId = Number(flag?.xlvask_usage_log_id || flag?.context?.xlvask_usage_log_id || flag?.target_id || 0);
|
const usageLogId = Number(flag?.xlvask_usage_log_id || flag?.context?.xlvask_usage_log_id || flag?.target_id || 0);
|
||||||
|
const startTime = extractDateOnly(flag?.context?.start_time || flag?.start_time);
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
activeTab: "period",
|
activeTab: "period",
|
||||||
periodView: "self_wash",
|
periodView: "self_wash",
|
||||||
@@ -296,6 +307,10 @@ const openXlVaskUsageLog = (flag: any) => {
|
|||||||
query.set("xlvaskUsageLogId", String(usageLogId));
|
query.set("xlvaskUsageLogId", String(usageLogId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (startTime !== "") {
|
||||||
|
query.set("xlvaskUsageLogStartTime", startTime);
|
||||||
|
}
|
||||||
|
|
||||||
SessionUser.functions.redirectTo.superUser(`/invoices?${query.toString()}`, true);
|
SessionUser.functions.redirectTo.superUser(`/invoices?${query.toString()}`, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+18
-2
@@ -5,12 +5,28 @@ import { computed } from "vue";
|
|||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
|
|
||||||
|
const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||||
|
|
||||||
|
const parseRouteDateOnly = (value: any) => {
|
||||||
|
const rawValue = Array.isArray(value) ? String(value[0] ?? "") : String(value ?? "");
|
||||||
|
const match = DATE_ONLY_PATTERN.exec(rawValue.trim());
|
||||||
|
if (!match) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `${match[1]}-${match[2]}-${match[3]}`;
|
||||||
|
};
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const highlightedUsageLogId = computed(() => {
|
const highlightedUsageLogId = computed(() => {
|
||||||
const parsedId = Number.parseInt(String(route.query.xlvaskUsageLogId || ""), 10);
|
const parsedId = Number.parseInt(String(route.query.xlvaskUsageLogId || ""), 10);
|
||||||
return Number.isInteger(parsedId) && parsedId > 0 ? parsedId : 0;
|
return Number.isInteger(parsedId) && parsedId > 0 ? parsedId : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const flaggedWashStartDate = computed(() => parseRouteDateOnly(route.query.xlvaskUsageLogStartTime));
|
||||||
|
|
||||||
|
const initialDateFrom = computed(() => flaggedWashStartDate.value || dates.computed.formattedStartDate.value);
|
||||||
|
const initialDateTo = computed(() => flaggedWashStartDate.value || dates.computed.formattedEndDate.value);
|
||||||
|
|
||||||
const selfWashTitle = computed(() => i18n.global.t("nav.self_wash"));
|
const selfWashTitle = computed(() => i18n.global.t("nav.self_wash"));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -18,8 +34,8 @@ const selfWashTitle = computed(() => i18n.global.t("nav.self_wash"));
|
|||||||
<section data-testid="invoicing-period-self-wash-view">
|
<section data-testid="invoicing-period-self-wash-view">
|
||||||
<XLVaskUsagePagination
|
<XLVaskUsagePagination
|
||||||
:title="selfWashTitle"
|
:title="selfWashTitle"
|
||||||
:initial-date-from="dates.computed.formattedStartDate.value"
|
:initial-date-from="initialDateFrom"
|
||||||
:initial-date-to="dates.computed.formattedEndDate.value"
|
:initial-date-to="initialDateTo"
|
||||||
:inherit-period-filters="true"
|
:inherit-period-filters="true"
|
||||||
:load-all-at-once="false"
|
:load-all-at-once="false"
|
||||||
:highlight-usage-log-id="highlightedUsageLogId"
|
:highlight-usage-log-id="highlightedUsageLogId"
|
||||||
|
|||||||
@@ -814,4 +814,49 @@ test.describe("i18n v2 catalog integrity", () => {
|
|||||||
|
|
||||||
expect(unexpectedGroups).toEqual([]);
|
expect(unexpectedGroups).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("mirrors xlvask_review keys into the global v2 fallback", () => {
|
||||||
|
const daSource = readJsonFile(
|
||||||
|
path.join(SOURCE_DIRECTORY, "da", "phrases", "compat", "invoicing_period", "xlvask_review.json")
|
||||||
|
);
|
||||||
|
const globalShared = readJsonFile(
|
||||||
|
path.join(SOURCE_DIRECTORY, "global", "shared", "invoicing_period", "xlvask_review.json")
|
||||||
|
);
|
||||||
|
|
||||||
|
const daEntries = flattenStringEntries(getValueAtPath(daSource, "compat.invoicing_period.xlvask_review"));
|
||||||
|
const globalEntries = flattenStringEntries(getValueAtPath(globalShared, "invoicing_period.xlvask_review"));
|
||||||
|
|
||||||
|
const daKeys = daEntries.map((entry) => entry.key).sort();
|
||||||
|
const globalKeys = globalEntries.map((entry) => entry.key).sort();
|
||||||
|
|
||||||
|
expect(globalKeys, "global fallback should mirror every da xlvask_review key").toEqual(daKeys);
|
||||||
|
|
||||||
|
const nonLinked = globalEntries.filter((entry) => !entry.value.startsWith("@")).map((entry) => entry.key);
|
||||||
|
|
||||||
|
expect(nonLinked, "every global xlvask_review entry should be a linked reference").toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mirrors xlvask_usage_log flag keys into the global v2 fallback", () => {
|
||||||
|
const daSource = readJsonFile(
|
||||||
|
path.join(SOURCE_DIRECTORY, "da", "phrases", "compat", "invoice_period", "flags.json")
|
||||||
|
);
|
||||||
|
const globalShared = readJsonFile(path.join(SOURCE_DIRECTORY, "global", "shared", "invoice_period", "flags.json"));
|
||||||
|
|
||||||
|
const daEntries = flattenStringEntries(getValueAtPath(daSource, "compat.invoice_period.flags")).filter(
|
||||||
|
(entry) => entry.key.includes("xlvask_usage_log") || entry.key.includes("xlvask_missing_order_link")
|
||||||
|
);
|
||||||
|
const globalEntries = flattenStringEntries(getValueAtPath(globalShared, "invoice_period.flags")).filter(
|
||||||
|
(entry) => entry.key.includes("xlvask_usage_log") || entry.key.includes("xlvask_missing_order_link")
|
||||||
|
);
|
||||||
|
|
||||||
|
const daKeySet = new Set(daEntries.map((entry) => entry.key));
|
||||||
|
const globalKeySet = new Set(globalEntries.map((entry) => entry.key));
|
||||||
|
|
||||||
|
const missing = [...daKeySet].filter((key) => !globalKeySet.has(key));
|
||||||
|
expect(missing, "global flags fallback should mirror every da xlvask flag key").toEqual([]);
|
||||||
|
|
||||||
|
const nonLinked = globalEntries.filter((entry) => !entry.value.startsWith("@")).map((entry) => entry.key);
|
||||||
|
|
||||||
|
expect(nonLinked, "every mirrored global xlvask flag entry should be a linked reference").toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
||||||
|
import { isDesktopProject } from "./support/projects";
|
||||||
|
|
||||||
|
const XL_VASK_USAGE_LOG_ID = 55;
|
||||||
|
const PERIOD_FROM_DATE = "2026-07-01";
|
||||||
|
const PERIOD_TO_DATE = "2026-07-31";
|
||||||
|
|
||||||
|
const PERIOD_VIEW_URL = `/superuser/invoices?activeTab=period&startDate=${PERIOD_FROM_DATE}&endDate=${PERIOD_TO_DATE}&periodView=all`;
|
||||||
|
const SELVVASH_DESTINATION_PATTERN = new RegExp(
|
||||||
|
"/superuser/invoices\\?activeTab=period&periodView=self_wash&xlvaskUsageLogId=" + XL_VASK_USAGE_LOG_ID
|
||||||
|
);
|
||||||
|
|
||||||
|
function json(body: unknown, status = 200) {
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createXlVaskFlaggedPeriodPayload({ dateFrom = PERIOD_FROM_DATE } = {}) {
|
||||||
|
const fixtureDate = String(dateFrom).slice(0, 10);
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
types: {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
customer_number: 4001,
|
||||||
|
customer_name: "Acme Fleet",
|
||||||
|
requires_action: true,
|
||||||
|
transactions: [
|
||||||
|
{
|
||||||
|
id: 9001,
|
||||||
|
date: `${fixtureDate}T10:00:00.000Z`,
|
||||||
|
amount: 120,
|
||||||
|
booked: false,
|
||||||
|
excluded: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queue: { has_active_job: false, statuses: [], invoice_collection_ids: [], is_action_blocked: false },
|
||||||
|
meta: {},
|
||||||
|
flags: [
|
||||||
|
{
|
||||||
|
id: "auto-xlvask-missing-1",
|
||||||
|
source: "automatic",
|
||||||
|
severity: "yellow",
|
||||||
|
status: "active",
|
||||||
|
target_type: "xlvask_usage_log",
|
||||||
|
target_id: XL_VASK_USAGE_LOG_ID,
|
||||||
|
customer_number: 4001,
|
||||||
|
definition_key: "xlvask_missing_order_link",
|
||||||
|
fingerprint: "xlvask-missing-fingerprint-1",
|
||||||
|
message_key: "invoice_period.flags.automatic.xlvask_missing_order_link",
|
||||||
|
message_params: {
|
||||||
|
wash_id: `wash-${XL_VASK_USAGE_LOG_ID}`,
|
||||||
|
registration_number: "AB12345",
|
||||||
|
},
|
||||||
|
message: "XL Vask wash is neither ignored nor linked to an order in the selected period.",
|
||||||
|
context: {
|
||||||
|
customer_name: "Acme Fleet",
|
||||||
|
xlvask_usage_log_id: XL_VASK_USAGE_LOG_ID,
|
||||||
|
wash_id: `wash-${XL_VASK_USAGE_LOG_ID}`,
|
||||||
|
registration_number: "AB12345",
|
||||||
|
start_time: `${fixtureDate}T10:00:00.000Z`,
|
||||||
|
},
|
||||||
|
xlvask_usage_log_id: XL_VASK_USAGE_LOG_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
flag_counts: { manual: 0, automatic: 1, total: 1 },
|
||||||
|
status_indicator: "flag_yellow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invoice_per_order: [],
|
||||||
|
fixed_pricing: [],
|
||||||
|
tank_cleaning: [],
|
||||||
|
special_arrangements: [],
|
||||||
|
vehicle_subscriptions: [],
|
||||||
|
possible_duplicates: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function suppressVueDevtoolsOverlay(page) {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
const STYLE_ID = "__e2e-hide-vue-devtools";
|
||||||
|
localStorage.setItem("lastVersionCheck", String(Date.now()));
|
||||||
|
|
||||||
|
const apply = () => {
|
||||||
|
const target = document.head || document.documentElement;
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!document.getElementById(STYLE_ID)) {
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.id = STYLE_ID;
|
||||||
|
style.textContent =
|
||||||
|
"#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
|
||||||
|
target.appendChild(style);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
apply();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function primePeriodViewApi(page) {
|
||||||
|
return page.route("**/superuser/invoicing/period**", async (route) => {
|
||||||
|
if (route.request().method() !== "GET") {
|
||||||
|
await route.fallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(route.request().url());
|
||||||
|
const dateFrom = url.searchParams.get("dateFrom") || PERIOD_FROM_DATE;
|
||||||
|
|
||||||
|
await route.fulfill(json(createXlVaskFlaggedPeriodPayload({ dateFrom })));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("XL Vask flag → Selvvash navigation", () => {
|
||||||
|
test("@smoke flag token opens the Selvvash tab with the highlighted wash", async ({ page, context }, testInfo) => {
|
||||||
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
|
const token = "superuser-xl-vask-navigation-token";
|
||||||
|
await suppressVueDevtoolsOverlay(page);
|
||||||
|
await seedAuthenticatedState(page, token);
|
||||||
|
await mockApi(page, {
|
||||||
|
authenticated: true,
|
||||||
|
permissions: ["superuser", "user"],
|
||||||
|
loginToken: token,
|
||||||
|
});
|
||||||
|
|
||||||
|
await primePeriodViewApi(page);
|
||||||
|
|
||||||
|
await context.addInitScript((tokenValue) => {
|
||||||
|
window.localStorage.setItem("token", tokenValue);
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
await page.goto(PERIOD_VIEW_URL, { waitUntil: "domcontentloaded" });
|
||||||
|
await expect(page).toHaveURL(/activeTab=period/);
|
||||||
|
await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible({ timeout: 30_000 });
|
||||||
|
|
||||||
|
const customerRow = page.getByTestId("invoicing-period-customer-4001");
|
||||||
|
await expect(customerRow).toBeVisible();
|
||||||
|
|
||||||
|
const flagRow = page.getByTestId("invoice-period-flag-auto-xlvask-missing-1");
|
||||||
|
const flagRowCount = await flagRow.count();
|
||||||
|
test.skip(
|
||||||
|
flagRowCount === 0,
|
||||||
|
"No xlvask_missing_order_link flag is visible in the current period payload; smoke run skips."
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(flagRow).toBeVisible();
|
||||||
|
const flagToken = flagRow.locator(".invoice-period-flag-token");
|
||||||
|
await expect(flagToken).toBeVisible();
|
||||||
|
|
||||||
|
const popupPromise = page.waitForEvent("popup", { timeout: 15_000 });
|
||||||
|
await flagToken.click();
|
||||||
|
const popup = await popupPromise;
|
||||||
|
|
||||||
|
await expect(popup).toHaveURL(SELVVASH_DESTINATION_PATTERN);
|
||||||
|
await popup.waitForLoadState("domcontentloaded").catch(() => {});
|
||||||
|
|
||||||
|
await popup.close();
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// Regression guard for TRU-13 / AUT-9: every `invoicing_period.xlvask_review.*`
|
||||||
|
// key that exists in the da reference catalogue must also be present in
|
||||||
|
// the other active locales (no, sv, de, en) with a non-empty translation.
|
||||||
|
// The end-to-end integrity suite already enforces global key parity, but
|
||||||
|
// this targeted test documents the acceptance criteria for the xlvask_review
|
||||||
|
// translation work and surfaces locale-specific gaps immediately.
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const root = process.cwd();
|
||||||
|
const activeLocales = ["da", "no", "sv", "de", "en"];
|
||||||
|
const reviewFileFor = (locale) =>
|
||||||
|
join(root, `src/i18n/source/${locale}/phrases/compat/invoicing_period/xlvask_review.json`);
|
||||||
|
|
||||||
|
const flattenPairs = (node, prefix = "") => {
|
||||||
|
if (typeof node === "string") {
|
||||||
|
return [{ key: prefix, value: node }];
|
||||||
|
}
|
||||||
|
if (!node || typeof node !== "object" || Array.isArray(node)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Object.entries(node).flatMap(([key, value]) => {
|
||||||
|
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
||||||
|
return flattenPairs(value, nextPrefix);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const readReviewSection = (locale) => {
|
||||||
|
const file = JSON.parse(readFileSync(reviewFileFor(locale), "utf8"));
|
||||||
|
return file?.compat?.invoicing_period?.xlvask_review ?? {};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("xlvask_review translation coverage", () => {
|
||||||
|
it("covers every da xlvask_review key in no, sv, de, en with a non-empty value", () => {
|
||||||
|
const daEntries = flattenPairs(readReviewSection("da"));
|
||||||
|
expect(daEntries.length, "da xlvask_review should expose translatable strings").toBeGreaterThan(0);
|
||||||
|
|
||||||
|
for (const locale of activeLocales.filter((entry) => entry !== "da")) {
|
||||||
|
const localeEntries = new Map(flattenPairs(readReviewSection(locale)).map((entry) => [entry.key, entry.value]));
|
||||||
|
|
||||||
|
const missing = [];
|
||||||
|
const empty = [];
|
||||||
|
for (const { key } of daEntries) {
|
||||||
|
if (!localeEntries.has(key)) {
|
||||||
|
missing.push(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const localized = localeEntries.get(key);
|
||||||
|
if (typeof localized !== "string" || localized.trim().length === 0) {
|
||||||
|
empty.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(missing, `${locale} xlvask_review keys missing from da`).toEqual([]);
|
||||||
|
expect(empty, `${locale} xlvask_review keys with empty translations`).toEqual([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -55,6 +55,8 @@ const i18n = createTestI18n({
|
|||||||
preview: {
|
preview: {
|
||||||
no_order_items: "No order items available.",
|
no_order_items: "No order items available.",
|
||||||
no_xlvask_usage_log: "No XL Vask details available.",
|
no_xlvask_usage_log: "No XL Vask details available.",
|
||||||
|
no_xlvask_usage_log_metadata:
|
||||||
|
"XL Vask registration (no metadata available — see api flag-context serializer)...",
|
||||||
product: "Product",
|
product: "Product",
|
||||||
quantity: "Qty",
|
quantity: "Qty",
|
||||||
price: "Price",
|
price: "Price",
|
||||||
@@ -390,11 +392,90 @@ describe("InvoicingPeriodFlagList", () => {
|
|||||||
|
|
||||||
await token.trigger("click");
|
await token.trigger("click");
|
||||||
expect(SessionUser.functions.redirectTo.superUser).toHaveBeenCalledWith(
|
expect(SessionUser.functions.redirectTo.superUser).toHaveBeenCalledWith(
|
||||||
"/invoices?activeTab=period&periodView=self_wash&xlvaskUsageLogId=55",
|
"/invoices?activeTab=period&periodView=self_wash&xlvaskUsageLogId=55&xlvaskUsageLogStartTime=2026-05-11",
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("propagates the flagged wash start time when navigating to the Selvvask view", async () => {
|
||||||
|
const wrapper = mountList([
|
||||||
|
{
|
||||||
|
id: "auto-xlvask-root",
|
||||||
|
source: "automatic",
|
||||||
|
fingerprint: "xlvask-root",
|
||||||
|
definition_key: "xlvask_missing_order_link",
|
||||||
|
message_key: "invoice_period.flags.automatic.xlvask_missing_order_link",
|
||||||
|
target_type: "xlvask_usage_log",
|
||||||
|
target_id: 77,
|
||||||
|
xlvask_usage_log_id: 77,
|
||||||
|
context: {
|
||||||
|
xlvask_usage_log_id: 77,
|
||||||
|
start_time: "2026-04-28T08:15:00",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await wrapper.get(".invoice-period-flag-token").trigger("click");
|
||||||
|
expect(SessionUser.functions.redirectTo.superUser).toHaveBeenLastCalledWith(
|
||||||
|
"/invoices?activeTab=period&periodView=self_wash&xlvaskUsageLogId=77&xlvaskUsageLogStartTime=2026-04-28",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("navigates to Selvvask without a start-time query when the flag has no wash date", async () => {
|
||||||
|
const wrapper = mountList([
|
||||||
|
{
|
||||||
|
id: "auto-xlvask-no-date",
|
||||||
|
source: "automatic",
|
||||||
|
fingerprint: "xlvask-no-date",
|
||||||
|
definition_key: "xlvask_missing_order_link",
|
||||||
|
message_key: "invoice_period.flags.automatic.xlvask_missing_order_link",
|
||||||
|
target_type: "xlvask_usage_log",
|
||||||
|
target_id: 91,
|
||||||
|
xlvask_usage_log_id: 91,
|
||||||
|
context: {
|
||||||
|
xlvask_usage_log_id: 91,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await wrapper.get(".invoice-period-flag-token").trigger("click");
|
||||||
|
expect(SessionUser.functions.redirectTo.superUser).toHaveBeenLastCalledWith(
|
||||||
|
"/invoices?activeTab=period&periodView=self_wash&xlvaskUsageLogId=91",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the API flag-context serializer does not provide any of the
|
||||||
|
// identity fields (wash_id / registration_number / start_time / customer_name),
|
||||||
|
// the hover preview should not pretend the data is missing — it should
|
||||||
|
// tell the operator that the metadata simply isn't being supplied.
|
||||||
|
it("explains the empty XL Vask hover preview when flag context has no metadata fields", async () => {
|
||||||
|
const wrapper = mountList([
|
||||||
|
{
|
||||||
|
id: "auto-xlvask-empty",
|
||||||
|
source: "automatic",
|
||||||
|
fingerprint: "xlvask-empty-fingerprint",
|
||||||
|
definition_key: "xlvask_missing_order_link",
|
||||||
|
message_key: "invoice_period.flags.automatic.xlvask_missing_order_link",
|
||||||
|
target_type: "xlvask_usage_log",
|
||||||
|
target_id: 99,
|
||||||
|
xlvask_usage_log_id: 99,
|
||||||
|
context: {},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const token = wrapper.get(".invoice-period-flag-token");
|
||||||
|
expect(token.text()).toBe("XL Vask wash");
|
||||||
|
|
||||||
|
await token.trigger("mouseover");
|
||||||
|
expect(popperBoxMock).toHaveBeenLastCalledWith(
|
||||||
|
"XL Vask registration",
|
||||||
|
"XL Vask registration (no metadata available — see api flag-context serializer)..."
|
||||||
|
);
|
||||||
|
expect(showPopperMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("renders invoice period warnings in proper Danish", () => {
|
it("renders invoice period warnings in proper Danish", () => {
|
||||||
const wrapper = mountList(
|
const wrapper = mountList(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -2,8 +2,16 @@ import { describe, expect, it } from "vitest";
|
|||||||
import {
|
import {
|
||||||
getSelfServeCompletedDynamicImageStep,
|
getSelfServeCompletedDynamicImageStep,
|
||||||
getSelfServeTaskDynamicImagePresentation,
|
getSelfServeTaskDynamicImagePresentation,
|
||||||
|
isSelfServeProgramNumberButton,
|
||||||
normalizeSelfServeProgramPickerTaskButtons,
|
normalizeSelfServeProgramPickerTaskButtons,
|
||||||
|
parseSelfServeDynamicImageThumbPosition,
|
||||||
} from "@/services/selfServeDynamicImage.js";
|
} from "@/services/selfServeDynamicImage.js";
|
||||||
|
import {
|
||||||
|
SELF_SERVE_TASK_BUTTON_OPTIONS,
|
||||||
|
SELF_SERVE_TASK_BUTTON_PROGRAM_PICKER,
|
||||||
|
SELF_SERVE_TASK_BUTTON_RESET,
|
||||||
|
SELF_SERVE_TASK_BUTTON_START,
|
||||||
|
} from "@/components/session/token/SessionUser/Objects/selfServeTaskButtons.js";
|
||||||
|
|
||||||
describe("selfServeDynamicImage", () => {
|
describe("selfServeDynamicImage", () => {
|
||||||
it("uses a selected program picker button number as thumb position", () => {
|
it("uses a selected program picker button number as thumb position", () => {
|
||||||
@@ -70,4 +78,55 @@ describe("selfServeDynamicImage", () => {
|
|||||||
"start",
|
"start",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Smoke check that the program number range is stable across the codebase.
|
||||||
|
// The wash bay exposes 12 programs (buttons 0-11, thumb positions 1-12).
|
||||||
|
// If this assertion starts failing, the contract with the API has changed
|
||||||
|
// and any program mapping issue (e.g. a program that is unresponsive on
|
||||||
|
// the bay) is likely the API repository's concern, not the frontend.
|
||||||
|
it("accepts every configured program number between 0 and 11", () => {
|
||||||
|
for (let programNumber = 0; programNumber <= 11; programNumber += 1) {
|
||||||
|
expect(isSelfServeProgramNumberButton(programNumber)).toBe(true);
|
||||||
|
}
|
||||||
|
expect(isSelfServeProgramNumberButton(-1)).toBe(false);
|
||||||
|
expect(isSelfServeProgramNumberButton(12)).toBe(false);
|
||||||
|
expect(isSelfServeProgramNumberButton(100)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps 1-indexed thumb positions 1..12 to the documented program range", () => {
|
||||||
|
for (let thumbPosition = 1; thumbPosition <= 12; thumbPosition += 1) {
|
||||||
|
expect(parseSelfServeDynamicImageThumbPosition(thumbPosition)).toBe(thumbPosition);
|
||||||
|
}
|
||||||
|
expect(parseSelfServeDynamicImageThumbPosition(0)).toBeNull();
|
||||||
|
expect(parseSelfServeDynamicImageThumbPosition(13)).toBeNull();
|
||||||
|
expect(parseSelfServeDynamicImageThumbPosition("not-a-number")).toBeNull();
|
||||||
|
expect(parseSelfServeDynamicImageThumbPosition(null)).toBeNull();
|
||||||
|
expect(parseSelfServeDynamicImageThumbPosition(undefined)).toBeNull();
|
||||||
|
expect(parseSelfServeDynamicImageThumbPosition("")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smoke check that the program button registry still covers every configured
|
||||||
|
// program. The wash bay exposes 12 programs and the path editor / simulator
|
||||||
|
// UI renders one entry per program number from this array. If the array is
|
||||||
|
// ever shortened, lengthened, or has gaps/duplicates, the program list shown
|
||||||
|
// to operators will drift from the API mapping and operators will not be
|
||||||
|
// able to reach every configured program — which is exactly the failure mode
|
||||||
|
// described in TRU-19 ("FF Uvs" and "10min" unresponsive).
|
||||||
|
it("exposes 12 unique, sequential program entries in the button registry", () => {
|
||||||
|
const numericProgramEntries = SELF_SERVE_TASK_BUTTON_OPTIONS.filter(
|
||||||
|
(entry) => Number.isInteger(entry?.id) && entry.id >= 0 && entry.id <= 11
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(numericProgramEntries).toHaveLength(12);
|
||||||
|
expect(numericProgramEntries.map((entry) => entry.id)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
|
||||||
|
expect(new Set(numericProgramEntries.map((entry) => entry.id)).size).toBe(12);
|
||||||
|
expect(new Set(numericProgramEntries.map((entry) => entry.name)).size).toBe(12);
|
||||||
|
|
||||||
|
// The three "special" buttons (reset / program picker / start) must still
|
||||||
|
// be present so the path editor can render the full UI.
|
||||||
|
const specialIds = new Set(SELF_SERVE_TASK_BUTTON_OPTIONS.map((entry) => entry?.id));
|
||||||
|
expect(specialIds.has(SELF_SERVE_TASK_BUTTON_RESET)).toBe(true);
|
||||||
|
expect(specialIds.has(SELF_SERVE_TASK_BUTTON_PROGRAM_PICKER)).toBe(true);
|
||||||
|
expect(specialIds.has(SELF_SERVE_TASK_BUTTON_START)).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -577,8 +577,11 @@ describe("Periode tab contract", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("uses the selected period while keeping Selvvask results server-paginated", () => {
|
it("uses the selected period while keeping Selvvask results server-paginated", () => {
|
||||||
expect(periodViewSelfWashSource).toContain(':initial-date-from="dates.computed.formattedStartDate.value"');
|
expect(periodViewSelfWashSource).toContain(':initial-date-from="initialDateFrom"');
|
||||||
expect(periodViewSelfWashSource).toContain(':initial-date-to="dates.computed.formattedEndDate.value"');
|
expect(periodViewSelfWashSource).toContain(':initial-date-to="initialDateTo"');
|
||||||
|
expect(periodViewSelfWashSource).toContain("dates.computed.formattedStartDate.value");
|
||||||
|
expect(periodViewSelfWashSource).toContain("dates.computed.formattedEndDate.value");
|
||||||
|
expect(periodViewSelfWashSource).toContain("route.query.xlvaskUsageLogStartTime");
|
||||||
expect(periodViewSelfWashSource).toContain(':inherit-period-filters="true"');
|
expect(periodViewSelfWashSource).toContain(':inherit-period-filters="true"');
|
||||||
expect(periodViewSelfWashSource).toContain(':load-all-at-once="false"');
|
expect(periodViewSelfWashSource).toContain(':load-all-at-once="false"');
|
||||||
expect(xlvaskUsagePaginationSource).toContain("inheritPeriodFilters");
|
expect(xlvaskUsagePaginationSource).toContain("inheritPeriodFilters");
|
||||||
@@ -684,6 +687,27 @@ describe("Periode tab contract", () => {
|
|||||||
expect(xlvaskUsageOrdersTableSource).not.toContain("'preview'");
|
expect(xlvaskUsageOrdersTableSource).not.toContain("'preview'");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders a friendly notice when the orders endpoint returns 404", () => {
|
||||||
|
// The pagination must distinguish a 404 ("API endpoint not implemented
|
||||||
|
// yet") from other failures and surface a friendly notice instead of
|
||||||
|
// the generic error popper every other paginated surface uses.
|
||||||
|
expect(xlvaskUsagePaginationSource).toContain("apiEndpointNotImplemented");
|
||||||
|
expect(xlvaskUsagePaginationSource).toContain("lastError");
|
||||||
|
expect(xlvaskUsagePaginationSource).toContain("404");
|
||||||
|
expect(xlvaskUsagePaginationSource).toContain("xlvask_review.errors.api_endpoint_not_implemented");
|
||||||
|
expect(xlvaskUsagePaginationSource).toContain("xlvask-api-not-implemented");
|
||||||
|
expect(xlvaskUsagePaginationSource).toContain('removeError("paginatedGetRequest")');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ships the API-not-implemented notice in every supported locale", () => {
|
||||||
|
// The friendly notice must be present in all five source locales so
|
||||||
|
// the operator reads a real translation rather than a raw key.
|
||||||
|
for (const locale of localeMessages) {
|
||||||
|
const notice = locale.messages?.invoicing_period?.xlvask_review?.errors?.api_endpoint_not_implemented;
|
||||||
|
expect(notice, `locale ${locale.locale} missing api_endpoint_not_implemented`).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps period subview navigation on valid keys", () => {
|
it("keeps period subview navigation on valid keys", () => {
|
||||||
expect(periodViewInvoicePerOrderSource).toContain("view.functions.setCurrentView('vehicle_subscriptions')");
|
expect(periodViewInvoicePerOrderSource).toContain("view.functions.setCurrentView('vehicle_subscriptions')");
|
||||||
expect(periodViewSpecialArrangementsSource).toContain("view.functions.setCurrentView('vehicle_subscriptions')");
|
expect(periodViewSpecialArrangementsSource).toContain("view.functions.setCurrentView('vehicle_subscriptions')");
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const root = process.cwd();
|
||||||
|
const readSource = (relativePath) => readFileSync(join(root, relativePath), "utf8");
|
||||||
|
|
||||||
|
describe("xlvask usage pagination department selector propagation", () => {
|
||||||
|
it("declares a departmentId prop on XLVaskUsagePagination", () => {
|
||||||
|
const source = readSource("src/components/displays/pagination/models/DepartmentPos/XLVaskUsagePagination.vue");
|
||||||
|
|
||||||
|
expect(source).toMatch(/departmentId:\s*\{\s*type:\s*Number,\s*default:\s*0\s*\}/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies the HallId filter when the departmentId prop is provided", () => {
|
||||||
|
const source = readSource("src/components/displays/pagination/models/DepartmentPos/XLVaskUsagePagination.vue");
|
||||||
|
|
||||||
|
expect(source).toMatch(/effectiveDepartmentId\s*>\s*0/);
|
||||||
|
expect(source).toMatch(/setFilter\(\s*["']HallId["']\s*,\s*effectiveDepartmentId\s*,\s*false\s*\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the departmentId route param when the prop is not provided", () => {
|
||||||
|
const source = readSource("src/components/displays/pagination/models/DepartmentPos/XLVaskUsagePagination.vue");
|
||||||
|
|
||||||
|
expect(source).toMatch(/router\.currentRoute\.value\.params\.departmentId/);
|
||||||
|
expect(source).toMatch(/Number\.parseInt\(\s*String\(router\.currentRoute\.value\.params\.departmentId/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not apply the HallId filter when no departmentId is provided", () => {
|
||||||
|
const source = readSource("src/components/displays/pagination/models/DepartmentPos/XLVaskUsagePagination.vue");
|
||||||
|
|
||||||
|
expect(source).toContain(
|
||||||
|
"const effectiveDepartmentId =\n props.departmentId > 0\n ? props.departmentId\n : Number.isInteger(routeDepartmentId) && routeDepartmentId > 0\n ? routeDepartmentId\n : 0;"
|
||||||
|
);
|
||||||
|
expect(source).toMatch(/if\s*\(effectiveDepartmentId\s*>\s*0\)\s*\{\s*setFilter\(\s*["']HallId["']/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("DepartmentPosSync forwards the URL departmentId to XLVaskUsagePagination", () => {
|
||||||
|
const source = readSource("src/views/dashboards/departmentDashboard/modules/Pos/DepartmentPosSync.vue");
|
||||||
|
|
||||||
|
expect(source).toMatch(
|
||||||
|
/<XLVaskUsagePagination[^>]*:department-id="SessionUser\.functions\.getDepartmentIdFromUrl\(\)"/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("InvoicingBillingPeriodViewSelfWash does not pass a departmentId", () => {
|
||||||
|
const source = readSource(
|
||||||
|
"src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewSelfWash.vue"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(source).not.toMatch(/department-id[\s=]/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import axios from "axios";
|
||||||
|
import { usePaginatedList } from "@/components/pagination/paginatedList.vue";
|
||||||
|
|
||||||
|
vi.mock("axios", () => ({
|
||||||
|
default: {
|
||||||
|
get: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("paginatedList tracks the last error so consumers can detect 404", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
localStorage.clear();
|
||||||
|
localStorage.setItem("token", "test-token");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes a reactive lastError ref and clears it on success", async () => {
|
||||||
|
axios.get.mockResolvedValueOnce({
|
||||||
|
data: { data: [{ id: 1 }], meta: { pagination: { page: 1, per_page: 100, total: 1 } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = usePaginatedList();
|
||||||
|
list.setEndpoint("/modules/xlvask/services/usage/orders", false);
|
||||||
|
expect(list.lastError.value).toBeNull();
|
||||||
|
|
||||||
|
await list.paginatedGetRequest();
|
||||||
|
|
||||||
|
expect(list.lastError.value).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures the HTTP status of a failed request for the current endpoint", async () => {
|
||||||
|
axios.get.mockRejectedValueOnce({
|
||||||
|
response: { status: 404, data: { data: { message: "Not Found" } } },
|
||||||
|
message: "Request failed with status code 404",
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = usePaginatedList();
|
||||||
|
list.setEndpoint("/modules/xlvask/services/usage/orders", false);
|
||||||
|
|
||||||
|
const result = await list.paginatedGetRequest();
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
expect(list.lastError.value).toEqual({
|
||||||
|
status: 404,
|
||||||
|
endpoint: "/modules/xlvask/services/usage/orders",
|
||||||
|
message: "Not Found",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets lastError at the start of every request", async () => {
|
||||||
|
axios.get
|
||||||
|
.mockRejectedValueOnce({
|
||||||
|
response: { status: 500, data: { data: { message: "Server Error" } } },
|
||||||
|
message: "Request failed with status code 500",
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
data: { data: [], meta: { pagination: { page: 1, per_page: 100, total: 0 } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = usePaginatedList();
|
||||||
|
list.setEndpoint("/modules/xlvask/services/usage/orders", false);
|
||||||
|
|
||||||
|
await list.paginatedGetRequest();
|
||||||
|
expect(list.lastError.value?.status).toBe(500);
|
||||||
|
|
||||||
|
await list.paginatedGetRequest();
|
||||||
|
expect(list.lastError.value).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not set lastError when the request was cancelled", async () => {
|
||||||
|
axios.get.mockRejectedValueOnce({
|
||||||
|
name: "CanceledError",
|
||||||
|
code: "ERR_CANCELED",
|
||||||
|
message: "canceled",
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = usePaginatedList();
|
||||||
|
list.setEndpoint("/modules/xlvask/services/usage/orders", false);
|
||||||
|
|
||||||
|
await list.paginatedGetRequest();
|
||||||
|
|
||||||
|
expect(list.lastError.value).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,63 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>AUT-1 primary-button hover preview</title>
|
||||||
|
<!--
|
||||||
|
Bulma 1.x stylesheet from the project's node_modules so the preview
|
||||||
|
matches the real app's default theme. The :root override is the same
|
||||||
|
--bulma-primary-h override that lives in src/assets/main.css so the
|
||||||
|
primary colour renders as the brand teal-blue (#0787BB ≈ HSL 197deg).
|
||||||
|
-->
|
||||||
|
<link rel="stylesheet" href="../../../node_modules/bulma/css/bulma.min.css" />
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bulma-primary-h: 197deg;
|
||||||
|
--bulma-primary-s: 92%;
|
||||||
|
--bulma-primary-l: 38%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: #f5f5f5;
|
||||||
|
margin: 0;
|
||||||
|
padding: 48px 64px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
.stage {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 48px;
|
||||||
|
background: white;
|
||||||
|
padding: 40px 56px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(10, 10, 10, 0.12);
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7a7a7a;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.button.is-primary {
|
||||||
|
min-width: 168px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="stage-idle" class="stage" data-state="idle">
|
||||||
|
<div class="label">Primary button — default</div>
|
||||||
|
<button class="button is-primary" data-testid="primary-button-idle">
|
||||||
|
Confirm booking
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style="height: 32px"></div>
|
||||||
|
<div id="stage-hover" class="stage" data-state="hover">
|
||||||
|
<div class="label">Primary button — hover</div>
|
||||||
|
<button class="button is-primary" data-testid="primary-button-hover">
|
||||||
|
Confirm booking
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user