Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa1e915b9c | ||
|
|
5cc22f14bc | ||
|
|
8519af5cbb | ||
|
|
3fc4be8335 | ||
|
|
c41954993c | ||
|
|
36c1f10de8 | ||
|
|
4378dad2b9 | ||
|
|
ee41366c08 | ||
|
|
e22f78a449 | ||
|
|
0e9292d6d9 | ||
|
|
69250ada66 | ||
|
|
ce844137a0 | ||
|
|
7f3a8c07e2 | ||
|
|
bd14d58f08 | ||
|
|
b8494cd1d9 | ||
|
|
1a1c3db8e5 | ||
|
|
49772c1334 | ||
|
|
02ddb99000 |
@@ -102,7 +102,7 @@ jobs:
|
||||
needs: build-and-unit
|
||||
name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }}
|
||||
runs-on: [self-hosted, Linux, X64, pleno, frontend, docker]
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
@@ -112,6 +112,8 @@ jobs:
|
||||
env:
|
||||
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-pr-${{ matrix.suite }}-${{ matrix.project }}
|
||||
PLAYWRIGHT_REPORTER_MODE: line-html
|
||||
PLAYWRIGHT_WORKERS: 1
|
||||
PLAYWRIGHT_VIDEO_MODE: on-first-retry
|
||||
steps:
|
||||
- name: Repair self-hosted workspace permissions
|
||||
shell: bash
|
||||
@@ -140,20 +142,24 @@ jobs:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PUSH_BEFORE_SHA: ${{ github.event.before }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
zero_sha="0000000000000000000000000000000000000000"
|
||||
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_BASE_SHA" ]]; then
|
||||
base_ref="$PR_BASE_SHA"
|
||||
head_ref="$PR_HEAD_SHA"
|
||||
elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then
|
||||
git fetch --no-tags --prune origin "$DEFAULT_BRANCH"
|
||||
base_ref="origin/$DEFAULT_BRANCH"
|
||||
head_ref="$HEAD_SHA"
|
||||
else
|
||||
base_ref="$PUSH_BEFORE_SHA"
|
||||
head_ref="$HEAD_SHA"
|
||||
fi
|
||||
echo "base=$base_ref" >> "$GITHUB_OUTPUT"
|
||||
echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "head=$head_ref" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
@@ -223,6 +229,8 @@ jobs:
|
||||
--env CI="${CI:-}" \
|
||||
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
|
||||
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
|
||||
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
|
||||
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
|
||||
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
|
||||
--env MATRIX_SUITE="$MATRIX_SUITE" \
|
||||
--env MATRIX_PROJECT="$MATRIX_PROJECT" \
|
||||
|
||||
@@ -7,14 +7,18 @@ export const fallbackChangePatterns = [
|
||||
/^vite\.config\.js$/u,
|
||||
/^playwright(?:\..+)?\.config\.(?:js|ts)$/u,
|
||||
/^playwright\.global-(?:setup|teardown)\.mjs$/u,
|
||||
/^scripts\/run-playwright-(?:pr|ci-parallel|batched-chromium)\.mjs$/u,
|
||||
/^scripts\/run-playwright-(?:ci-parallel|batched-chromium)\.mjs$/u,
|
||||
/^tests\/e2e\/(?:support|fixtures)\//u,
|
||||
];
|
||||
|
||||
export const sourceMappings = [
|
||||
{
|
||||
name: "auth",
|
||||
patterns: [/^src\/(?:views|components|middleware)\/.*auth/iu, /^src\/views\/auth\//u, /^src\/components\/session\//u],
|
||||
patterns: [
|
||||
/^src\/(?:views|components|middleware)\/.*auth/iu,
|
||||
/^src\/views\/auth\//u,
|
||||
/^src\/components\/session\/(?!token\/SessionUser\/Objects\/)/u,
|
||||
],
|
||||
specs: ["tests/e2e/auth.smoke.spec.js", "tests/e2e/userAuth.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
@@ -70,6 +74,12 @@ export const sourceMappings = [
|
||||
specs: ["tests/e2e/admin-department-notifications.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "limited-backoffice",
|
||||
patterns: [/^src\/views\/backoffice\/LimitedBackoffice/u],
|
||||
specs: ["tests/e2e/limited-backoffice.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "invoicing",
|
||||
patterns: [/invoic/iu, /economic[-/]?queue/iu, /collected-order/iu],
|
||||
@@ -92,6 +102,15 @@ export const sourceMappings = [
|
||||
specs: ["tests/e2e/superuser-system-status.smoke.spec.js"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "superuser-department-pricing",
|
||||
patterns: [
|
||||
/^src\/views\/dashboards\/superUserDashboard\/department\/(?:DepartmentPricing|SuperUserSelectedDepartmentObject)\.vue$/u,
|
||||
/^src\/components\/session\/token\/SessionUser\/Objects\/Departments\.vue$/u,
|
||||
],
|
||||
specs: ["tests/e2e/superuser-department-pricing-custom-only.spec.ts"],
|
||||
projects: ["chromium-desktop"],
|
||||
},
|
||||
{
|
||||
name: "self-serve",
|
||||
patterns: [/self[-/]?serve/iu, /selfserve/iu, /wash\/MyWash/iu],
|
||||
|
||||
@@ -103,6 +103,7 @@ export const ownedFilesByRole = {
|
||||
"superuser-department-branding.spec.js",
|
||||
"superuser-department-gates.spec.ts",
|
||||
"superuser-department-lanes.spec.ts",
|
||||
"superuser-department-pricing-custom-only.spec.ts",
|
||||
"superuser-departments-archive.spec.ts",
|
||||
"superuser-drafts.spec.ts",
|
||||
"superuser-products-layout.spec.ts",
|
||||
|
||||
@@ -261,6 +261,8 @@ function selectChangedTests(changedFiles) {
|
||||
specProjects: new Map(),
|
||||
mappedFiles: [],
|
||||
unmappedFiles: [],
|
||||
directSpecFiles: [],
|
||||
skippedDirectSpecFiles: [],
|
||||
fallback: false,
|
||||
};
|
||||
|
||||
@@ -270,8 +272,7 @@ function selectChangedTests(changedFiles) {
|
||||
const file = normalizePath(rawFile);
|
||||
|
||||
if (isE2eSpec(file)) {
|
||||
addSpec(selection, file, selectedProjects);
|
||||
selection.mappedFiles.push(file);
|
||||
selection.directSpecFiles.push(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -295,13 +296,26 @@ function selectChangedTests(changedFiles) {
|
||||
|
||||
selection.mappedFiles.push(file);
|
||||
for (const mapping of matches) {
|
||||
const projects = mapping.projects.filter((project) => selectedProjects.includes(project));
|
||||
const mappedProjects = mapping.projects.length > 0 ? mapping.projects : selectedProjects;
|
||||
const projects = mappedProjects.filter((project) => selectedProjects.includes(project));
|
||||
if (projects.length === 0) {
|
||||
continue;
|
||||
}
|
||||
for (const spec of mapping.specs) {
|
||||
addSpec(selection, spec, projects.length > 0 ? projects : selectedProjects);
|
||||
addSpec(selection, spec, projects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selection.specProjects.size === 0 && !selection.fallback) {
|
||||
for (const file of selection.directSpecFiles) {
|
||||
addSpec(selection, file, selectedProjects);
|
||||
selection.mappedFiles.push(file);
|
||||
}
|
||||
} else {
|
||||
selection.skippedDirectSpecFiles.push(...selection.directSpecFiles);
|
||||
}
|
||||
|
||||
return selection;
|
||||
}
|
||||
|
||||
@@ -370,6 +384,12 @@ async function runChangedSelection(selection) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (selection.skippedDirectSpecFiles.length > 0) {
|
||||
console.log(
|
||||
`[playwright-pr] Source mappings selected changed-area specs; direct E2E file edits are covered by mapped/core gates: ${selection.skippedDirectSpecFiles.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
for (const [index, group] of groups.entries()) {
|
||||
for (const project of group.projects) {
|
||||
const code = await runPlaywright({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useWindowSize } from "@vueuse/core";
|
||||
import { BMessage } from "buefy";
|
||||
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
type DateRange = {
|
||||
startDate: Date,
|
||||
@@ -115,23 +116,11 @@ const availableYears = computed(() => (
|
||||
));
|
||||
|
||||
const formatDateInputValue = (date: Date) => {
|
||||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
return formatLocalDateOnly(date);
|
||||
};
|
||||
|
||||
const parseDateInputValue = (value: string) => {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
||||
if (!match) {
|
||||
return new Date(value);
|
||||
}
|
||||
|
||||
return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
||||
return parseLocalDateOnly(value);
|
||||
};
|
||||
|
||||
const isSameDateInputValue = (left: Date, right: Date) => (
|
||||
|
||||
@@ -30,6 +30,7 @@ import PosLastScannedLicensePlatesV2 from "@/components/displays/department/pos/
|
||||
import PosDesktopOrderBookingSelectorModal from "@/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue";
|
||||
import PosDesktopCustomerConflictModal from "@/components/displays/department/pos/steps/elements/PosDesktopCustomerConflictModal.vue";
|
||||
import PosDesktopDuplicateWarning from "@/components/displays/department/pos/steps/elements/PosDesktopDuplicateWarning.vue";
|
||||
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { parsePosRouteSearch } from "@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js";
|
||||
import {
|
||||
@@ -377,8 +378,8 @@ const fetchDuplicateOrdersForContext = async (context) => {
|
||||
try {
|
||||
const response = await SessionUser.request(SessionUser.objects.orders.meta.endpoint, "GET", {
|
||||
filters: `reg_1:${normalizedContext.reg1},department_id:${department_id.value},created_at-date_from:${
|
||||
new Date().toISOString().split("T")[0]
|
||||
},created_at-date_to:${new Date().toISOString().split("T")[0]}`,
|
||||
todayLocalDateOnly()
|
||||
},created_at-date_to:${todayLocalDateOnly()}`,
|
||||
limit: 5,
|
||||
});
|
||||
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ import PosDepartmentStepMobileButtonNextStep from "@/components/displays/departm
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
import { PosOrder } from "@/components/displays/department/pos/steps/mobile/objects/PosOrder.vue";
|
||||
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
|
||||
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
// Define the close event to emit when the component is closed
|
||||
const emit = defineEmits(["close"]);
|
||||
/** Display variables */
|
||||
@@ -126,7 +127,7 @@ const liveTransactions = ref(null);
|
||||
const isLoading = ref(true);
|
||||
|
||||
const syncListTransactionHistory = async () => {
|
||||
let dateToday = new Date().toISOString().split("T")[0]; // Get today's date in YYYY-MM-DD format
|
||||
let dateToday = todayLocalDateOnly(); // Get today's date in YYYY-MM-DD format
|
||||
isLoading.value = true;
|
||||
// Fetch the list of orders created today for the current department
|
||||
SessionUser.objects.orders.get
|
||||
|
||||
@@ -22,7 +22,7 @@ const props = defineProps({
|
||||
import {computed, ref, watch} from 'vue';
|
||||
import { departments, getDepartments, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import {orderBy, orderDirection, setOrder, loadList} from "@/components/pagination/paginatedList.vue";
|
||||
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
|
||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||
import Swal from "sweetalert2";
|
||||
import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue";
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
setCachedXlvaskUsageAmount
|
||||
} from "@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js";
|
||||
|
||||
const { orderBy, orderDirection, setOrder, loadList } = usePaginatedListInstance();
|
||||
|
||||
const redirectDepartmentOrderPage = (orderId, departmentId) => {
|
||||
// Send the user to the order page (In a new tab)
|
||||
// `/admin/${departmentId}/modules/pos/orders/${orderId}`
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const SELFWASH_PERIOD_ALL_LIMIT = 10000;
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatLocalDateOnly } from '@/services/dateOnly.js';
|
||||
type startEndDate = {
|
||||
start: Date; // The start date of the range
|
||||
end: Date; // The end date of the range
|
||||
@@ -27,7 +28,7 @@ const datePresetFunctions = {
|
||||
month: {
|
||||
// Get the first day of the month based on the provided date first day at 00:00:01
|
||||
firstDayOfMonth: (date: Date): Date => {
|
||||
return new Date(date.getFullYear(), date.getMonth(), 2, 0, 0, 1, 0); // Set to the first day of the month at 00:00:01
|
||||
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0); // Set to the first day of the month at 00:00:00
|
||||
},
|
||||
// Get the last day of the month based on the provided date last day at 23:59:59
|
||||
lastDayOfMonth: (date: Date): Date => {
|
||||
@@ -116,7 +117,7 @@ export const datePresets = <datePreset[]>[
|
||||
*/
|
||||
|
||||
const convertToISO = (date: Date): string => {
|
||||
return date.toISOString().split('T')[0]; // Convert to YYYY-MM-DD format
|
||||
return formatLocalDateOnly(date); // Convert to YYYY-MM-DD format
|
||||
};
|
||||
|
||||
export const dateFunctions = {
|
||||
@@ -150,4 +151,4 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template></template>
|
||||
<template></template>
|
||||
|
||||
@@ -21,9 +21,32 @@ const props = defineProps({
|
||||
type: Number, default: 0
|
||||
}
|
||||
})
|
||||
import {computed, ref, watch } from "vue";
|
||||
import {computed, provide, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import {
|
||||
PaginatedListKey,
|
||||
usePaginatedList,
|
||||
} from "@/components/pagination/paginatedList.vue";
|
||||
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||
import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
|
||||
import PaginationDisplayTemplateDates
|
||||
from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
|
||||
import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { BSwitch } from "buefy";
|
||||
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
|
||||
import { SELFWASH_PERIOD_ALL_LIMIT } from "@/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js";
|
||||
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter();
|
||||
const paginatedList = usePaginatedList();
|
||||
provide(PaginatedListKey, paginatedList);
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
list,
|
||||
loadList,
|
||||
@@ -38,22 +61,8 @@ import {
|
||||
setOrder,
|
||||
hideSearchField,
|
||||
setHideSearchField,
|
||||
} from "@/components/pagination/paginatedList.vue";
|
||||
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||
} = paginatedList;
|
||||
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||
import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
|
||||
import PaginationDisplayTemplateDates
|
||||
from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
|
||||
import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { BSwitch } from "buefy";
|
||||
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter();
|
||||
const SELFWASH_PERIOD_ALL_LIMIT = 10000;
|
||||
setEndpoint("/modules/xlvask/services/usage/orders", false);
|
||||
if (props.loadAllAtOnce) {
|
||||
setMetaItemsPerPage(SELFWASH_PERIOD_ALL_LIMIT, false);
|
||||
@@ -63,7 +72,7 @@ const parseInitialDate = (value) => {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
const parsed = new Date(`${value}T00:00:00`);
|
||||
const parsed = parseLocalDateOnly(value);
|
||||
return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
|
||||
};
|
||||
|
||||
@@ -129,7 +138,7 @@ const dateFrom = ref(parseInitialDate(props.initialDateFrom));
|
||||
const dateTo = ref(parseInitialDate(props.initialDateTo));
|
||||
|
||||
const parsedDate = (date) => {
|
||||
return new Date(date);
|
||||
return parseLocalDateOnly(date);
|
||||
};
|
||||
|
||||
const reloadScheduled = ref(false);
|
||||
@@ -148,14 +157,14 @@ const actions = {
|
||||
from: {
|
||||
select: (date) => {
|
||||
dateFrom.value = parsedDate(date);
|
||||
setFilter("StartTime-date_from", date.toISOString().split("T")[0], false);
|
||||
setFilter("StartTime-date_from", formatLocalDateOnly(date), false);
|
||||
scheduleReload();
|
||||
}
|
||||
},
|
||||
to: {
|
||||
select: (date) => {
|
||||
dateTo.value = parsedDate(date);
|
||||
setFilter("StartTime-date_to", date.toISOString().split("T")[0], false);
|
||||
setFilter("StartTime-date_to", formatLocalDateOnly(date), false);
|
||||
scheduleReload();
|
||||
}
|
||||
},
|
||||
|
||||
+4
-4
@@ -109,8 +109,8 @@ import OrdersTable from "@/components/displays/department/pos/orders/ordersTable
|
||||
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||
import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue";
|
||||
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
|
||||
import {now} from "@vueuse/core";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { formatLocalDateOnly, parseLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter();
|
||||
@@ -176,8 +176,8 @@ if (props.autoLoad) {
|
||||
const date_from = ref(null);
|
||||
const date_to = ref(null);
|
||||
const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => {
|
||||
const formattedStartDate = newSelectionStartDate.toISOString().split("T")[0];
|
||||
const formattedEndDate = newSelectionToDate.toISOString().split("T")[0];
|
||||
const formattedStartDate = formatLocalDateOnly(newSelectionStartDate);
|
||||
const formattedEndDate = formatLocalDateOnly(newSelectionToDate);
|
||||
date_from.value = formattedStartDate;
|
||||
date_to.value = formattedEndDate;
|
||||
setFilter("created_at-date_from", formattedStartDate, true);
|
||||
@@ -361,7 +361,7 @@ const doesEndpointMatch = (matcher) => {
|
||||
<!-- Shortcuts for date filters -->
|
||||
<DatePeriodSelector :on-selection-change="onDateRangeSelected"
|
||||
:visibility="{ showDailySelector: false, showWeeklySelector: false, showMultipleMonthWarning: false, showUpdateButton: false, showMonthSelector: false, showStartDate: false, showEndDate: false, showSelectionValidity: false, showYearSelector: false, showToLabel: false }"
|
||||
v-bind:selection="{ startDate: date_from ? new Date(date_from) : new Date(now()), endDate: date_to ? new Date(date_to) : new Date(now()) }"/>
|
||||
v-bind:selection="{ startDate: parseLocalDateOnly(date_from || todayLocalDateOnly()), endDate: parseLocalDateOnly(date_to || todayLocalDateOnly()) }"/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {ref, watch} from "vue";
|
||||
import { Colors } from "@/ThemeConfig.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter();
|
||||
@@ -52,7 +53,7 @@ watch(() => getFilter("status"), (value) => {
|
||||
// If the route starts with /user, set the endpoint to /user/bookings
|
||||
if (router.currentRoute.value.path.startsWith("/user")) {
|
||||
setEndpoint("/user/bookings", false);
|
||||
setFilter("date", new Date().toISOString().split("T")[0], false);
|
||||
setFilter("date", todayLocalDateOnly(), false);
|
||||
setOrder("created_at", "desc");
|
||||
}
|
||||
// If the route starts with /superuser, set the endpoint to /bookings
|
||||
@@ -81,7 +82,7 @@ loadList();
|
||||
const showingToday = ref(true);
|
||||
|
||||
watch(() => getFilter("date"), (value) => {
|
||||
showingToday.value = value === new Date().toISOString().split("T")[0];
|
||||
showingToday.value = value === todayLocalDateOnly();
|
||||
});
|
||||
|
||||
// If the screen is mobile, set the is small variable to true
|
||||
@@ -151,7 +152,7 @@ const showNewOrderBookingsPortal = () => {
|
||||
<label class="label">{{ isSmall ? t('common.today') : t('pagination.show_only_today') }}</label>
|
||||
<div class="control">
|
||||
<div class="field">
|
||||
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? new Date().toISOString().split('T')[0] : '*') }" checked="checked" :class="{ 'is-link': showingToday }" />
|
||||
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? todayLocalDateOnly() : '*') }" checked="checked" :class="{ 'is-link': showingToday }" />
|
||||
<label for="today"></label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,4 +189,4 @@ const showNewOrderBookingsPortal = () => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,7 @@ import OrderBookingsTable from "@/views/dashboards/userDashboard/bookings/displa
|
||||
import { departments, getDepartments } from "@/components/pagination/departmentTabs.vue";
|
||||
import { Colors } from "@/ThemeConfig.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { endOfLocalDate, startOfLocalDate, todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
const { t } = useI18n();
|
||||
/**
|
||||
@@ -52,11 +53,9 @@ const onOnlyTodayFilterChange = (event, autoLoadList = true) => {
|
||||
setFilter("datetime-date_from", null, false);
|
||||
setFilter("datetime-date_to", null, false);
|
||||
} else {
|
||||
const startOfDay = new Date().setHours(0, 0, 0, 0);
|
||||
const endOfDay = new Date().setHours(23, 59, 59, 999);
|
||||
//setFilter('datetime', null, false);
|
||||
setFilter("datetime-date_from", new Date(startOfDay).toISOString(), false);
|
||||
setFilter("datetime-date_to", new Date(endOfDay).toISOString(), false);
|
||||
setFilter("datetime-date_from", startOfLocalDate(val).toISOString(), false);
|
||||
setFilter("datetime-date_to", endOfLocalDate(val).toISOString(), false);
|
||||
}
|
||||
if (autoLoadList) {
|
||||
loadList();
|
||||
@@ -95,7 +94,7 @@ onMounted(() => {
|
||||
departmentFilter.value = value;
|
||||
} else if (key === "only_today" && value === true) {
|
||||
setFilterKey = false; // Since the only_today filter is handled separately
|
||||
onOnlyTodayFilterChange({ target: { value: new Date().toISOString().split("T")[0] } }, false);
|
||||
onOnlyTodayFilterChange({ target: { value: todayLocalDateOnly() } }, false);
|
||||
}
|
||||
if (setFilterKey) {
|
||||
setFilter(key, value, false);
|
||||
@@ -170,7 +169,7 @@ onMounted(() => {
|
||||
<div class="select">
|
||||
<select :value="onlyTodayFilter" @change="onOnlyTodayFilterChange">
|
||||
<option value="*">{{ t("common.all") }}</option>
|
||||
<option :value="new Date().toISOString().split('T')[0]">{{ t("common.yes") }}</option>
|
||||
<option :value="todayLocalDateOnly()">{{ t("common.yes") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,7 +192,7 @@ onMounted(() => {
|
||||
@change="
|
||||
(event) => {
|
||||
onOnlyTodayFilterChange({
|
||||
target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' },
|
||||
target: { value: event.target.checked ? todayLocalDateOnly() : '*' },
|
||||
});
|
||||
}
|
||||
"
|
||||
@@ -226,7 +225,7 @@ onMounted(() => {
|
||||
@change="
|
||||
(event) => {
|
||||
onOnlyTodayFilterChange(
|
||||
{ target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' } },
|
||||
{ target: { value: event.target.checked ? todayLocalDateOnly() : '*' } },
|
||||
false
|
||||
);
|
||||
onOrderIdFilterChange({ target: { value: event.target.checked ? 'is null' : '*' } });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import PaginationDisplayItemColumn from "@/components/displays/pagination/PaginationDisplayItemColumn.vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
@@ -19,7 +20,7 @@ defineExpose({
|
||||
});
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
return date.value.toISOString().split('T')[0];
|
||||
return formatLocalDateOnly(date.value);
|
||||
});
|
||||
|
||||
// This component is used to display a date input in a pagination display item column.
|
||||
@@ -36,8 +37,7 @@ const formattedDate = computed(() => {
|
||||
class="input"
|
||||
:value="formattedDate"
|
||||
@input="(e) => {
|
||||
const newDate = new Date(e.target.value);
|
||||
console.log('Selected date:', newDate);
|
||||
const newDate = parseLocalDateOnly(e.target.value);
|
||||
emit('update:date', newDate);
|
||||
}"
|
||||
/>
|
||||
@@ -47,4 +47,4 @@ const formattedDate = computed(() => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { SessionUser } from '@/components/session/token/SessionUser.vue';
|
||||
|
||||
@@ -10,7 +9,7 @@ import PaginationDisplayTemplateButton
|
||||
from "@/components/displays/pagination/templates/PaginationDisplayTemplateButton.vue";
|
||||
import { datePresets , dateFunctions} from '@/components/displays/pagination/PaginationDisplayDates.vue';
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
startDate: {
|
||||
type: Date,
|
||||
required: true,
|
||||
@@ -46,6 +45,7 @@ function updateStartDate(date: Date) {
|
||||
// Update the end date model when the date is changed
|
||||
function updateEndDate(date: Date) {
|
||||
endDateModel.value = date;
|
||||
emits('update:endDate', date);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -79,4 +79,4 @@ function updateEndDate(date: Date) {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { showDownloadWashCertificate } from "@/components/shop/DownloadWashCerti
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
import Swal from 'sweetalert2';
|
||||
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||
import { todayLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
const redirectBookingObjectPage = (objectId) => {
|
||||
// Send the user to the object page
|
||||
window.location.href = `/user/bookings/${objectId}`;
|
||||
@@ -237,7 +238,8 @@ window.addEventListener('resize', () => {
|
||||
* To organize the bookings, we want to show the bookings that are created today first, then yesterday, then all other days
|
||||
* @type {string}
|
||||
*/
|
||||
const currentDate = new Date().toISOString().split("T")[0];
|
||||
const currentDate = todayLocalDateOnly();
|
||||
const previousDate = yesterdayLocalDateOnly();
|
||||
|
||||
/**
|
||||
* Sort the bookings by date
|
||||
@@ -362,7 +364,7 @@ const canUserEditObject = (object) => {
|
||||
</span>
|
||||
<span class="has-text-grey">
|
||||
<!-- Human readable date (Today, Yesterday, etc.) -->
|
||||
{{ object.date === currentDate ? $t('tables.bookings.today') : object.date === new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split("T")[0] ? $t('tables.bookings.yesterday') : object.date }}
|
||||
{{ object.date === currentDate ? $t('tables.bookings.today') : object.date === previousDate ? $t('tables.bookings.yesterday') : object.date }}
|
||||
<!-- Number of bookings on the date -->
|
||||
({{ $t('tables.bookings.bookings_count', { count: objects.filter((booking) => booking.date === object.date).length }) }})
|
||||
</span>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ref, watch, onMounted } from "vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { isAccessibleVisibleNamedDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js";
|
||||
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
// Get the department ID from the URL
|
||||
const route = useRoute();
|
||||
const department_id = route.params.departmentId || SessionUser.functions.getDepartmentIdFromUrl() || null;
|
||||
@@ -73,9 +74,9 @@ const emphasis = ref({
|
||||
"department:" +
|
||||
department_id +
|
||||
",status:pending,date-date_from:" +
|
||||
new Date().toISOString().split("T")[0] +
|
||||
todayLocalDateOnly() +
|
||||
",date-date_to:" +
|
||||
new Date().toISOString().split("T")[0],
|
||||
todayLocalDateOnly(),
|
||||
limit: 1, // Limit to 1 booking, we only need to know if there are any bookings or not
|
||||
page: 1,
|
||||
})
|
||||
@@ -97,7 +98,7 @@ const emphasis = ref({
|
||||
* Check if the department has a daily report for today
|
||||
*/
|
||||
SessionUser.request(SessionUser.objects.department_daily_reports.meta.endpoint + "/get", "GET", {
|
||||
date: new Date().toISOString().split("T")[0],
|
||||
date: todayLocalDateOnly(),
|
||||
id: department_id, // This refers to the department ID
|
||||
})
|
||||
.then((response) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/Ob
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import {ref} from "vue";
|
||||
import i18n from '@/i18n';
|
||||
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
const t = (key) => i18n.global.t(key);
|
||||
|
||||
@@ -241,7 +242,7 @@ export const Bookings = {
|
||||
'/bookings',
|
||||
'GET',
|
||||
{
|
||||
filters: `department:${department},status:pending,date-date_from:${new Date().toISOString().split('T')[0]},date-date_to:${new Date().toISOString().split('T')[0]}`,
|
||||
filters: `department:${department},status:pending,date-date_from:${todayLocalDateOnly()},date-date_to:${todayLocalDateOnly()}`,
|
||||
limit: 100,
|
||||
page: 1
|
||||
}
|
||||
@@ -289,4 +290,4 @@ export const Bookings = {
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/Ob
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import {ref} from "vue";
|
||||
import i18n from '@/i18n';
|
||||
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
const t = (key) => i18n.global.t(key);
|
||||
|
||||
@@ -80,7 +81,7 @@ const t = (key) => i18n.global.t(key);
|
||||
water_usage_morning: parseInt(water_usage_morning),
|
||||
water_usage: parseInt(water_usage),
|
||||
notes: notes ? notes : 'Daily report',
|
||||
date: date ? date : new Date().toISOString().split('T')[0]
|
||||
date: date ? date : todayLocalDateOnly()
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { formatLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
/**
|
||||
* The DepartmentTimeBookingsEntries object
|
||||
@@ -230,10 +231,10 @@ export const DepartmentTimeBookingsEntries = {
|
||||
if (!dateFrom) {
|
||||
const lastWeek = new Date();
|
||||
lastWeek.setDate(lastWeek.getDate() - 7);
|
||||
dateFrom = lastWeek.toISOString().split('T')[0]; // Format as YYYY-MM-DD
|
||||
dateFrom = formatLocalDateOnly(lastWeek); // Format as YYYY-MM-DD
|
||||
}
|
||||
if (!dateTo) {
|
||||
dateTo = new Date().toISOString().split('T')[0]; // Format as YYYY-MM-DD
|
||||
dateTo = todayLocalDateOnly(); // Format as YYYY-MM-DD
|
||||
}
|
||||
console.log("Fetching public available times for department:", departmentId, "from", dateFrom, "to", dateTo);
|
||||
// Make the request to fetch public available times for the department
|
||||
@@ -279,4 +280,4 @@ export const DepartmentTimeBookingsEntries = {
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -108,6 +108,14 @@ export const Departments = {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
custom_pricing_only: {
|
||||
label: t("objects.departments.columns.custom_pricing_only"),
|
||||
type: "boolean",
|
||||
sortable: true,
|
||||
creation: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
longitude: {
|
||||
label: t("objects.departments.columns.longitude"),
|
||||
type: "number",
|
||||
@@ -164,6 +172,14 @@ export const Departments = {
|
||||
archived: async (id, archived) => {
|
||||
return ObjectsGlobal.set.column(Departments.meta.endpoint, id, "archived", ObjectsGlobal.parse.boolean(archived));
|
||||
},
|
||||
custom_pricing_only: async (id, custom_pricing_only) => {
|
||||
return ObjectsGlobal.set.column(
|
||||
Departments.meta.endpoint,
|
||||
id,
|
||||
"custom_pricing_only",
|
||||
ObjectsGlobal.parse.boolean(custom_pricing_only)
|
||||
);
|
||||
},
|
||||
longitude: async (id, longitude) => {
|
||||
return ObjectsGlobal.set.column(Departments.meta.endpoint, id, "longitude", parseFloat(longitude));
|
||||
},
|
||||
|
||||
@@ -1190,6 +1190,14 @@
|
||||
"edit_title": "@:{'templates.generated.compat.limited_backoffice.employees.edit_title'}",
|
||||
"name": "@:{'templates.generated.compat.limited_backoffice.employees.name'}",
|
||||
"email": "@:{'templates.generated.compat.limited_backoffice.employees.email'}",
|
||||
"phone": "@:{'templates.generated.compat.limited_backoffice.employees.phone'}",
|
||||
"phone_validation": "@:{'templates.generated.compat.limited_backoffice.employees.phone_validation'}",
|
||||
"country_codes": {
|
||||
"denmark": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.denmark'}",
|
||||
"sweden": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.sweden'}",
|
||||
"norway": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.norway'}",
|
||||
"finland": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.finland'}"
|
||||
},
|
||||
"password": "@:{'templates.generated.compat.limited_backoffice.employees.password'}",
|
||||
"password_create_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_create_help'}",
|
||||
"password_update_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_update_help'}",
|
||||
@@ -3455,6 +3463,13 @@
|
||||
"opening_hours": {
|
||||
"closed": "Stengt"
|
||||
},
|
||||
"pricing": {
|
||||
"custom_pricing_disabled": "Fallback aktiv",
|
||||
"custom_pricing_enabled": "Kun egne priser",
|
||||
"custom_pricing_missing_price": "Manglende afdelingspriser bliver 999999.",
|
||||
"custom_pricing_only": "Ingen fallback-priser",
|
||||
"effective_department_price": "Effektiv afdelingspris"
|
||||
},
|
||||
"search_placeholder": "@.capitalize:{'words.generated.søg'} @:{'words.generated.efter'} @:{'words.generated.afdelingsnavn'}",
|
||||
"select_department": "@.capitalize:{'words.generated.vælg'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.afdeling'}",
|
||||
"subtitle": "@.capitalize:{'words.generated.administrer'} @:{'words.generated.afdelinger'}",
|
||||
@@ -4285,6 +4300,7 @@
|
||||
"departments": {
|
||||
"columns": {
|
||||
"archived": "Arkiveret",
|
||||
"custom_pricing_only": "Ingen fallback-priser",
|
||||
"dimension": "Dimension",
|
||||
"economic_department": "@.upper:{'words.generated.e'}-@:{'words.generated.conomic'} @:{'words.generated.afdeling'}",
|
||||
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
|
||||
@@ -6024,6 +6040,14 @@
|
||||
"edit_title": "@:{'words.generated.rediger'} @:{'words.generated.medarbejder'}",
|
||||
"name": "Navn",
|
||||
"email": "Email",
|
||||
"phone": "Telefon",
|
||||
"phone_validation": "Telefonnummeret skal være 4-15 cifre.",
|
||||
"country_codes": {
|
||||
"denmark": "Danmark",
|
||||
"sweden": "Sverige",
|
||||
"norway": "Norge",
|
||||
"finland": "Finland"
|
||||
},
|
||||
"password": "@.capitalize:{'words.generated.adgangskode'}",
|
||||
"password_create_help": "@.capitalize:{'words.generated.pakrævet'} @:{'words.generated.for'} nye @:{'words.generated.medarbejdere'}. Brug @:{'words.generated.mindst'} 8 tegn.",
|
||||
"password_update_help": "Lad feltet være tomt @:{'words.generated.for'} at beholde den nuværende @:{'words.generated.adgangskode'}.",
|
||||
|
||||
@@ -1301,6 +1301,14 @@
|
||||
"edit_title": "@:{'templates.generated.compat.limited_backoffice.employees.edit_title'}",
|
||||
"name": "@:{'templates.generated.compat.limited_backoffice.employees.name'}",
|
||||
"email": "@:{'templates.generated.compat.limited_backoffice.employees.email'}",
|
||||
"phone": "@:{'templates.generated.compat.limited_backoffice.employees.phone'}",
|
||||
"phone_validation": "@:{'templates.generated.compat.limited_backoffice.employees.phone_validation'}",
|
||||
"country_codes": {
|
||||
"denmark": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.denmark'}",
|
||||
"sweden": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.sweden'}",
|
||||
"norway": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.norway'}",
|
||||
"finland": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.finland'}"
|
||||
},
|
||||
"password": "@:{'templates.generated.compat.limited_backoffice.employees.password'}",
|
||||
"password_create_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_create_help'}",
|
||||
"password_update_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_update_help'}",
|
||||
@@ -6121,6 +6129,14 @@
|
||||
"edit_title": "@:{'words.generated.mitarbeiter'} @:{'words.generated.bearbeiten'}",
|
||||
"name": "Name",
|
||||
"email": "E-Mail",
|
||||
"phone": "Telefon",
|
||||
"phone_validation": "Die Telefonnummer muss 4-15 Ziffern enthalten.",
|
||||
"country_codes": {
|
||||
"denmark": "Dänemark",
|
||||
"sweden": "Schweden",
|
||||
"norway": "Norwegen",
|
||||
"finland": "Finnland"
|
||||
},
|
||||
"password": "@:{'words.generated.passwort'}",
|
||||
"password_create_help": "@.capitalize:{'words.generated.fur'} neue @:{'words.generated.mitarbeiter'} @:{'words.generated.erforderlich'}. Verwenden @.capitalize:{'words.generated.sie'} @:{'words.generated.mindestens'} 8 Zeichen.",
|
||||
"password_update_help": "Leer lassen, um das vorhandene @:{'words.generated.passwort'} beizubehalten.",
|
||||
|
||||
@@ -1022,6 +1022,14 @@
|
||||
"edit_title": "@:{'templates.generated.compat.limited_backoffice.employees.edit_title'}",
|
||||
"name": "@:{'templates.generated.compat.limited_backoffice.employees.name'}",
|
||||
"email": "@:{'templates.generated.compat.limited_backoffice.employees.email'}",
|
||||
"phone": "@:{'templates.generated.compat.limited_backoffice.employees.phone'}",
|
||||
"phone_validation": "@:{'templates.generated.compat.limited_backoffice.employees.phone_validation'}",
|
||||
"country_codes": {
|
||||
"denmark": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.denmark'}",
|
||||
"sweden": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.sweden'}",
|
||||
"norway": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.norway'}",
|
||||
"finland": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.finland'}"
|
||||
},
|
||||
"password": "@:{'templates.generated.compat.limited_backoffice.employees.password'}",
|
||||
"password_create_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_create_help'}",
|
||||
"password_update_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_update_help'}",
|
||||
@@ -3287,6 +3295,13 @@
|
||||
"opening_hours": {
|
||||
"closed": "@:{'templates.generated.compat.global.closed'}"
|
||||
},
|
||||
"pricing": {
|
||||
"custom_pricing_disabled": "Fallback enabled",
|
||||
"custom_pricing_enabled": "Custom only",
|
||||
"custom_pricing_missing_price": "Missing department prices resolve to 999999.",
|
||||
"custom_pricing_only": "No fallback pricing",
|
||||
"effective_department_price": "Effective department price"
|
||||
},
|
||||
"search_placeholder": "@.capitalize:{'words.generated.search'} @:{'words.generated.by'} @:{'words.generated.department'} @:{'words.generated.name'}",
|
||||
"select_department": "@.capitalize:{'words.generated.select'} @:{'words.generated.a'} @:{'words.generated.department'}",
|
||||
"subtitle": "@.capitalize:{'words.generated.manage'} @:{'words.generated.departments'}",
|
||||
@@ -4117,6 +4132,7 @@
|
||||
"departments": {
|
||||
"columns": {
|
||||
"archived": "@:{'words.generated.archived'}",
|
||||
"custom_pricing_only": "No fallback pricing",
|
||||
"dimension": "Dimension",
|
||||
"economic_department": "@.upper:{'words.generated.e'}-@:{'words.generated.conomic'} @.capitalize:{'words.generated.department'}",
|
||||
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
|
||||
@@ -5856,6 +5872,14 @@
|
||||
"edit_title": "@.capitalize:{'words.generated.edit'} @:{'words.generated.employee'}",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"phone": "Phone",
|
||||
"phone_validation": "Phone number must be 4-15 digits.",
|
||||
"country_codes": {
|
||||
"denmark": "Denmark",
|
||||
"sweden": "Sweden",
|
||||
"norway": "Norway",
|
||||
"finland": "Finland"
|
||||
},
|
||||
"password": "@.capitalize:{'words.generated.password'}",
|
||||
"password_create_help": "@.capitalize:{'words.generated.required'} @:{'words.generated.for'} new @:{'words.generated.employees'}. Use @:{'words.generated.at'} @:{'words.generated.least'} 8 characters.",
|
||||
"password_update_help": "Leave blank @:{'words.generated.to'} keep @:{'words.replication.article.host_mention'} existing @:{'words.generated.password'}.",
|
||||
|
||||
@@ -2420,6 +2420,13 @@
|
||||
"title": "@:{'templates.generated.compat.departments.tab.opening_hours'}"
|
||||
},
|
||||
"phone": "@:common.phone",
|
||||
"pricing": {
|
||||
"custom_pricing_disabled": "@:{'templates.generated.compat.departments.pricing.custom_pricing_disabled'}",
|
||||
"custom_pricing_enabled": "@:{'templates.generated.compat.departments.pricing.custom_pricing_enabled'}",
|
||||
"custom_pricing_missing_price": "@:{'templates.generated.compat.departments.pricing.custom_pricing_missing_price'}",
|
||||
"custom_pricing_only": "@:{'templates.generated.compat.departments.pricing.custom_pricing_only'}",
|
||||
"effective_department_price": "@:{'templates.generated.compat.departments.pricing.effective_department_price'}"
|
||||
},
|
||||
"products": {
|
||||
"new_product": "@:products.new_product"
|
||||
},
|
||||
@@ -3486,6 +3493,7 @@
|
||||
"departments": {
|
||||
"columns": {
|
||||
"archived": "@:{'templates.generated.compat.objects.departments.columns.archived'}",
|
||||
"custom_pricing_only": "@:{'templates.generated.compat.objects.departments.columns.custom_pricing_only'}",
|
||||
"dimension": "@:{'templates.generated.compat.objects.departments.columns.dimension'}",
|
||||
"economic_department": "@:{'templates.generated.compat.objects.departments.columns.economic_department'}",
|
||||
"latitude": "@:{'templates.generated.compat.objects.departments.columns.latitude'}",
|
||||
|
||||
@@ -1304,6 +1304,14 @@
|
||||
"edit_title": "@:{'templates.generated.compat.limited_backoffice.employees.edit_title'}",
|
||||
"name": "@:{'templates.generated.compat.limited_backoffice.employees.name'}",
|
||||
"email": "@:{'templates.generated.compat.limited_backoffice.employees.email'}",
|
||||
"phone": "@:{'templates.generated.compat.limited_backoffice.employees.phone'}",
|
||||
"phone_validation": "@:{'templates.generated.compat.limited_backoffice.employees.phone_validation'}",
|
||||
"country_codes": {
|
||||
"denmark": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.denmark'}",
|
||||
"sweden": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.sweden'}",
|
||||
"norway": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.norway'}",
|
||||
"finland": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.finland'}"
|
||||
},
|
||||
"password": "@:{'templates.generated.compat.limited_backoffice.employees.password'}",
|
||||
"password_create_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_create_help'}",
|
||||
"password_update_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_update_help'}",
|
||||
@@ -6124,6 +6132,14 @@
|
||||
"edit_title": "@:{'words.generated.rediger'} @:{'words.generated.medarbeider'}",
|
||||
"name": "Navn",
|
||||
"email": "E-post",
|
||||
"phone": "Telefon",
|
||||
"phone_validation": "Telefonnummeret må være 4-15 sifre.",
|
||||
"country_codes": {
|
||||
"denmark": "Danmark",
|
||||
"sweden": "Sverige",
|
||||
"norway": "Norge",
|
||||
"finland": "Finland"
|
||||
},
|
||||
"password": "@.capitalize:{'words.generated.passord'}",
|
||||
"password_create_help": "@.capitalize:{'words.generated.kreves'} @:{'words.generated.for'} nye @:{'words.generated.medarbeidere'}. Bruk @:{'words.generated.minst'} 8 tegn.",
|
||||
"password_update_help": "La stå tomt @:{'words.generated.for'} å beholde nåværende @:{'words.generated.passord'}.",
|
||||
|
||||
@@ -1354,6 +1354,14 @@
|
||||
"edit_title": "@:{'templates.generated.compat.limited_backoffice.employees.edit_title'}",
|
||||
"name": "@:{'templates.generated.compat.limited_backoffice.employees.name'}",
|
||||
"email": "@:{'templates.generated.compat.limited_backoffice.employees.email'}",
|
||||
"phone": "@:{'templates.generated.compat.limited_backoffice.employees.phone'}",
|
||||
"phone_validation": "@:{'templates.generated.compat.limited_backoffice.employees.phone_validation'}",
|
||||
"country_codes": {
|
||||
"denmark": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.denmark'}",
|
||||
"sweden": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.sweden'}",
|
||||
"norway": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.norway'}",
|
||||
"finland": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.finland'}"
|
||||
},
|
||||
"password": "@:{'templates.generated.compat.limited_backoffice.employees.password'}",
|
||||
"password_create_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_create_help'}",
|
||||
"password_update_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_update_help'}",
|
||||
@@ -6174,6 +6182,14 @@
|
||||
"edit_title": "@:{'words.generated.redigera'} @:{'words.generated.medarbetare'}",
|
||||
"name": "Namn",
|
||||
"email": "E-post",
|
||||
"phone": "Telefon",
|
||||
"phone_validation": "Telefonnumret måste vara 4-15 siffror.",
|
||||
"country_codes": {
|
||||
"denmark": "Danmark",
|
||||
"sweden": "Sverige",
|
||||
"norway": "Norge",
|
||||
"finland": "Finland"
|
||||
},
|
||||
"password": "@.capitalize:{'words.generated.losenord'}",
|
||||
"password_create_help": "@.capitalize:{'words.generated.kravs'} @:{'words.generated.for'} nya @:{'words.generated.medarbetare'}. Använd @:{'words.generated.minst'} 8 tecken.",
|
||||
"password_update_help": "Lämna tomt @:{'words.generated.for'} att behålla nuvarande @:{'words.generated.losenord'}.",
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
"opening_hours": {
|
||||
"closed": "Stengt"
|
||||
},
|
||||
"pricing": {
|
||||
"custom_pricing_disabled": "Fallback aktiv",
|
||||
"custom_pricing_enabled": "Kun egne priser",
|
||||
"custom_pricing_missing_price": "Manglende afdelingspriser bliver 999999.",
|
||||
"custom_pricing_only": "Ingen fallback-priser",
|
||||
"effective_department_price": "Effektiv afdelingspris"
|
||||
},
|
||||
"search_placeholder": "@.capitalize:{'terms.glossary.søg'} @:{'terms.glossary.efter'} @:{'terms.glossary.afdelingsnavn'}",
|
||||
"select_department": "@.capitalize:{'terms.glossary.vælg'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.afdeling'}",
|
||||
"subtitle": "@.capitalize:{'terms.glossary.administrer'} @:{'terms.glossary.afdelinger'}",
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
"departments": {
|
||||
"columns": {
|
||||
"archived": "Arkiveret",
|
||||
"custom_pricing_only": "Ingen fallback-priser",
|
||||
"dimension": "Dimension",
|
||||
"economic_department": "@.upper:{'terms.glossary.e'}-@:{'terms.glossary.conomic'} @:{'terms.glossary.afdeling'}",
|
||||
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
|
||||
|
||||
@@ -46,6 +46,14 @@
|
||||
"edit_title": "@:{'phrases.compat.limited_backoffice.employees.edit_title'}",
|
||||
"name": "@:{'phrases.compat.limited_backoffice.employees.name'}",
|
||||
"email": "@:{'phrases.compat.limited_backoffice.employees.email'}",
|
||||
"phone": "@:{'phrases.compat.limited_backoffice.employees.phone'}",
|
||||
"phone_validation": "@:{'phrases.compat.limited_backoffice.employees.phone_validation'}",
|
||||
"country_codes": {
|
||||
"denmark": "@:{'phrases.compat.limited_backoffice.employees.country_codes.denmark'}",
|
||||
"sweden": "@:{'phrases.compat.limited_backoffice.employees.country_codes.sweden'}",
|
||||
"norway": "@:{'phrases.compat.limited_backoffice.employees.country_codes.norway'}",
|
||||
"finland": "@:{'phrases.compat.limited_backoffice.employees.country_codes.finland'}"
|
||||
},
|
||||
"password": "@:{'phrases.compat.limited_backoffice.employees.password'}",
|
||||
"password_create_help": "@:{'phrases.compat.limited_backoffice.employees.password_create_help'}",
|
||||
"password_update_help": "@:{'phrases.compat.limited_backoffice.employees.password_update_help'}",
|
||||
@@ -279,6 +287,14 @@
|
||||
"edit_title": "@:{'terms.glossary.rediger'} @:{'terms.glossary.medarbejder'}",
|
||||
"name": "Navn",
|
||||
"email": "Email",
|
||||
"phone": "Telefon",
|
||||
"phone_validation": "Telefonnummeret skal være 4-15 cifre.",
|
||||
"country_codes": {
|
||||
"denmark": "Danmark",
|
||||
"sweden": "Sverige",
|
||||
"norway": "Norge",
|
||||
"finland": "Finland"
|
||||
},
|
||||
"password": "@.capitalize:{'terms.glossary.adgangskode'}",
|
||||
"password_create_help": "@.capitalize:{'terms.glossary.pakrævet'} @:{'terms.glossary.for'} nye @:{'terms.glossary.medarbejdere'}. Brug @:{'terms.glossary.mindst'} 8 tegn.",
|
||||
"password_update_help": "Lad feltet være tomt @:{'terms.glossary.for'} at beholde den nuværende @:{'terms.glossary.adgangskode'}.",
|
||||
|
||||
@@ -46,6 +46,14 @@
|
||||
"edit_title": "@:{'phrases.compat.limited_backoffice.employees.edit_title'}",
|
||||
"name": "@:{'phrases.compat.limited_backoffice.employees.name'}",
|
||||
"email": "@:{'phrases.compat.limited_backoffice.employees.email'}",
|
||||
"phone": "@:{'phrases.compat.limited_backoffice.employees.phone'}",
|
||||
"phone_validation": "@:{'phrases.compat.limited_backoffice.employees.phone_validation'}",
|
||||
"country_codes": {
|
||||
"denmark": "@:{'phrases.compat.limited_backoffice.employees.country_codes.denmark'}",
|
||||
"sweden": "@:{'phrases.compat.limited_backoffice.employees.country_codes.sweden'}",
|
||||
"norway": "@:{'phrases.compat.limited_backoffice.employees.country_codes.norway'}",
|
||||
"finland": "@:{'phrases.compat.limited_backoffice.employees.country_codes.finland'}"
|
||||
},
|
||||
"password": "@:{'phrases.compat.limited_backoffice.employees.password'}",
|
||||
"password_create_help": "@:{'phrases.compat.limited_backoffice.employees.password_create_help'}",
|
||||
"password_update_help": "@:{'phrases.compat.limited_backoffice.employees.password_update_help'}",
|
||||
@@ -279,6 +287,14 @@
|
||||
"edit_title": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.bearbeiten'}",
|
||||
"name": "Name",
|
||||
"email": "E-Mail",
|
||||
"phone": "Telefon",
|
||||
"phone_validation": "Die Telefonnummer muss 4-15 Ziffern enthalten.",
|
||||
"country_codes": {
|
||||
"denmark": "Dänemark",
|
||||
"sweden": "Schweden",
|
||||
"norway": "Norwegen",
|
||||
"finland": "Finnland"
|
||||
},
|
||||
"password": "@:{'terms.glossary.passwort'}",
|
||||
"password_create_help": "@.capitalize:{'terms.glossary.fur'} neue @:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.erforderlich'}. Verwenden @.capitalize:{'terms.glossary.sie'} @:{'terms.glossary.mindestens'} 8 Zeichen.",
|
||||
"password_update_help": "Leer lassen, um das vorhandene @:{'terms.glossary.passwort'} beizubehalten.",
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
"opening_hours": {
|
||||
"closed": "@:{'phrases.compat.global.closed'}"
|
||||
},
|
||||
"pricing": {
|
||||
"custom_pricing_disabled": "Fallback enabled",
|
||||
"custom_pricing_enabled": "Custom only",
|
||||
"custom_pricing_missing_price": "Missing department prices resolve to 999999.",
|
||||
"custom_pricing_only": "No fallback pricing",
|
||||
"effective_department_price": "Effective department price"
|
||||
},
|
||||
"search_placeholder": "@.capitalize:{'terms.glossary.search'} @:{'terms.glossary.by'} @:{'terms.glossary.department'} @:{'terms.glossary.name'}",
|
||||
"select_department": "@.capitalize:{'terms.glossary.select'} @:{'terms.glossary.a'} @:{'terms.glossary.department'}",
|
||||
"subtitle": "@.capitalize:{'terms.glossary.manage'} @:{'terms.glossary.departments'}",
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
"departments": {
|
||||
"columns": {
|
||||
"archived": "@:{'terms.glossary.archived'}",
|
||||
"custom_pricing_only": "No fallback pricing",
|
||||
"dimension": "Dimension",
|
||||
"economic_department": "@.upper:{'terms.glossary.e'}-@:{'terms.glossary.conomic'} @.capitalize:{'terms.glossary.department'}",
|
||||
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
|
||||
|
||||
@@ -46,6 +46,14 @@
|
||||
"edit_title": "@:{'phrases.compat.limited_backoffice.employees.edit_title'}",
|
||||
"name": "@:{'phrases.compat.limited_backoffice.employees.name'}",
|
||||
"email": "@:{'phrases.compat.limited_backoffice.employees.email'}",
|
||||
"phone": "@:{'phrases.compat.limited_backoffice.employees.phone'}",
|
||||
"phone_validation": "@:{'phrases.compat.limited_backoffice.employees.phone_validation'}",
|
||||
"country_codes": {
|
||||
"denmark": "@:{'phrases.compat.limited_backoffice.employees.country_codes.denmark'}",
|
||||
"sweden": "@:{'phrases.compat.limited_backoffice.employees.country_codes.sweden'}",
|
||||
"norway": "@:{'phrases.compat.limited_backoffice.employees.country_codes.norway'}",
|
||||
"finland": "@:{'phrases.compat.limited_backoffice.employees.country_codes.finland'}"
|
||||
},
|
||||
"password": "@:{'phrases.compat.limited_backoffice.employees.password'}",
|
||||
"password_create_help": "@:{'phrases.compat.limited_backoffice.employees.password_create_help'}",
|
||||
"password_update_help": "@:{'phrases.compat.limited_backoffice.employees.password_update_help'}",
|
||||
@@ -279,6 +287,14 @@
|
||||
"edit_title": "@.capitalize:{'terms.glossary.edit'} @:{'terms.glossary.employee'}",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"phone": "Phone",
|
||||
"phone_validation": "Phone number must be 4-15 digits.",
|
||||
"country_codes": {
|
||||
"denmark": "Denmark",
|
||||
"sweden": "Sweden",
|
||||
"norway": "Norway",
|
||||
"finland": "Finland"
|
||||
},
|
||||
"password": "@.capitalize:{'terms.glossary.password'}",
|
||||
"password_create_help": "@.capitalize:{'terms.glossary.required'} @:{'terms.glossary.for'} new @:{'terms.glossary.employees'}. Use @:{'terms.glossary.at'} @:{'terms.glossary.least'} 8 characters.",
|
||||
"password_update_help": "Leave blank @:{'terms.glossary.to'} keep @:{'terms.replication.article.host_mention'} existing @:{'terms.glossary.password'}.",
|
||||
|
||||
@@ -38,6 +38,13 @@
|
||||
"title": "@:{'phrases.compat.departments.tab.opening_hours'}"
|
||||
},
|
||||
"phone": "@:common.phone",
|
||||
"pricing": {
|
||||
"custom_pricing_disabled": "@:{'phrases.compat.departments.pricing.custom_pricing_disabled'}",
|
||||
"custom_pricing_enabled": "@:{'phrases.compat.departments.pricing.custom_pricing_enabled'}",
|
||||
"custom_pricing_missing_price": "@:{'phrases.compat.departments.pricing.custom_pricing_missing_price'}",
|
||||
"custom_pricing_only": "@:{'phrases.compat.departments.pricing.custom_pricing_only'}",
|
||||
"effective_department_price": "@:{'phrases.compat.departments.pricing.effective_department_price'}"
|
||||
},
|
||||
"products": {
|
||||
"new_product": "@:products.new_product"
|
||||
},
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"departments": {
|
||||
"columns": {
|
||||
"archived": "@:{'phrases.compat.objects.departments.columns.archived'}",
|
||||
"custom_pricing_only": "@:{'phrases.compat.objects.departments.columns.custom_pricing_only'}",
|
||||
"dimension": "@:{'phrases.compat.objects.departments.columns.dimension'}",
|
||||
"economic_department": "@:{'phrases.compat.objects.departments.columns.economic_department'}",
|
||||
"latitude": "@:{'phrases.compat.objects.departments.columns.latitude'}",
|
||||
|
||||
@@ -46,6 +46,14 @@
|
||||
"edit_title": "@:{'phrases.compat.limited_backoffice.employees.edit_title'}",
|
||||
"name": "@:{'phrases.compat.limited_backoffice.employees.name'}",
|
||||
"email": "@:{'phrases.compat.limited_backoffice.employees.email'}",
|
||||
"phone": "@:{'phrases.compat.limited_backoffice.employees.phone'}",
|
||||
"phone_validation": "@:{'phrases.compat.limited_backoffice.employees.phone_validation'}",
|
||||
"country_codes": {
|
||||
"denmark": "@:{'phrases.compat.limited_backoffice.employees.country_codes.denmark'}",
|
||||
"sweden": "@:{'phrases.compat.limited_backoffice.employees.country_codes.sweden'}",
|
||||
"norway": "@:{'phrases.compat.limited_backoffice.employees.country_codes.norway'}",
|
||||
"finland": "@:{'phrases.compat.limited_backoffice.employees.country_codes.finland'}"
|
||||
},
|
||||
"password": "@:{'phrases.compat.limited_backoffice.employees.password'}",
|
||||
"password_create_help": "@:{'phrases.compat.limited_backoffice.employees.password_create_help'}",
|
||||
"password_update_help": "@:{'phrases.compat.limited_backoffice.employees.password_update_help'}",
|
||||
@@ -279,6 +287,14 @@
|
||||
"edit_title": "@:{'terms.glossary.rediger'} @:{'terms.glossary.medarbeider'}",
|
||||
"name": "Navn",
|
||||
"email": "E-post",
|
||||
"phone": "Telefon",
|
||||
"phone_validation": "Telefonnummeret må være 4-15 sifre.",
|
||||
"country_codes": {
|
||||
"denmark": "Danmark",
|
||||
"sweden": "Sverige",
|
||||
"norway": "Norge",
|
||||
"finland": "Finland"
|
||||
},
|
||||
"password": "@.capitalize:{'terms.glossary.passord'}",
|
||||
"password_create_help": "@.capitalize:{'terms.glossary.kreves'} @:{'terms.glossary.for'} nye @:{'terms.glossary.medarbeidere'}. Bruk @:{'terms.glossary.minst'} 8 tegn.",
|
||||
"password_update_help": "La stå tomt @:{'terms.glossary.for'} å beholde nåværende @:{'terms.glossary.passord'}.",
|
||||
|
||||
@@ -46,6 +46,14 @@
|
||||
"edit_title": "@:{'phrases.compat.limited_backoffice.employees.edit_title'}",
|
||||
"name": "@:{'phrases.compat.limited_backoffice.employees.name'}",
|
||||
"email": "@:{'phrases.compat.limited_backoffice.employees.email'}",
|
||||
"phone": "@:{'phrases.compat.limited_backoffice.employees.phone'}",
|
||||
"phone_validation": "@:{'phrases.compat.limited_backoffice.employees.phone_validation'}",
|
||||
"country_codes": {
|
||||
"denmark": "@:{'phrases.compat.limited_backoffice.employees.country_codes.denmark'}",
|
||||
"sweden": "@:{'phrases.compat.limited_backoffice.employees.country_codes.sweden'}",
|
||||
"norway": "@:{'phrases.compat.limited_backoffice.employees.country_codes.norway'}",
|
||||
"finland": "@:{'phrases.compat.limited_backoffice.employees.country_codes.finland'}"
|
||||
},
|
||||
"password": "@:{'phrases.compat.limited_backoffice.employees.password'}",
|
||||
"password_create_help": "@:{'phrases.compat.limited_backoffice.employees.password_create_help'}",
|
||||
"password_update_help": "@:{'phrases.compat.limited_backoffice.employees.password_update_help'}",
|
||||
@@ -279,6 +287,14 @@
|
||||
"edit_title": "@:{'terms.glossary.redigera'} @:{'terms.glossary.medarbetare'}",
|
||||
"name": "Namn",
|
||||
"email": "E-post",
|
||||
"phone": "Telefon",
|
||||
"phone_validation": "Telefonnumret måste vara 4-15 siffror.",
|
||||
"country_codes": {
|
||||
"denmark": "Danmark",
|
||||
"sweden": "Sverige",
|
||||
"norway": "Norge",
|
||||
"finland": "Finland"
|
||||
},
|
||||
"password": "@.capitalize:{'terms.glossary.losenord'}",
|
||||
"password_create_help": "@.capitalize:{'terms.glossary.kravs'} @:{'terms.glossary.for'} nya @:{'terms.glossary.medarbetare'}. Använd @:{'terms.glossary.minst'} 8 tecken.",
|
||||
"password_update_help": "Lämna tomt @:{'terms.glossary.for'} att behålla nuvarande @:{'terms.glossary.losenord'}.",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||
const DATE_ONLY_PREFIX_PATTERN = /^(\d{4}-\d{2}-\d{2})/;
|
||||
|
||||
const padDatePart = (value) => String(value).padStart(2, "0");
|
||||
|
||||
export const isValidDate = (value) => value instanceof Date && !Number.isNaN(value.getTime());
|
||||
|
||||
export const formatLocalDateOnly = (value = new Date()) => {
|
||||
if (typeof value === "string") {
|
||||
const directMatch = value.trim().match(DATE_ONLY_PREFIX_PATTERN);
|
||||
if (directMatch) {
|
||||
return directMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
if (!isValidDate(date)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
date.getFullYear(),
|
||||
padDatePart(date.getMonth() + 1),
|
||||
padDatePart(date.getDate()),
|
||||
].join("-");
|
||||
};
|
||||
|
||||
export const parseLocalDateOnly = (value) => {
|
||||
if (value instanceof Date) {
|
||||
return isValidDate(value)
|
||||
? new Date(value.getFullYear(), value.getMonth(), value.getDate())
|
||||
: new Date(Number.NaN);
|
||||
}
|
||||
|
||||
const stringValue = String(value ?? "").trim();
|
||||
const match = DATE_ONLY_PATTERN.exec(stringValue);
|
||||
if (match) {
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
const day = Number(match[3]);
|
||||
const parsed = new Date(year, month - 1, day);
|
||||
|
||||
if (
|
||||
parsed.getFullYear() === year
|
||||
&& parsed.getMonth() === month - 1
|
||||
&& parsed.getDate() === day
|
||||
) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return new Date(Number.NaN);
|
||||
}
|
||||
|
||||
const parsed = new Date(stringValue);
|
||||
return isValidDate(parsed)
|
||||
? new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate())
|
||||
: parsed;
|
||||
};
|
||||
|
||||
export const todayLocalDateOnly = () => formatLocalDateOnly(new Date());
|
||||
|
||||
export const yesterdayLocalDateOnly = () => {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return formatLocalDateOnly(yesterday);
|
||||
};
|
||||
|
||||
export const startOfLocalDate = (value) => {
|
||||
const date = parseLocalDateOnly(value);
|
||||
if (!isValidDate(date)) {
|
||||
return date;
|
||||
}
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return date;
|
||||
};
|
||||
|
||||
export const endOfLocalDate = (value) => {
|
||||
const date = parseLocalDateOnly(value);
|
||||
if (!isValidDate(date)) {
|
||||
return date;
|
||||
}
|
||||
date.setHours(23, 59, 59, 999);
|
||||
return date;
|
||||
};
|
||||
@@ -16,6 +16,12 @@ import {
|
||||
} from "@/services/limitedBackoffice.js";
|
||||
|
||||
const ROLE_KEYS = ["viewer", "cashier", "booking_coordinator", "operations_lead", "department_admin"];
|
||||
const PHONE_COUNTRY_CODES = [
|
||||
{ value: "45", flag: "🇩🇰", labelKey: "denmark" },
|
||||
{ value: "46", flag: "🇸🇪", labelKey: "sweden" },
|
||||
{ value: "47", flag: "🇳🇴", labelKey: "norway" },
|
||||
{ value: "358", flag: "🇫🇮", labelKey: "finland" },
|
||||
];
|
||||
|
||||
const { t } = useI18n();
|
||||
const departments = ref([]);
|
||||
@@ -32,6 +38,8 @@ const showRolePermissions = ref(false);
|
||||
const form = ref({
|
||||
display_name: "",
|
||||
email: "",
|
||||
phone_country_code: "45",
|
||||
phone: "",
|
||||
password: "",
|
||||
role_key: "viewer",
|
||||
department_ids: [],
|
||||
@@ -212,15 +220,41 @@ const rolePermissionGroups = (role) =>
|
||||
|
||||
const visibleRoles = computed(() => roles.value.filter((role) => ROLE_KEYS.includes(role.key)));
|
||||
const isEditing = computed(() => Boolean(editingEmployee.value));
|
||||
const trimmedEmail = computed(() => String(form.value.email || "").trim());
|
||||
const isEmailValid = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedEmail.value));
|
||||
const normalizedPhone = computed(() => String(form.value.phone || "").replace(/\D/g, ""));
|
||||
const hasPhone = computed(() => normalizedPhone.value.length > 0);
|
||||
const isPhoneValid = computed(() => !hasPhone.value || /^\d{4,15}$/.test(normalizedPhone.value));
|
||||
const isPasswordValid = computed(() => isEditing.value || String(form.value.password || "").length >= 8);
|
||||
const isFormValid = computed(
|
||||
() =>
|
||||
String(form.value.display_name || "").trim().length > 0 &&
|
||||
isEmailValid.value &&
|
||||
isPhoneValid.value &&
|
||||
ROLE_KEYS.includes(form.value.role_key) &&
|
||||
form.value.department_ids.length > 0 &&
|
||||
isPasswordValid.value
|
||||
);
|
||||
|
||||
const countryCodeLabel = (countryCode) => {
|
||||
const option = PHONE_COUNTRY_CODES.find((item) => item.value === String(countryCode || ""));
|
||||
if (!option) {
|
||||
return "";
|
||||
}
|
||||
return `${option.flag} +${option.value} ${t(`templates.limited_backoffice.employees.country_codes.${option.labelKey}`)}`;
|
||||
};
|
||||
|
||||
const formatEmployeePhone = (employee) => {
|
||||
if (!employee?.phone_country_code || !employee?.phone) {
|
||||
return "";
|
||||
}
|
||||
return `+${employee.phone_country_code} ${employee.phone}`;
|
||||
};
|
||||
|
||||
const sanitizePhone = (event) => {
|
||||
form.value.phone = String(event?.target?.value || "").replace(/\D/g, "");
|
||||
};
|
||||
|
||||
const loadEmployees = async () => {
|
||||
const response = await getLimitedBackofficeEmployees({ includeInactive: includeInactive.value });
|
||||
employees.value = unwrapLimitedBackofficeResponse(response) || [];
|
||||
@@ -257,6 +291,8 @@ const resetForm = () => {
|
||||
form.value = {
|
||||
display_name: "",
|
||||
email: "",
|
||||
phone_country_code: "45",
|
||||
phone: "",
|
||||
password: "",
|
||||
role_key: roles.value[0]?.key || "viewer",
|
||||
department_ids: [],
|
||||
@@ -269,6 +305,8 @@ const editEmployee = (employee) => {
|
||||
form.value = {
|
||||
display_name: employee.display_name || "",
|
||||
email: employee.email || "",
|
||||
phone_country_code: employee.phone_country_code ? String(employee.phone_country_code) : "45",
|
||||
phone: employee.phone ? String(employee.phone) : "",
|
||||
password: "",
|
||||
role_key: employee.role?.key || roles.value[0]?.key || "viewer",
|
||||
department_ids: (employee.departments || []).map((department) => Number(department.id)),
|
||||
@@ -291,7 +329,9 @@ const setDepartmentChecked = (departmentId, checked) => {
|
||||
const buildPayload = () => {
|
||||
const payload = {
|
||||
display_name: String(form.value.display_name || "").trim(),
|
||||
email: String(form.value.email || "").trim() || null,
|
||||
email: trimmedEmail.value,
|
||||
phone_country_code: hasPhone.value ? Number.parseInt(form.value.phone_country_code, 10) : null,
|
||||
phone: hasPhone.value ? Number.parseInt(normalizedPhone.value, 10) : null,
|
||||
role_key: form.value.role_key,
|
||||
department_ids: form.value.department_ids,
|
||||
};
|
||||
@@ -416,12 +456,51 @@ onMounted(() => {
|
||||
id="limited-employee-email"
|
||||
v-model="form.email"
|
||||
class="input"
|
||||
:class="{ 'is-danger': formAttempted && !isEmailValid }"
|
||||
type="email"
|
||||
required
|
||||
data-testid="limited-employee-email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="limited-employee-phone">
|
||||
{{ t("templates.limited_backoffice.employees.phone") }}
|
||||
</label>
|
||||
<div class="limited-employee-phone">
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
id="limited-employee-phone-country-code"
|
||||
v-model="form.phone_country_code"
|
||||
data-testid="limited-employee-phone-country-code"
|
||||
>
|
||||
<option v-for="option in PHONE_COUNTRY_CODES" :key="option.value" :value="option.value">
|
||||
{{ countryCodeLabel(option.value) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<input
|
||||
id="limited-employee-phone"
|
||||
v-model="form.phone"
|
||||
class="input"
|
||||
:class="{ 'is-danger': formAttempted && !isPhoneValid }"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
data-testid="limited-employee-phone"
|
||||
@input="sanitizePhone"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="formAttempted && !isPhoneValid" class="help is-danger">
|
||||
{{ t("templates.limited_backoffice.employees.phone_validation") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="limited-employee-password">
|
||||
{{ t("templates.limited_backoffice.employees.password") }}
|
||||
@@ -488,15 +567,17 @@ onMounted(() => {
|
||||
<div class="field">
|
||||
<label class="label">{{ t("templates.limited_backoffice.employees.departments") }}</label>
|
||||
<div class="limited-employee-departments" data-testid="limited-employee-departments">
|
||||
<label v-for="department in departments" :key="department.id" class="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="departmentChecked(department.id)"
|
||||
:data-testid="`limited-employee-department-${department.id}`"
|
||||
@change="setDepartmentChecked(department.id, $event.target.checked)"
|
||||
/>
|
||||
<span>{{ department.name }}</span>
|
||||
</label>
|
||||
<b-switch
|
||||
v-for="department in departments"
|
||||
:key="department.id"
|
||||
size="is-small"
|
||||
type="is-link"
|
||||
:model-value="departmentChecked(department.id)"
|
||||
:data-testid="`limited-employee-department-${department.id}`"
|
||||
@update:model-value="setDepartmentChecked(department.id, $event)"
|
||||
>
|
||||
{{ department.name }}
|
||||
</b-switch>
|
||||
</div>
|
||||
<p v-if="formAttempted && form.department_ids.length === 0" class="help is-danger">
|
||||
{{ t("templates.limited_backoffice.employees.department_required") }}
|
||||
@@ -507,12 +588,12 @@ onMounted(() => {
|
||||
{{ t("templates.limited_backoffice.employees.validation") }}
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<div class="limited-employee-actions">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-dark"
|
||||
class="button is-dark is-fullwidth"
|
||||
:class="{ 'is-loading': saving }"
|
||||
:disabled="saving"
|
||||
:disabled="saving || !isFormValid"
|
||||
data-testid="limited-employee-save"
|
||||
>
|
||||
<span class="icon is-small"><i class="fas fa-save" aria-hidden="true"></i></span>
|
||||
@@ -521,7 +602,7 @@ onMounted(() => {
|
||||
<button
|
||||
v-if="isEditing"
|
||||
type="button"
|
||||
class="button is-light"
|
||||
class="button is-light is-fullwidth"
|
||||
data-testid="limited-employee-cancel"
|
||||
@click="resetForm"
|
||||
>
|
||||
@@ -582,6 +663,13 @@ onMounted(() => {
|
||||
<td>
|
||||
<strong>{{ employee.display_name }}</strong>
|
||||
<p v-if="employee.email" class="has-text-grey is-size-7">{{ employee.email }}</p>
|
||||
<p
|
||||
v-if="formatEmployeePhone(employee)"
|
||||
class="has-text-grey is-size-7"
|
||||
:data-testid="`limited-employee-phone-${employee.id}`"
|
||||
>
|
||||
{{ formatEmployeePhone(employee) }}
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ roleLabel(employee.role) }}</strong>
|
||||
@@ -733,9 +821,14 @@ onMounted(() => {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.limited-employee-departments .checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.limited-employee-phone {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(9.5rem, 0.75fr) minmax(0, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.limited-employee-actions {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -866,6 +959,10 @@ onMounted(() => {
|
||||
.limited-employees-header__inactive {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.limited-employee-phone {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
+4
-3
@@ -19,6 +19,7 @@ import DepartmentDashboardDailyReportNavigation from "@/views/dashboards/departm
|
||||
import DepartmentDashboardDailyReportProductSales from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportProductSales.vue";
|
||||
import DepartmentDashboardDailyReportTodayForm from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportTodayForm.vue";
|
||||
import DepartmentWeather from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentWeather.vue";
|
||||
import { parseLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
import {
|
||||
complaints_metric_state,
|
||||
count_bookings,
|
||||
@@ -131,7 +132,7 @@ onMounted(() => {
|
||||
return [...accessibleDepartmentIds.value];
|
||||
})();
|
||||
|
||||
const initialDateFrom = typeof query.dateFrom === "string" ? query.dateFrom : new Date().toISOString().split("T")[0];
|
||||
const initialDateFrom = typeof query.dateFrom === "string" ? query.dateFrom : todayLocalDateOnly();
|
||||
const initialDateTo = typeof query.dateTo === "string" ? query.dateTo : initialDateFrom;
|
||||
|
||||
initializeDailyReportFilters({
|
||||
@@ -170,7 +171,7 @@ watch(
|
||||
);
|
||||
|
||||
const formatDateShort = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
const date = parseLocalDateOnly(dateString);
|
||||
const dayName = date.toLocaleDateString("da-DK", { weekday: "long" });
|
||||
const dateValue = date.toLocaleDateString("da-DK", { day: "2-digit", month: "2-digit", year: "2-digit" });
|
||||
return `${dayName} d. ${dateValue}`;
|
||||
@@ -178,7 +179,7 @@ const formatDateShort = (dateString) => {
|
||||
|
||||
const formatDateRangeSubtitle = (startDateString, endDateString) => {
|
||||
const dateOptions = { day: "2-digit", month: "2-digit", year: "2-digit" };
|
||||
return `${new Date(startDateString).toLocaleDateString("da-DK", dateOptions)} - ${new Date(endDateString).toLocaleDateString("da-DK", dateOptions)}`;
|
||||
return `${parseLocalDateOnly(startDateString).toLocaleDateString("da-DK", dateOptions)} - ${parseLocalDateOnly(endDateString).toLocaleDateString("da-DK", dateOptions)}`;
|
||||
};
|
||||
|
||||
const formatSubtitle = (subtitle) => {
|
||||
|
||||
+3
-5
@@ -2,18 +2,16 @@
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { createEmptyOutsideHours, normalizeOutsideHours } from "@/views/dashboards/departmentDashboard/modules/daily-report/outsideHours.js";
|
||||
import { todayLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
const GENERATE_UNIQUE_ID = () => Math.random().toString(36).slice(2, 11);
|
||||
|
||||
const DATE_TODAY = () => {
|
||||
const date = new Date();
|
||||
return date.toISOString().split("T")[0];
|
||||
return todayLocalDateOnly();
|
||||
};
|
||||
|
||||
const DATE_YESTERDAY = () => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 1);
|
||||
return date.toISOString().split("T")[0];
|
||||
return yesterdayLocalDateOnly();
|
||||
};
|
||||
|
||||
const date_shortcuts = ref({
|
||||
|
||||
+3
-9
@@ -17,6 +17,7 @@ import {
|
||||
isLoading as departmentsStoreLoading,
|
||||
} from "@/components/pagination/departmentTabs.vue";
|
||||
import { isAccessibleVisibleDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js";
|
||||
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
/**
|
||||
* Props:
|
||||
@@ -141,14 +142,7 @@ const toggleAllDepartments = () => {
|
||||
};
|
||||
|
||||
const formatDateSelectionValue = (date) => {
|
||||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
return formatLocalDateOnly(date);
|
||||
};
|
||||
|
||||
const onDateSelectionChange = (startDate, endDate) => {
|
||||
@@ -161,7 +155,7 @@ const onDateSelectionChange = (startDate, endDate) => {
|
||||
<div class="column is-12" data-testid="daily-report-date-controls">
|
||||
<DatePeriodSelector
|
||||
:on-selection-change="onDateSelectionChange"
|
||||
:selection="{ startDate: new Date(selected_date), endDate: new Date(selected_date_to) }"
|
||||
:selection="{ startDate: parseLocalDateOnly(selected_date), endDate: parseLocalDateOnly(selected_date_to) }"
|
||||
:visibility="{
|
||||
showDailySelector: true,
|
||||
showWeeklySelector: true,
|
||||
|
||||
+4
-3
@@ -5,6 +5,7 @@ import { parseError, getError, clearErrors} from "@/components/request/HandleGlo
|
||||
import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
||||
import { selected_date, onChangeCall, selected_department_id} from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
|
||||
import Swal from "sweetalert2";
|
||||
import { parseLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
const props = defineProps({
|
||||
department_id: Number,
|
||||
@@ -23,7 +24,7 @@ const created_at = ref(null);
|
||||
const id = ref(null);
|
||||
const message = ref('');
|
||||
const yesterday = ref({
|
||||
date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split('T')[0],
|
||||
date: yesterdayLocalDateOnly(),
|
||||
water_usage: 0,
|
||||
water_usage_morning: 0,
|
||||
notes: null,
|
||||
@@ -104,7 +105,7 @@ const getDailyReport = async () => {
|
||||
// Check if the date is today
|
||||
const created_at_val = new Date(response.data.data.created_at);
|
||||
const today = new Date();
|
||||
if (created_at_val.toDateString() === (selected_date.value ? new Date(selected_date.value) : today).toDateString()) {
|
||||
if (created_at_val.toDateString() === (selected_date.value ? parseLocalDateOnly(selected_date.value) : today).toDateString()) {
|
||||
// The daily report is today
|
||||
// So we can set the values
|
||||
water_usage.value = parseInt(response.data.data.water_usage);
|
||||
@@ -212,4 +213,4 @@ onChangeCall(() => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
+4
-3
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { timeBookingsNewScheduler } from '@/views/dashboards/departmentDashboard/modules/time-bookings/book/TimeBookingsNewScheduler.vue';
|
||||
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||
const date = ref();
|
||||
// Set the initial date to the current date YYYY-MM-DD format
|
||||
const formatDate = (date_input) => {
|
||||
return date_input.toISOString().split('T')[0];
|
||||
return formatLocalDateOnly(date_input);
|
||||
};
|
||||
date.value = formatDate(new Date());
|
||||
|
||||
@@ -15,7 +16,7 @@ watch(timeBookingsNewScheduler.date.selected, (newDate) => {
|
||||
|
||||
watch(date, (newDate) => {
|
||||
// Update the selected date in the scheduler when the date input changes
|
||||
timeBookingsNewScheduler.date.selected.value = new Date(newDate);
|
||||
timeBookingsNewScheduler.date.selected.value = parseLocalDateOnly(newDate);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -63,4 +64,4 @@ watch(date, (newDate) => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
+2
-2
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { SessionUser } from '@/components/session/token/SessionUser.vue';
|
||||
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
const props = defineProps({
|
||||
department_id: {
|
||||
@@ -68,8 +69,7 @@ const getBookings = async () => {
|
||||
};
|
||||
|
||||
const getDate = () => {
|
||||
const date = new Date();
|
||||
return date.toISOString().split('T')[0];
|
||||
return todayLocalDateOnly();
|
||||
};
|
||||
|
||||
// Get the department
|
||||
|
||||
+4
-3
@@ -2,6 +2,7 @@
|
||||
|
||||
import { selected_date, selected_date_to, selectDate as selectDailyReportDate } from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
|
||||
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
|
||||
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
// Emits for parents that want to react to date changes (non-breaking if unused)
|
||||
const emit = defineEmits(['date-change']);
|
||||
@@ -16,12 +17,12 @@ const selectDate = (startIso, endIso) => {
|
||||
|
||||
<template>
|
||||
<DatePeriodSelector
|
||||
:on-selection-change="(startDate, endDate) => selectDate(startDate.toISOString().split('T')[0], endDate.toISOString().split('T')[0])"
|
||||
:selection="{ startDate: new Date(selected_date), endDate: new Date(selected_date_to) }"
|
||||
:on-selection-change="(startDate, endDate) => selectDate(formatLocalDateOnly(startDate), formatLocalDateOnly(endDate))"
|
||||
:selection="{ startDate: parseLocalDateOnly(selected_date), endDate: parseLocalDateOnly(selected_date_to) }"
|
||||
:visibility="{ showDailySelector: true, showWeeklySelector: true, showMultipleMonthWarning: false, showUpdateButton: false }"
|
||||
:reverse-level-order="true"
|
||||
@update:selection="(newSelection) => {
|
||||
selectDate(newSelection.startDate.toISOString().split('T')[0], newSelection.endDate.toISOString().split('T')[0]);
|
||||
selectDate(formatLocalDateOnly(newSelection.startDate), formatLocalDateOnly(newSelection.endDate));
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
+4
-3
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {defineComponent, ref, watch} from 'vue';
|
||||
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
|
||||
type fetch = {
|
||||
@@ -11,8 +12,8 @@ type fetch = {
|
||||
};
|
||||
/** Define the variables */
|
||||
export const show_this_week = ref(false);
|
||||
export const selected_date = ref(new Date().toISOString().split('T')[0]);
|
||||
export const selected_date_to = ref(new Date().toISOString().split('T')[0]);
|
||||
export const selected_date = ref(todayLocalDateOnly());
|
||||
export const selected_date_to = ref(todayLocalDateOnly());
|
||||
export const last_fetch_id = ref(0);
|
||||
export const fetches = ref<fetch[]>([]);
|
||||
/** Define the functions */
|
||||
@@ -53,4 +54,4 @@ watch([selected_date, selected_date_to], () => {
|
||||
export default defineComponent({
|
||||
name: 'DepartmentsOverviewObject'
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import {
|
||||
} from "../../imports/InvoicingBillingPeriodImportPaging.js";
|
||||
import { buildPossibleDuplicateGroups } from "../../utils/possibleDuplicateGroups.js";
|
||||
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
|
||||
import { SELFWASH_PERIOD_ALL_LIMIT } from "@/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js";
|
||||
|
||||
const period_result = ref<any>(null);
|
||||
const route = useRoute();
|
||||
@@ -416,7 +417,7 @@ const loadSelfWashCounts = async ({ forceRefresh = false } = {}) => {
|
||||
'GET',
|
||||
{
|
||||
page: 1,
|
||||
limit: 500,
|
||||
limit: SELFWASH_PERIOD_ALL_LIMIT,
|
||||
filters,
|
||||
order: 'StartTime:desc',
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ export const PERIOD_CACHE_TTL_MS = 10 * 60 * 1000;
|
||||
export const PERIOD_LIMIT_OPTIONS = [10, 25, 50, 100, 200, 500, "all"];
|
||||
|
||||
const CACHE_PREFIX = "invoicing-period-page:v2:";
|
||||
const SELF_WASH_COUNTS_CACHE_PREFIX = "invoicing-period-self-wash-counts:";
|
||||
const SELF_WASH_COUNTS_CACHE_PREFIX = "invoicing-period-self-wash-counts:v2:";
|
||||
const DEFAULT_LIMIT = 100;
|
||||
const ALL_LIMIT = "all";
|
||||
|
||||
|
||||
@@ -3,7 +3,15 @@ import PageTitle from "@/components/global/PageTitle.vue";
|
||||
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import DepartmentSubPageWrapper from "@/views/dashboards/superUserDashboard/department/DepartmentSubPageWrapper.vue";
|
||||
import { setDepartment, department, departmentId, getDepartmentPrice, editDepartmentPrice } from "@/views/dashboards/superUserDashboard/department/SuperUserSelectedDepartmentObject.vue";
|
||||
import {
|
||||
CUSTOM_PRICING_MISSING_PRICE,
|
||||
setDepartment,
|
||||
getDepartmentPrices,
|
||||
getExplicitDepartmentPrice,
|
||||
editDepartmentPrice,
|
||||
isCustomPricingOnly,
|
||||
updateCustomPricingOnly,
|
||||
} from "@/views/dashboards/superUserDashboard/department/SuperUserSelectedDepartmentObject.vue";
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref } from 'vue';
|
||||
import { getProducts } from "@/components/shop/Products.vue";
|
||||
@@ -11,14 +19,40 @@ import { getProducts } from "@/components/shop/Products.vue";
|
||||
// Get the department from the route
|
||||
const router = useRouter()
|
||||
setDepartment(router.currentRoute.value.params.departmentId);
|
||||
getDepartmentPrices();
|
||||
|
||||
// Set the products
|
||||
const products = ref([]);
|
||||
const isUpdatingCustomPricingOnly = ref(false);
|
||||
|
||||
// Get the products
|
||||
getProducts().then((response) => {
|
||||
products.value = response.data.data;
|
||||
});
|
||||
|
||||
const getDepartmentPriceDisplay = (product) => {
|
||||
const explicitPrice = getExplicitDepartmentPrice(product);
|
||||
if (explicitPrice !== null) {
|
||||
return explicitPrice;
|
||||
}
|
||||
return isCustomPricingOnly() ? CUSTOM_PRICING_MISSING_PRICE : '-';
|
||||
};
|
||||
|
||||
const isMissingCustomPrice = (product) => {
|
||||
return isCustomPricingOnly() && getExplicitDepartmentPrice(product) === null;
|
||||
};
|
||||
|
||||
const toggleCustomPricingOnly = async (event) => {
|
||||
const enabled = event.target.checked;
|
||||
isUpdatingCustomPricingOnly.value = true;
|
||||
try {
|
||||
await updateCustomPricingOnly(enabled);
|
||||
} catch {
|
||||
event.target.checked = isCustomPricingOnly();
|
||||
} finally {
|
||||
isUpdatingCustomPricingOnly.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -28,12 +62,38 @@ getProducts().then((response) => {
|
||||
<PageTitle title="Department" subtitle="Department pricing" />
|
||||
</template>
|
||||
<div>
|
||||
<section class="department-pricing-settings" data-testid="department-custom-pricing-settings">
|
||||
<div>
|
||||
<h2 class="title is-5 mb-1">{{ $t('departments.pricing.custom_pricing_only') }}</h2>
|
||||
<p class="is-size-7 has-text-grey">
|
||||
{{ $t('departments.pricing.custom_pricing_missing_price') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="department-pricing-toggle">
|
||||
<input
|
||||
id="department-custom-pricing-only"
|
||||
class="switch is-rounded is-info"
|
||||
type="checkbox"
|
||||
:checked="isCustomPricingOnly()"
|
||||
:disabled="isUpdatingCustomPricingOnly"
|
||||
data-testid="department-custom-pricing-only-toggle"
|
||||
@change="toggleCustomPricingOnly"
|
||||
/>
|
||||
<label for="department-custom-pricing-only">
|
||||
{{
|
||||
isCustomPricingOnly()
|
||||
? $t('departments.pricing.custom_pricing_enabled')
|
||||
: $t('departments.pricing.custom_pricing_disabled')
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
<table class="is-fullwidth table table-striped is-hoverable is-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('common.product') }}</th>
|
||||
<th>{{ $t('tables.common.default_price') }}</th>
|
||||
<th>{{ $t('tables.common.department_price') }}</th>
|
||||
<th>{{ $t('departments.pricing.effective_department_price') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -43,24 +103,45 @@ getProducts().then((response) => {
|
||||
<td
|
||||
@click="editDepartmentPrice(product)"
|
||||
class="is-clickable"
|
||||
>{{ getDepartmentPrice(product) === product.price ? '-' : getDepartmentPrice(product) }}
|
||||
:class="{ 'has-text-danger has-text-weight-semibold': isMissingCustomPrice(product) }"
|
||||
:data-testid="`department-price-cell-${product.id}`"
|
||||
>{{ getDepartmentPriceDisplay(product) }}
|
||||
<i class="is-pulled-right fas fa-edit"></i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
Department prices...
|
||||
<code>{{ departmentId }}</code>
|
||||
<code>{{ department.id }}</code>
|
||||
<code>{{ department.name }}</code>
|
||||
<code>{{ department.description }}</code>
|
||||
<code>{{ department.created_at }}</code>
|
||||
<code>{{ department.updated_at }}</code>
|
||||
</div>
|
||||
</DepartmentSubPageWrapper>
|
||||
</RestrictedPageWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.department-pricing-settings {
|
||||
align-items: center;
|
||||
border: 1px solid #dbdbdb;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
.department-pricing-toggle {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
min-width: 16rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.department-pricing-settings {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.department-pricing-toggle {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+40
-3
@@ -4,12 +4,14 @@ import { authenticatedRequest } from "@/components/session/authenticatedRequest.
|
||||
import Swal from "sweetalert2";
|
||||
// Define the department id
|
||||
export const departmentId = ref(0);
|
||||
export const CUSTOM_PRICING_MISSING_PRICE = 999999;
|
||||
|
||||
const default_department = {
|
||||
id: null,
|
||||
name: null,
|
||||
description: null,
|
||||
branding: null,
|
||||
custom_pricing_only: false,
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
prices: []
|
||||
@@ -34,6 +36,7 @@ export const department = {
|
||||
id: ref(''),
|
||||
name: ref(''),
|
||||
description: ref(''),
|
||||
custom_pricing_only: ref(false),
|
||||
created_at: ref(''),
|
||||
updated_at: ref(''),
|
||||
prices: ref([])
|
||||
@@ -49,6 +52,13 @@ const isDepartmentPricesLoaded = ref(false);
|
||||
// Set the department id
|
||||
export const setDepartment = (id) => {
|
||||
departmentId.value = id;
|
||||
isDepartmentPricesLoaded.value = false;
|
||||
department.prices.value = [];
|
||||
department.custom_pricing_only.value = false;
|
||||
departmentAdvanced.value = {
|
||||
...default_department,
|
||||
...default_department_functions
|
||||
};
|
||||
// Load the department data
|
||||
return getDepartmentData();
|
||||
}
|
||||
@@ -60,11 +70,13 @@ export const getDepartmentData = async () => {
|
||||
department.id.value = response.data.data.id;
|
||||
department.name.value = response.data.data.name;
|
||||
department.description.value = response.data.data.description;
|
||||
department.custom_pricing_only.value = Boolean(response.data.data.custom_pricing_only);
|
||||
department.created_at.value = response.data.data.created_at;
|
||||
department.updated_at.value = response.data.data.updated_at;
|
||||
departmentAdvanced.value = {
|
||||
...default_department,
|
||||
...response.data.data,
|
||||
custom_pricing_only: Boolean(response.data.data.custom_pricing_only),
|
||||
...default_department_functions
|
||||
};
|
||||
})
|
||||
@@ -88,17 +100,42 @@ export const getDepartmentPrice = (product) => {
|
||||
getDepartmentPrices();
|
||||
}
|
||||
if (isDepartmentPricesLoaded.value) {
|
||||
const price = department.prices.value.find((price) => price.product_id === product.id);
|
||||
return price ? price.price : product.price;
|
||||
const price = getExplicitDepartmentPrice(product);
|
||||
if (price !== null) {
|
||||
return price;
|
||||
}
|
||||
return isCustomPricingOnly() ? CUSTOM_PRICING_MISSING_PRICE : product.price;
|
||||
}
|
||||
return product.price;
|
||||
}
|
||||
|
||||
export const getExplicitDepartmentPrice = (product) => {
|
||||
const price = department.prices.value.find((price) => price.product_id === product.id);
|
||||
return price ? price.price : null;
|
||||
}
|
||||
|
||||
export const isCustomPricingOnly = () => Boolean(departmentAdvanced.value.custom_pricing_only || department.custom_pricing_only.value);
|
||||
|
||||
export const updateCustomPricingOnly = async (enabled) => {
|
||||
return authenticatedRequest(`/departments`, "PUT", {
|
||||
id: departmentId.value,
|
||||
custom_pricing_only: Boolean(enabled)
|
||||
}).then(async () => {
|
||||
department.custom_pricing_only.value = Boolean(enabled);
|
||||
departmentAdvanced.value = {
|
||||
...departmentAdvanced.value,
|
||||
custom_pricing_only: Boolean(enabled)
|
||||
};
|
||||
await getDepartmentData();
|
||||
});
|
||||
}
|
||||
|
||||
export const editDepartmentPrice = (product) => {
|
||||
const explicitPrice = getExplicitDepartmentPrice(product);
|
||||
Swal.fire({
|
||||
title: product.name + ' ( product: ' + product.id + ' )',
|
||||
input: 'number',
|
||||
inputValue: getDepartmentPrice(product),
|
||||
inputValue: explicitPrice === null ? '' : explicitPrice,
|
||||
inputLabel: 'Price',
|
||||
inputAttributes: {
|
||||
autocapitalize: 'off'
|
||||
|
||||
@@ -13,6 +13,7 @@ import StatisticsDepartmentGoal
|
||||
from "@/views/dashboards/superUserDashboard/statistics/displays/overview/StatisticsDepartmentGoal.vue";
|
||||
import {departments, getDepartments} from "@/components/pagination/departmentTabs.vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -70,7 +71,8 @@ getDepartments()
|
||||
// Get the statistics
|
||||
const getStatistics = async () => {
|
||||
// Get the statistics
|
||||
const response = await authenticatedRequest('/statistics/income/departments?start_date=' + (new Date()).toISOString().split('T')[0] + '&end_date=' + (new Date()).toISOString().split('T')[0])
|
||||
const today = todayLocalDateOnly();
|
||||
const response = await authenticatedRequest('/statistics/income/departments?start_date=' + today + '&end_date=' + today)
|
||||
// Get the sent data
|
||||
statistics_department_today.value = response.data.data
|
||||
// Log the data
|
||||
@@ -131,4 +133,4 @@ getStatistics()
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {ref} from 'vue';
|
||||
import { Bar } from 'vue-chartjs';
|
||||
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js'
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { todayLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||
@@ -16,7 +17,7 @@ const chartOptions = {
|
||||
// Shortcut to statistics
|
||||
|
||||
// Set the time for the statistics to today
|
||||
SessionUser.adminUser.statistics.set_time((new Date()).toISOString().split('T')[0], (new Date()).toISOString().split('T')[0])
|
||||
SessionUser.adminUser.statistics.set_time(todayLocalDateOnly(), todayLocalDateOnly())
|
||||
|
||||
|
||||
|
||||
@@ -33,4 +34,4 @@ SessionUser.adminUser.statistics.set_time((new Date()).toISOString().split('T')[
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -16,6 +16,7 @@ const json = (body: unknown, status = 200) => ({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const pageReadyTimeout = process.env.CI ? 30_000 : 15_000;
|
||||
|
||||
const setupNotificationSmsApi = async (page: Page) => {
|
||||
let listRequestCount = 0;
|
||||
@@ -115,9 +116,10 @@ test("@smoke department notification SMS active toggle and delete refresh the ta
|
||||
const notificationSmsApi = await setupNotificationSmsApi(page);
|
||||
|
||||
await page.goto("/admin/1/modules/notifications", { waitUntil: "domcontentloaded" });
|
||||
await expect.poll(() => notificationSmsApi.listRequestCount, { timeout: pageReadyTimeout }).toBeGreaterThan(0);
|
||||
|
||||
const row = page.locator("tr", { hasText: "Dispatch line" });
|
||||
await expect(row).toBeVisible();
|
||||
await expect(row).toBeVisible({ timeout: pageReadyTimeout });
|
||||
const enabledToggle = page.getByTestId("department-notification-sms-enabled-toggle-501");
|
||||
await expect(enabledToggle).toBeChecked();
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ const POS_PERMISSIONS = [
|
||||
const SUPERUSER_POS_PERMISSIONS = [...POS_PERMISSIONS, "superuser"];
|
||||
|
||||
const POS_BOOT_URL = "/admin/12/modules/pos?step=1";
|
||||
const POS_BOOT_READY_TIMEOUT = 20_000;
|
||||
const POS_ORDER_READY_TIMEOUT = 20_000;
|
||||
const DRAFT_TRANSACTION_CUSTOMER_ID = 44556677;
|
||||
const DRAFT_TRANSACTION_CUSTOMER_NAME = "(TEST) Draft Transaction Customer";
|
||||
|
||||
@@ -49,6 +51,22 @@ function getPosBootStep(page: Page) {
|
||||
return page.locator('[data-testid="pos-step-1"]:visible, [data-testid="pos-mobile-step-1"]:visible').first();
|
||||
}
|
||||
|
||||
function getPosOrderReadyTarget(page: Page) {
|
||||
return page
|
||||
.locator(
|
||||
[
|
||||
'[data-testid="pos-order-card"]:visible',
|
||||
'[data-testid="pos-order-main"]:visible',
|
||||
'[data-testid="pos-order-detail"]:visible',
|
||||
].join(", ")
|
||||
)
|
||||
.first();
|
||||
}
|
||||
|
||||
async function expectPosOrderReady(page: Page, timeout = POS_ORDER_READY_TIMEOUT) {
|
||||
await expect(getPosOrderReadyTarget(page)).toBeVisible({ timeout });
|
||||
}
|
||||
|
||||
async function navigateToPosBootPage(page: Page, { waitForSession = false }: { waitForSession?: boolean } = {}) {
|
||||
let lastError: unknown;
|
||||
|
||||
@@ -64,9 +82,8 @@ async function navigateToPosBootPage(page: Page, { waitForSession = false }: { w
|
||||
|
||||
try {
|
||||
await page.goto(POS_BOOT_URL, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
||||
await Promise.race([sessionRequest, getPosBootStep(page).waitFor({ state: "visible", timeout: 10_000 })]);
|
||||
await expect(getPosBootStep(page)).toBeVisible({ timeout: 10_000 });
|
||||
await sessionRequest;
|
||||
await expect(getPosBootStep(page)).toBeVisible({ timeout: POS_BOOT_READY_TIMEOUT });
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
@@ -162,19 +179,24 @@ async function waitForSwalToClose(page: Page) {
|
||||
|
||||
async function openOrderSettings(page: Page, orderId: number) {
|
||||
await page.goto(`/admin/12/modules/pos/orders/${orderId}`);
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible({ timeout: 20_000 });
|
||||
await expectPosOrderReady(page);
|
||||
await clickVisibleTestId(page, "pos-order-tab-settings");
|
||||
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
|
||||
}
|
||||
|
||||
async function revisitCurrentPage(page: Page) {
|
||||
await page.goto(page.url(), { waitUntil: "domcontentloaded" });
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible({ timeout: 20_000 });
|
||||
if (new URL(page.url()).pathname.includes("/modules/pos/orders/")) {
|
||||
await expectPosOrderReady(page);
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(getPosBootStep(page)).toBeVisible({ timeout: POS_BOOT_READY_TIMEOUT });
|
||||
}
|
||||
|
||||
async function reloadOrderSettings(page: Page, orderId: number) {
|
||||
await revisitCurrentPage(page);
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
||||
await page.goto(`/admin/12/modules/pos/orders/${orderId}`, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
||||
await expectPosOrderReady(page);
|
||||
await clickVisibleTestId(page, "pos-order-tab-settings");
|
||||
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
|
||||
}
|
||||
@@ -313,17 +335,18 @@ async function changeOrderInvoiceCollection(page: Page, closedAt: string) {
|
||||
async function openOrderDetail(page: Page, orderId = 54518) {
|
||||
const orderUrl = `/admin/12/modules/pos/orders/${orderId}`;
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
await page.goto(orderUrl, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible({ timeout: 10_000 });
|
||||
await expectPosOrderReady(page);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (attempt === 1) {
|
||||
if (attempt === 2) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await navigateToPosBootPage(page);
|
||||
await page.goto("about:blank").catch(() => {});
|
||||
await delay(250);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -989,7 +1012,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
||||
expect(Math.abs((beforeRailBox?.width ?? 0) - (restoredRailBox?.width ?? 0))).toBeLessThanOrEqual(20);
|
||||
|
||||
await page.goto("/admin/12/modules/pos/orders/54518", { waitUntil: "domcontentloaded" });
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
||||
await expectPosOrderReady(page);
|
||||
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
|
||||
await expectOrderTotal(page, 1397);
|
||||
});
|
||||
@@ -1107,7 +1130,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
||||
await expect(noteInput).toBeFocused();
|
||||
|
||||
await revisitCurrentPage(page);
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
||||
await expectPosOrderReady(page);
|
||||
await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toContainText(referenceValue);
|
||||
await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toContainText(poValue);
|
||||
await expect(page.getByTestId("pos-order-metadata-note")).toContainText(noteValue);
|
||||
@@ -1160,7 +1183,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
||||
});
|
||||
|
||||
await revisitCurrentPage(page);
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
||||
await expectPosOrderReady(page);
|
||||
await expect(page.getByTestId("pos-order-customer-wishes-reference")).toContainText("+ Tilføj");
|
||||
await expect(page.getByTestId("pos-order-customer-wishes-po")).toContainText("+ Tilføj");
|
||||
});
|
||||
@@ -1221,7 +1244,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
||||
await expect(reg3Input).toHaveValue("NO9012", { timeout: 10000 });
|
||||
|
||||
await revisitCurrentPage(page);
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
||||
await expectPosOrderReady(page);
|
||||
await expect(page.getByTestId("pos-order-registration-1")).toContainText("XY1234");
|
||||
await expect(page.getByTestId("pos-order-registration-2")).toContainText("TR5678");
|
||||
await expect(page.getByTestId("pos-order-registration-3")).toContainText("NO9012");
|
||||
@@ -1249,7 +1272,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
||||
await expectOrderTotal(page, 2123);
|
||||
|
||||
await revisitCurrentPage(page);
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
||||
await expectPosOrderReady(page);
|
||||
await expect(page.getByTestId("pos-order-item-note-trigger-9101")).toBeVisible();
|
||||
await expect(page.getByTestId("pos-order-item-reference-trigger-9101")).toBeVisible();
|
||||
await expect(page.getByTestId("pos-order-item-price-9101")).toContainText("1400");
|
||||
@@ -1274,7 +1297,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
||||
await expectOrderTotal(page, 1423);
|
||||
|
||||
await revisitCurrentPage(page);
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
||||
await expectPosOrderReady(page);
|
||||
await expect(page.getByTestId("pos-order-item-note-trigger-9102")).toBeVisible();
|
||||
await expect(page.getByTestId("pos-order-item-price-9102")).toContainText("450");
|
||||
await expectOrderTotal(page, 1423);
|
||||
@@ -1324,7 +1347,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
||||
await expectOrderTotal(page, 0);
|
||||
|
||||
await revisitCurrentPage(page);
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
||||
await expectPosOrderReady(page);
|
||||
await expect(page.getByTestId("pos-order-empty-state")).toBeVisible();
|
||||
await expectOrderTotal(page, 0);
|
||||
});
|
||||
@@ -1347,7 +1370,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
||||
await expectOrderTotal(page, 2021);
|
||||
|
||||
await revisitCurrentPage(page);
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
||||
await expectPosOrderReady(page);
|
||||
await expect(page.getByTestId("pos-order-item-edit-9200").first()).toBeVisible();
|
||||
await expectOrderTotal(page, 2021);
|
||||
});
|
||||
@@ -1432,6 +1455,8 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
||||
});
|
||||
|
||||
test("persists scalar metadata edits across settings and details tabs", async ({ page }) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
const { orderId } = await createDisposableOrder(page);
|
||||
const submittedValues = {
|
||||
reference: "UPDATED-REF-2026",
|
||||
@@ -2964,7 +2989,7 @@ test.describe("Admin POS Orders - mobile smoke", () => {
|
||||
|
||||
test("opens the settings tab and renders the new controls", async ({ page }) => {
|
||||
await page.goto("/admin/12/modules/pos/orders/54518");
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
||||
await expectPosOrderReady(page);
|
||||
|
||||
await clickVisibleTestId(page, "pos-order-tab-settings");
|
||||
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
|
||||
@@ -3093,7 +3118,7 @@ test.describe("Admin POS Orders - mobile smoke", () => {
|
||||
expect(Math.abs((restoredMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0))).toBeLessThanOrEqual(20);
|
||||
|
||||
await revisitCurrentPage(page);
|
||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
||||
await expectPosOrderReady(page);
|
||||
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
|
||||
await expectOrderTotal(page, 1397);
|
||||
});
|
||||
|
||||
@@ -109,6 +109,58 @@ async function acceleratePageTimers(page, timerScale = 0.01) {
|
||||
}, timerScale);
|
||||
}
|
||||
|
||||
async function dismissUnexpectedSweetAlert(page) {
|
||||
const overlays = page.locator(".swal2-container.swal2-backdrop-show");
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
const overlay = overlays.first();
|
||||
if (!(await overlay.isVisible().catch(() => false))) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dismissed = false;
|
||||
for (const selector of [".swal2-close", ".swal2-cancel", ".swal2-deny", ".swal2-confirm"]) {
|
||||
const action = overlay.locator(selector).first();
|
||||
if (await action.isVisible().catch(() => false)) {
|
||||
await action.click({ force: true });
|
||||
dismissed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dismissed) {
|
||||
await page.keyboard.press("Escape");
|
||||
}
|
||||
|
||||
await expect(overlays)
|
||||
.toHaveCount(0, { timeout: 5_000 })
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function generateGatewayInstaller(page) {
|
||||
const button = page.getByTestId("gateway-installer-generate");
|
||||
await expect(button).toBeVisible({ timeout: 15_000 });
|
||||
await expect(button).toBeEnabled({ timeout: 15_000 });
|
||||
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
await dismissUnexpectedSweetAlert(page);
|
||||
|
||||
try {
|
||||
await button.click({ timeout: 15_000 });
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (!/swal2-container|intercepts pointer events/i.test(error?.message || "")) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
test.describe("Edge gateway management smoke", () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
@@ -193,7 +245,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
|
||||
await page.getByTestId("gateway-installer-department").selectOption("1");
|
||||
await page.getByTestId("gateway-installer-label").fill("Copy Test Pi");
|
||||
await page.getByTestId("gateway-installer-generate").click();
|
||||
await generateGatewayInstaller(page);
|
||||
|
||||
await expect(page.getByTestId("gateway-install-command")).toHaveValue(/install\.sh\?token=edge-install-token/);
|
||||
await page.getByTestId("gateway-installer-copy").click();
|
||||
@@ -246,7 +298,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
|
||||
await page.getByTestId("gateway-installer-department").selectOption("1");
|
||||
await page.getByTestId("gateway-installer-label").fill("Canary Pi");
|
||||
await page.getByTestId("gateway-installer-generate").click();
|
||||
await generateGatewayInstaller(page);
|
||||
|
||||
await expect(page.getByTestId("gateway-installer-status")).toBeVisible();
|
||||
await expect.poll(() => page.getByTestId("gateway-installer-status-state").textContent()).toContain("Running");
|
||||
@@ -274,7 +326,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
|
||||
await page.getByTestId("gateway-installer-department").selectOption("1");
|
||||
await page.getByTestId("gateway-installer-label").fill("CPH Edge 01");
|
||||
await page.getByTestId("gateway-installer-generate").click();
|
||||
await generateGatewayInstaller(page);
|
||||
|
||||
await expect(page.getByTestId("gateway-installer-status")).toBeVisible();
|
||||
await expect.poll(() => page.getByTestId("gateway-installer-status-state").textContent()).toContain("Running");
|
||||
@@ -318,7 +370,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
|
||||
await page.getByTestId("gateway-installer-department").selectOption("1");
|
||||
await page.getByTestId("gateway-installer-label").fill("Broken Pi");
|
||||
await page.getByTestId("gateway-installer-generate").click();
|
||||
await generateGatewayInstaller(page);
|
||||
|
||||
await expect(page.getByTestId("gateway-installer-status")).toBeVisible();
|
||||
await expect
|
||||
@@ -382,7 +434,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
|
||||
await page.getByTestId("gateway-installer-department").selectOption("1");
|
||||
await page.getByTestId("gateway-installer-label").fill("Canary Pi");
|
||||
await page.getByTestId("gateway-installer-generate").click();
|
||||
await generateGatewayInstaller(page);
|
||||
|
||||
await expect(page.getByTestId("gateway-install-command")).toHaveValue(/install\.sh\?token=edge-install-token/);
|
||||
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents\/703\/overview$/, { timeout: 20_000 });
|
||||
|
||||
@@ -1132,7 +1132,6 @@ test.describe("Invoicing period tab", () => {
|
||||
};
|
||||
|
||||
await openPeriodView(page);
|
||||
await setEntireMarchPeriod(page);
|
||||
|
||||
await page.route("**/modules/xlvask/services/usage/orders**", async (route) => {
|
||||
const path = new URL(route.request().url()).pathname;
|
||||
@@ -1242,8 +1241,10 @@ test.describe("Invoicing period tab", () => {
|
||||
);
|
||||
});
|
||||
|
||||
await setEntireMarchPeriod(page);
|
||||
|
||||
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toBeVisible();
|
||||
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toContainText("Selvvask (0/1)");
|
||||
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toContainText("Selvvask (0/2)");
|
||||
await expect(page.getByTestId("invoicing-period-view-selector-progress-self_wash")).toBeVisible();
|
||||
|
||||
await page.getByTestId("invoicing-period-view-selector-self_wash").click();
|
||||
@@ -1273,6 +1274,7 @@ test.describe("Invoicing period tab", () => {
|
||||
await page.getByTestId("xlvask-automation-accept-8101").click();
|
||||
await expect(page.getByTestId("xlvask-automation-suggestion-8101")).toContainText("Accepteret #7001");
|
||||
expect(automationAcceptRequests).toEqual([{ suggestion_id: 7101 }]);
|
||||
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toContainText("Selvvask (1/2)");
|
||||
|
||||
await expect.poll(() => usageOrderRequests.length).toBeGreaterThan(0);
|
||||
expect(usageOrderRequests.some((request) => request.limit === "10000")).toBe(true);
|
||||
@@ -2667,7 +2669,10 @@ test.describe("Invoicing period tab", () => {
|
||||
|
||||
await page.goto("/superuser/invoices?activeTab=period");
|
||||
await expect(page).toHaveURL(/activeTab=period/);
|
||||
await expect.poll(() => requestCount).toBeGreaterThan(0);
|
||||
await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible({
|
||||
timeout: periodRouteReadyTimeout,
|
||||
});
|
||||
await expect.poll(() => requestCount, { timeout: periodRouteReadyTimeout }).toBeGreaterThan(0);
|
||||
|
||||
await page.getByTestId("invoicing-period-view-selector-all").click();
|
||||
await expect(page.getByTestId("invoicing-period-customer-queue-6001")).toBeVisible();
|
||||
|
||||
@@ -213,6 +213,8 @@ const employeesPayload = [
|
||||
customer_number: 900000501,
|
||||
display_name: "Casey Clerk",
|
||||
email: "casey@example.com",
|
||||
phone_country_code: 45,
|
||||
phone: 12345678,
|
||||
active: true,
|
||||
role: { key: "cashier", label: "Cashier", description: "Can sell." },
|
||||
departments: [{ id: 1, name: "Assigned Depot" }],
|
||||
@@ -228,10 +230,17 @@ async function seedLimitedBackofficeSession(page, token = "limited-backoffice-to
|
||||
await seedAuthenticatedState(page, token);
|
||||
}
|
||||
|
||||
async function mockLimitedBackofficeApi(page, authSessionData = sessionData) {
|
||||
const cloneJson = <T>(value: T): T => JSON.parse(JSON.stringify(value));
|
||||
|
||||
async function mockLimitedBackofficeApi(page, authSessionData = sessionData, options: any = {}) {
|
||||
const calls: string[] = [];
|
||||
const forbiddenCalls: string[] = [];
|
||||
const priceUpdateCalls: unknown[] = [];
|
||||
const employeeCreateCalls: unknown[] = [];
|
||||
const employeeUpdateCalls: unknown[] = [];
|
||||
const currentEmployees = {
|
||||
value: cloneJson(options.employeesPayload ?? employeesPayload),
|
||||
};
|
||||
|
||||
await page.route(API_HOST, async (route) => {
|
||||
const request = route.request();
|
||||
@@ -298,7 +307,56 @@ async function mockLimitedBackofficeApi(page, authSessionData = sessionData) {
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/limited-backoffice/employees") && method === "GET") {
|
||||
await route.fulfill(json({ data: employeesPayload }));
|
||||
await route.fulfill(json({ data: currentEmployees.value }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/limited-backoffice/employees") && method === "POST") {
|
||||
const body = request.postDataJSON?.() || {};
|
||||
employeeCreateCalls.push(body);
|
||||
const role = rolesPayload.find((item) => item.key === body.role_key) || rolesPayload[0];
|
||||
const created = {
|
||||
id: 900 + currentEmployees.value.length + 1,
|
||||
customer_number: 900001000 + currentEmployees.value.length + 1,
|
||||
display_name: body.display_name,
|
||||
email: body.email,
|
||||
phone_country_code: body.phone_country_code ?? null,
|
||||
phone: body.phone ?? null,
|
||||
active: true,
|
||||
role,
|
||||
departments: assignedDepartments.filter((department) => body.department_ids?.includes(department.id)),
|
||||
created_at: "2026-01-01 00:00:00",
|
||||
updated_at: "2026-01-01 00:00:00",
|
||||
};
|
||||
currentEmployees.value.unshift(created);
|
||||
await route.fulfill(json({ data: created }));
|
||||
return;
|
||||
}
|
||||
|
||||
const employeeMatch = pathname.match(/\/limited-backoffice\/employees\/(\d+)$/);
|
||||
if (employeeMatch && method === "PUT") {
|
||||
const body = request.postDataJSON?.() || {};
|
||||
employeeUpdateCalls.push(body);
|
||||
const employeeId = Number(employeeMatch[1]);
|
||||
const target = currentEmployees.value.find((employee) => Number(employee.id) === employeeId);
|
||||
if (!target) {
|
||||
await route.fulfill(json({ message: "Employee not found" }, 404));
|
||||
return;
|
||||
}
|
||||
|
||||
const role = rolesPayload.find((item) => item.key === body.role_key) || target.role;
|
||||
Object.assign(target, {
|
||||
display_name: body.display_name ?? target.display_name,
|
||||
email: body.email ?? target.email,
|
||||
phone_country_code: body.phone_country_code ?? null,
|
||||
phone: body.phone ?? null,
|
||||
role,
|
||||
departments: Array.isArray(body.department_ids)
|
||||
? assignedDepartments.filter((department) => body.department_ids.includes(department.id))
|
||||
: target.departments,
|
||||
updated_at: "2026-01-01 01:00:00",
|
||||
});
|
||||
await route.fulfill(json({ data: target }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -309,6 +367,8 @@ async function mockLimitedBackofficeApi(page, authSessionData = sessionData) {
|
||||
calls,
|
||||
forbiddenCalls,
|
||||
priceUpdateCalls,
|
||||
employeeCreateCalls,
|
||||
employeeUpdateCalls,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -378,6 +438,7 @@ test.describe("Limited backoffice", () => {
|
||||
|
||||
await expect(page.getByTestId("limited-employees-title")).toBeVisible();
|
||||
await expect(page.getByTestId("limited-employee-row-501")).toContainText("Casey Clerk");
|
||||
await expect(page.getByTestId("limited-employee-phone-501")).toHaveText("+45 12345678");
|
||||
await expect(page.locator("#limited-employee-role option")).toHaveCount(5);
|
||||
await expect(page.getByTestId("limited-employee-role")).not.toContainText("Superuser");
|
||||
await expect(page.locator("body")).not.toContainText("department_access_1");
|
||||
@@ -436,6 +497,93 @@ test.describe("Limited backoffice", () => {
|
||||
await expect(page.getByTestId("limited-role-permissions-modal")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("validates employee fields and submits optional phone details", async ({ page }) => {
|
||||
await seedLimitedBackofficeSession(page);
|
||||
const api = await mockLimitedBackofficeApi(page);
|
||||
|
||||
await page.goto("/backoffice/employees");
|
||||
|
||||
await expect(page.getByTestId("limited-employees-title")).toBeVisible();
|
||||
await expect(page.getByTestId("limited-employee-save")).toBeDisabled();
|
||||
await expect(page.getByTestId("limited-employee-save")).toHaveClass(/is-fullwidth/);
|
||||
await expect(page.getByTestId("limited-employee-departments").locator(".switch")).toHaveCount(1);
|
||||
await expect(page.getByTestId("limited-employee-phone-country-code")).toContainText("+45");
|
||||
|
||||
await page.getByTestId("limited-employee-name").fill("No Phone Worker");
|
||||
await page.getByTestId("limited-employee-email").fill("no-phone@example.com");
|
||||
await page.getByTestId("limited-employee-password").fill("Secret123!");
|
||||
await expect(page.getByTestId("limited-employee-save")).toBeDisabled();
|
||||
|
||||
await page.getByTestId("limited-employee-department-1").click();
|
||||
await expect(page.getByTestId("limited-employee-save")).toBeEnabled();
|
||||
await page.getByTestId("limited-employee-save").click();
|
||||
|
||||
await expect.poll(() => api.employeeCreateCalls.length).toBe(1);
|
||||
expect(api.employeeCreateCalls[0]).toEqual({
|
||||
display_name: "No Phone Worker",
|
||||
email: "no-phone@example.com",
|
||||
phone_country_code: null,
|
||||
phone: null,
|
||||
password: "Secret123!",
|
||||
role_key: "viewer",
|
||||
department_ids: [1],
|
||||
});
|
||||
await expect(page.getByTestId("limited-employee-row-902")).toContainText("No Phone Worker");
|
||||
|
||||
await page.getByTestId("limited-employee-name").fill("Phone Worker");
|
||||
await page.getByTestId("limited-employee-email").fill("phone@example.com");
|
||||
await page.getByTestId("limited-employee-phone-country-code").selectOption("358");
|
||||
await page.getByTestId("limited-employee-phone").fill("87654321");
|
||||
await page.getByTestId("limited-employee-password").fill("Secret123!");
|
||||
await page.getByTestId("limited-employee-department-1").click();
|
||||
await expect(page.getByTestId("limited-employee-save")).toBeEnabled();
|
||||
await page.getByTestId("limited-employee-save").click();
|
||||
|
||||
await expect.poll(() => api.employeeCreateCalls.length).toBe(2);
|
||||
expect(api.employeeCreateCalls[1]).toEqual({
|
||||
display_name: "Phone Worker",
|
||||
email: "phone@example.com",
|
||||
phone_country_code: 358,
|
||||
phone: 87654321,
|
||||
password: "Secret123!",
|
||||
role_key: "viewer",
|
||||
department_ids: [1],
|
||||
});
|
||||
await expect(page.getByTestId("limited-employee-row-903")).toContainText("Phone Worker");
|
||||
await expect(page.getByTestId("limited-employee-phone-903")).toHaveText("+358 87654321");
|
||||
});
|
||||
|
||||
test("edits employee contact details without requiring a new password", async ({ page }) => {
|
||||
await seedLimitedBackofficeSession(page);
|
||||
const api = await mockLimitedBackofficeApi(page);
|
||||
|
||||
await page.goto("/backoffice/employees");
|
||||
|
||||
await expect(page.getByTestId("limited-employee-row-501")).toBeVisible();
|
||||
await page.getByTestId("limited-employee-edit-501").click();
|
||||
|
||||
await expect(page.getByTestId("limited-employee-password")).toHaveValue("");
|
||||
await expect(page.getByTestId("limited-employee-save")).toBeEnabled();
|
||||
await page.getByTestId("limited-employee-name").fill("Casey Lead");
|
||||
await page.getByTestId("limited-employee-email").fill("casey.lead@example.com");
|
||||
await page.getByTestId("limited-employee-phone-country-code").selectOption("358");
|
||||
await page.getByTestId("limited-employee-phone").fill("87654321");
|
||||
await page.getByTestId("limited-employee-save").click();
|
||||
|
||||
await expect.poll(() => api.employeeUpdateCalls.length).toBe(1);
|
||||
expect(api.employeeUpdateCalls[0]).toEqual({
|
||||
display_name: "Casey Lead",
|
||||
email: "casey.lead@example.com",
|
||||
phone_country_code: 358,
|
||||
phone: 87654321,
|
||||
role_key: "cashier",
|
||||
department_ids: [1],
|
||||
});
|
||||
expect(api.employeeUpdateCalls[0]).not.toHaveProperty("password");
|
||||
await expect(page.getByTestId("limited-employee-row-501")).toContainText("Casey Lead");
|
||||
await expect(page.getByTestId("limited-employee-phone-501")).toHaveText("+358 87654321");
|
||||
});
|
||||
|
||||
test("does not render data for a department outside the manager scope", async ({ page }, testInfo) => {
|
||||
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
||||
|
||||
|
||||
@@ -124,6 +124,39 @@ async function seedSavedProgress(page, overrides = {}) {
|
||||
await page.addInitScript((savedProgress) => {
|
||||
window.localStorage.setItem("mywash_progress_v6", JSON.stringify(savedProgress));
|
||||
}, payload);
|
||||
|
||||
try {
|
||||
await page.evaluate((savedProgress) => {
|
||||
window.localStorage.setItem("mywash_progress_v6", JSON.stringify(savedProgress));
|
||||
}, payload);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function expectFinishingOrCompletedWash(page) {
|
||||
const finishing = page.getByTestId("self-serve-finishing-wash");
|
||||
const completed = page.getByTestId("self-serve-completed-step");
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
if (await finishing.isVisible().catch(() => false)) {
|
||||
return "finishing";
|
||||
}
|
||||
if (await completed.isVisible().catch(() => false)) {
|
||||
return "completed";
|
||||
}
|
||||
return "pending";
|
||||
},
|
||||
{
|
||||
message: "expected the wash to show the finishing state or complete",
|
||||
timeout: 15_000,
|
||||
}
|
||||
)
|
||||
.toMatch(/^(finishing|completed)$/);
|
||||
|
||||
if (await finishing.isVisible().catch(() => false)) {
|
||||
await expect(finishing).toContainText("Afslutter vask, porten åbnes automatisk");
|
||||
}
|
||||
}
|
||||
|
||||
function captureSelfServeGatewayRequests(page) {
|
||||
@@ -276,9 +309,7 @@ test.describe("Self-serve wash", () => {
|
||||
const stopCommandRequestPromise = waitForLaneCommandRequest(page, "STOP");
|
||||
const exitGateCommandRequestPromise = waitForLaneCommandRequest(page, "OPEN_PROPERTY_EXIT_GATE");
|
||||
await page.getByTestId("self-serve-nav-complete").click();
|
||||
await expect(page.getByTestId("self-serve-finishing-wash")).toContainText(
|
||||
"Afslutter vask, porten åbnes automatisk"
|
||||
);
|
||||
await expectFinishingOrCompletedWash(page);
|
||||
const stopCommandRequest = await stopCommandRequestPromise;
|
||||
const exitGateCommandRequest = await exitGateCommandRequestPromise;
|
||||
expect(stopCommandRequest.postDataJSON?.()).toMatchObject({
|
||||
@@ -660,11 +691,18 @@ test.describe("Self-serve wash", () => {
|
||||
});
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.getByTestId("self-serve-questions-step")).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled();
|
||||
await page.getByTestId("self-serve-question-21-yes").click();
|
||||
await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled({ timeout: 10_000 });
|
||||
await page.getByTestId("self-serve-nav-confirm").click();
|
||||
|
||||
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByRole("radio", { name: /Maskine/i })).toBeDisabled();
|
||||
await expect(page.getByTestId("self-serve-lane-option-7")).toContainText("Tilgængelig");
|
||||
await expect(page.getByTestId("self-serve-lane-option-8")).toContainText("Vaskebanen er ikke tilgængelig");
|
||||
await expect(page.getByRole("radio", { name: /Maskine/i })).toBeEnabled();
|
||||
await expect(page.getByTestId("self-serve-machine-unavailable-guidance")).toHaveCount(0);
|
||||
await expect(page.locator("body")).not.toContainText(removedMachineUnavailableGuidanceText);
|
||||
await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled();
|
||||
|
||||
await page.getByTestId("self-serve-lane-option-7").click();
|
||||
await page.getByTestId("self-serve-wash-type-manual").click();
|
||||
@@ -1147,9 +1185,7 @@ test.describe("Self-serve wash", () => {
|
||||
const stopCommandRequestPromise = waitForLaneCommandRequest(page, "STOP");
|
||||
const exitGateCommandRequestPromise = waitForLaneCommandRequest(page, "OPEN_PROPERTY_EXIT_GATE");
|
||||
await page.getByTestId("self-serve-nav-complete").click();
|
||||
await expect(page.getByTestId("self-serve-finishing-wash")).toContainText(
|
||||
"Afslutter vask, porten åbnes automatisk"
|
||||
);
|
||||
await expectFinishingOrCompletedWash(page);
|
||||
const stopCommandRequest = await stopCommandRequestPromise;
|
||||
const exitGateCommandRequest = await exitGateCommandRequestPromise;
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
||||
import { isDesktopProject } from "./support/projects";
|
||||
|
||||
const json = (body: unknown, status = 200) => ({
|
||||
status,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const now = "2026-07-06 10:00:00";
|
||||
|
||||
const buildProduct = (product: Record<string, unknown>) => ({
|
||||
description: "E2E product",
|
||||
subscription_allowed: true,
|
||||
category: 1,
|
||||
piktogram: "truck",
|
||||
economic_product_id: 0,
|
||||
apply_category_discount: false,
|
||||
requires_note: false,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
addons: [],
|
||||
is_wash: true,
|
||||
display_in_booking_form: true,
|
||||
order_priority: 1,
|
||||
...product,
|
||||
});
|
||||
|
||||
test.describe("Superuser department custom-only pricing", () => {
|
||||
test("toggles no-fallback pricing and shows 999999 for missing department prices", async ({ page }, testInfo) => {
|
||||
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
||||
|
||||
let customPricingOnly = false;
|
||||
let receivedToggleBody: Record<string, unknown> | null = null;
|
||||
|
||||
const products = [
|
||||
buildProduct({ id: 10, name: "Fallback Wash", price: 12345 }),
|
||||
buildProduct({ id: 11, name: "Explicit Wash", price: 98765, order_priority: 2 }),
|
||||
];
|
||||
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await seedAuthenticatedState(page, "superuser-department-pricing-token");
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: [
|
||||
"superuser",
|
||||
"user",
|
||||
"list_products",
|
||||
"list_departments",
|
||||
"edit_department",
|
||||
"superuser_fetch_department",
|
||||
"superuser_fetch_department_prices",
|
||||
"superuser_set_department_prices",
|
||||
],
|
||||
sessionData: {
|
||||
group_id: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await page.route("**/api/products**", async (route) => {
|
||||
await route.fulfill(json({ data: products }));
|
||||
});
|
||||
|
||||
await page.route("**/api/superuser/department/prices**", async (route) => {
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: [{ id: 100, department_id: 42, product_id: 11, price: 2222 }],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
await page.route(/\/api\/superuser\/department(?:\?.*)?$/i, async (route) => {
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: {
|
||||
id: 42,
|
||||
name: "Custom Pricing Department",
|
||||
description: "E2E department",
|
||||
custom_pricing_only: customPricingOnly,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
await page.route("**/api/departments", async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method() !== "PUT") {
|
||||
await route.fallback();
|
||||
return;
|
||||
}
|
||||
|
||||
receivedToggleBody = request.postDataJSON() as Record<string, unknown>;
|
||||
customPricingOnly = Boolean(receivedToggleBody.custom_pricing_only);
|
||||
await route.fulfill(json({ data: { message: "Department updated successfully" } }));
|
||||
});
|
||||
|
||||
await page.goto("/superuser/departments/42/pricing", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.getByTestId("department-custom-pricing-settings")).toBeVisible();
|
||||
await expect(page.getByTestId("department-price-cell-10")).toHaveText(/-/);
|
||||
await expect(page.getByTestId("department-price-cell-11")).toHaveText(/2222/);
|
||||
|
||||
await page.locator('label[for="department-custom-pricing-only"]').click();
|
||||
|
||||
await expect(page.getByTestId("department-custom-pricing-only-toggle")).toBeChecked();
|
||||
await expect(page.getByTestId("department-price-cell-10")).toHaveText(/999999/);
|
||||
await expect(page.getByTestId("department-price-cell-11")).toHaveText(/2222/);
|
||||
expect(receivedToggleBody).toMatchObject({
|
||||
id: "42",
|
||||
custom_pricing_only: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { endOfLocalDate, formatLocalDateOnly, parseLocalDateOnly, startOfLocalDate } from "@/services/dateOnly.js";
|
||||
|
||||
describe("date-only helpers", () => {
|
||||
it("formats Date objects from their local calendar date", () => {
|
||||
expect(formatLocalDateOnly(new Date(2026, 2, 31, 0, 0, 0, 0))).toBe("2026-03-31");
|
||||
});
|
||||
|
||||
it("parses date input values as local calendar dates", () => {
|
||||
const parsed = parseLocalDateOnly("2026-03-31");
|
||||
|
||||
expect(parsed.getFullYear()).toBe(2026);
|
||||
expect(parsed.getMonth()).toBe(2);
|
||||
expect(parsed.getDate()).toBe(31);
|
||||
expect(formatLocalDateOnly(parsed)).toBe("2026-03-31");
|
||||
});
|
||||
|
||||
it("keeps date-only prefixes from API timestamps without timezone reinterpretation", () => {
|
||||
expect(formatLocalDateOnly("2026-03-31T23:30:00.000Z")).toBe("2026-03-31");
|
||||
});
|
||||
|
||||
it("rejects impossible date input values", () => {
|
||||
expect(Number.isNaN(parseLocalDateOnly("2026-02-31").getTime())).toBe(true);
|
||||
});
|
||||
|
||||
it("builds local day boundaries from the selected date", () => {
|
||||
expect(startOfLocalDate("2026-03-31").getHours()).toBe(0);
|
||||
expect(startOfLocalDate("2026-03-31").getMinutes()).toBe(0);
|
||||
expect(endOfLocalDate("2026-03-31").getHours()).toBe(23);
|
||||
expect(endOfLocalDate("2026-03-31").getMinutes()).toBe(59);
|
||||
expect(formatLocalDateOnly(endOfLocalDate("2026-03-31"))).toBe("2026-03-31");
|
||||
});
|
||||
});
|
||||
@@ -122,6 +122,32 @@ describe("DatePeriodSelector mobile layout", () => {
|
||||
expect(formatLocalDate(onSelectionChange.mock.calls[0][0])).toBe("2026-03-01");
|
||||
expect(formatLocalDate(onSelectionChange.mock.calls[0][1])).toBe("2026-03-05");
|
||||
});
|
||||
|
||||
it("emits the exact clicked date for start and end inputs", async () => {
|
||||
sharedState.width.value = 1024;
|
||||
const onSelectionChange = vi.fn();
|
||||
const wrapper = mount(DatePeriodSelector, {
|
||||
props: {
|
||||
selection: {
|
||||
startDate: new Date(2026, 2, 1, 0, 0, 0, 0),
|
||||
endDate: new Date(2026, 2, 3, 23, 59, 59, 999),
|
||||
},
|
||||
onSelectionChange,
|
||||
visibility: {
|
||||
showUpdateButton: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.get("[data-testid='date-period-start']").setValue("2026-03-31");
|
||||
await wrapper.get("[data-testid='date-period-end']").setValue("2026-04-02");
|
||||
|
||||
const emittedSelection = wrapper.emitted("update:selection")?.at(-1)?.[0];
|
||||
expect(formatLocalDate(emittedSelection.startDate)).toBe("2026-03-31");
|
||||
expect(formatLocalDate(emittedSelection.endDate)).toBe("2026-04-02");
|
||||
expect(formatLocalDate(onSelectionChange.mock.calls.at(-1)[0])).toBe("2026-03-31");
|
||||
expect(formatLocalDate(onSelectionChange.mock.calls.at(-1)[1])).toBe("2026-04-02");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DatePeriodSelector month warning", () => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { defineComponent, nextTick } from "vue";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { formatLocalDateOnly } from "@/services/dateOnly.js";
|
||||
|
||||
const selectDateMock = vi.hoisted(() => vi.fn());
|
||||
const sharedDateState = vi.hoisted(() => ({
|
||||
@@ -39,10 +40,10 @@ const DatePeriodSelectorStub = defineComponent({
|
||||
emits: ["update:selection"],
|
||||
template: `
|
||||
<div>
|
||||
<button class="trigger-on-selection-change" @click="onSelectionChange(new Date('2026-03-24T00:00:00.000Z'), new Date('2026-03-29T00:00:00.000Z'))">
|
||||
<button class="trigger-on-selection-change" @click="onSelectionChange(new Date(2026, 2, 24), new Date(2026, 2, 29))">
|
||||
onSelectionChange
|
||||
</button>
|
||||
<button class="trigger-update-selection" @click="$emit('update:selection', { startDate: new Date('2026-04-01T00:00:00.000Z'), endDate: new Date('2026-04-30T00:00:00.000Z') })">
|
||||
<button class="trigger-update-selection" @click="$emit('update:selection', { startDate: new Date(2026, 3, 1), endDate: new Date(2026, 3, 30) })">
|
||||
update:selection
|
||||
</button>
|
||||
</div>
|
||||
@@ -105,7 +106,7 @@ describe("DepartmentDashboardOverviewNavigation", () => {
|
||||
});
|
||||
|
||||
const selector = wrapper.findComponent(DatePeriodSelectorStub);
|
||||
expect(selector.props("selection").startDate.toISOString().split("T")[0]).toBe("2026-05-05");
|
||||
expect(selector.props("selection").endDate.toISOString().split("T")[0]).toBe("2026-05-11");
|
||||
expect(formatLocalDateOnly(selector.props("selection").startDate)).toBe("2026-05-05");
|
||||
expect(formatLocalDateOnly(selector.props("selection").endDate)).toBe("2026-05-11");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -755,6 +755,42 @@ describe("Invoicing period queue-driven refresh", () => {
|
||||
expect(selfWashCalls()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("loads Selvvask selector counts with the full selected-period limit", async () => {
|
||||
requestMock.mockImplementation((url) => {
|
||||
if (url === "/superuser/invoicing/period") {
|
||||
return Promise.resolve(emptyPeriodResponse());
|
||||
}
|
||||
|
||||
return Promise.resolve(
|
||||
selfWashResponse(
|
||||
[
|
||||
{ id: 8101, linked_order_id: 7001 },
|
||||
{
|
||||
id: 8102,
|
||||
linked_order_id: null,
|
||||
automation: {
|
||||
status: "accepted",
|
||||
action: "attach_order",
|
||||
},
|
||||
},
|
||||
{ id: 8103, linked_order_id: null },
|
||||
],
|
||||
3
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const wrapper = mountRight();
|
||||
await flushAll();
|
||||
|
||||
expect(wrapper.get("[data-testid='invoicing-period-view-selector-self_wash']").text()).toContain("Selvvask (2/3)");
|
||||
expect(selfWashCalls()[0][2]).toMatchObject({
|
||||
limit: 10000,
|
||||
filters: "StartTime-date_from:2026-04-01,StartTime-date_to:2026-04-30",
|
||||
order: "StartTime:desc",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores stale period responses that resolve after a newer search request", async () => {
|
||||
const deferred = [];
|
||||
requestMock.mockImplementation((url) => {
|
||||
|
||||
@@ -579,6 +579,7 @@ describe("MyWashStart", () => {
|
||||
|
||||
afterEach(() => {
|
||||
consoleWarnSpy?.mockRestore();
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// @vitest-environment jsdom
|
||||
import { defineComponent } from "vue";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("vue-i18n", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (value) => value,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
SessionUser: {
|
||||
objects: {
|
||||
global: {
|
||||
language: {
|
||||
date_from: "From",
|
||||
date_to: "To",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { formatLocalDateOnly } from "@/services/dateOnly.js";
|
||||
import PaginationDisplayTemplateDate from "@/components/displays/pagination/templates/PaginationDisplayTemplateDate.vue";
|
||||
import PaginationDisplayTemplateDates from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
|
||||
import { dateFunctions } from "@/components/displays/pagination/PaginationDisplayDates.vue";
|
||||
|
||||
const PaginationDisplayItemColumnStub = defineComponent({
|
||||
name: "PaginationDisplayItemColumn",
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
template: "<div><slot name='control' /></div>",
|
||||
});
|
||||
|
||||
describe("pagination date selection", () => {
|
||||
it("renders Date model values as the same local date", () => {
|
||||
const wrapper = mount(PaginationDisplayTemplateDate, {
|
||||
props: {
|
||||
label: "Date",
|
||||
modelValue: new Date(2026, 2, 31, 0, 0, 0, 0),
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
PaginationDisplayItemColumn: PaginationDisplayItemColumnStub,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.get("input[type='date']").element.value).toBe("2026-03-31");
|
||||
});
|
||||
|
||||
it("emits the clicked input date as the same local date", async () => {
|
||||
const wrapper = mount(PaginationDisplayTemplateDate, {
|
||||
props: {
|
||||
label: "Date",
|
||||
modelValue: new Date(2026, 2, 1, 0, 0, 0, 0),
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
PaginationDisplayItemColumn: PaginationDisplayItemColumnStub,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.get("input[type='date']").setValue("2026-03-31");
|
||||
|
||||
const emittedDate = wrapper.emitted("update:date")?.at(-1)?.[0];
|
||||
expect(formatLocalDateOnly(emittedDate)).toBe("2026-03-31");
|
||||
});
|
||||
|
||||
it("uses the real first day of the month for month presets", () => {
|
||||
const firstDay = dateFunctions.datePresetFunctions.month.firstDayOfMonth(new Date(2026, 4, 15, 12, 0, 0, 0));
|
||||
|
||||
expect(formatLocalDateOnly(firstDay)).toBe("2026-05-01");
|
||||
expect(firstDay.getHours()).toBe(0);
|
||||
expect(firstDay.getMinutes()).toBe(0);
|
||||
});
|
||||
|
||||
it("emits both start and end updates from the paired date selector", async () => {
|
||||
const wrapper = mount(PaginationDisplayTemplateDates, {
|
||||
props: {
|
||||
startDate: new Date(2026, 2, 1, 0, 0, 0, 0),
|
||||
endDate: new Date(2026, 2, 31, 0, 0, 0, 0),
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
PaginationDisplayItemColumn: PaginationDisplayItemColumnStub,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const inputs = wrapper.findAll("input[type='date']");
|
||||
await inputs[0].setValue("2026-04-01");
|
||||
await inputs[1].setValue("2026-04-30");
|
||||
|
||||
expect(formatLocalDateOnly(wrapper.emitted("update:startDate")?.at(-1)?.[0])).toBe("2026-04-01");
|
||||
expect(formatLocalDateOnly(wrapper.emitted("update:endDate")?.at(-1)?.[0])).toBe("2026-04-30");
|
||||
});
|
||||
});
|
||||
@@ -40,6 +40,16 @@ describe("Playwright full E2E workflow grouping", () => {
|
||||
expect(source).toContain("scripts/ci/runner-diagnostics.sh");
|
||||
});
|
||||
|
||||
it("keeps PR E2E runner pressure bounded and diagnosable", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?max-parallel: 4/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?PLAYWRIGHT_WORKERS: 1/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?PLAYWRIGHT_VIDEO_MODE: on-first-retry/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?--env PLAYWRIGHT_WORKERS="\$PLAYWRIGHT_WORKERS"/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?--env PLAYWRIGHT_VIDEO_MODE="\$PLAYWRIGHT_VIDEO_MODE"/u);
|
||||
});
|
||||
|
||||
it("allows CI to reduce Playwright video artifact pressure", () => {
|
||||
const source = readFileSync(join(root, "playwright.config.ts"), "utf8");
|
||||
|
||||
|
||||
@@ -15,6 +15,21 @@ describe("Playwright PR mapping", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("maps superuser department pricing changes to custom-only pricing coverage", () => {
|
||||
expect(specsFor("src/views/dashboards/superUserDashboard/department/DepartmentPricing.vue")).toContain(
|
||||
"tests/e2e/superuser-department-pricing-custom-only.spec.ts"
|
||||
);
|
||||
expect(
|
||||
specsFor("src/views/dashboards/superUserDashboard/department/SuperUserSelectedDepartmentObject.vue")
|
||||
).toContain("tests/e2e/superuser-department-pricing-custom-only.spec.ts");
|
||||
expect(specsFor("src/components/session/token/SessionUser/Objects/Departments.vue")).toContain(
|
||||
"tests/e2e/superuser-department-pricing-custom-only.spec.ts"
|
||||
);
|
||||
expect(specsFor("src/components/session/token/SessionUser/Objects/Departments.vue")).not.toContain(
|
||||
"tests/e2e/userAuth.spec.ts"
|
||||
);
|
||||
});
|
||||
|
||||
it("maps department notification table changes to the admin notification E2E coverage", () => {
|
||||
expect(
|
||||
specsFor("src/components/displays/department/notifications/departmentNotificationsPhoneTable.vue")
|
||||
@@ -33,9 +48,15 @@ describe("Playwright PR mapping", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps full-slice ownership metadata out of broad PR smoke fallback", () => {
|
||||
expect(triggersFallback("scripts/run-playwright-pr.mjs")).toBe(true);
|
||||
it("keeps PR runner and full-slice metadata edits out of broad PR smoke fallback", () => {
|
||||
expect(triggersFallback("scripts/run-playwright-pr.mjs")).toBe(false);
|
||||
expect(triggersFallback("scripts/run-playwright-ci-parallel.mjs")).toBe(true);
|
||||
expect(triggersFallback("scripts/run-playwright-full-slice.mjs")).toBe(false);
|
||||
});
|
||||
|
||||
it("maps limited backoffice changes to the limited backoffice E2E coverage", () => {
|
||||
expect(specsFor("src/views/backoffice/LimitedBackofficeEmployees.vue")).toContain(
|
||||
"tests/e2e/limited-backoffice.spec.ts"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,6 +95,7 @@ afterEach(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
|
||||
@@ -100,6 +100,10 @@ const xlvaskUsagePaginationSource = readFileSync(
|
||||
join(root, "src/components/displays/pagination/models/DepartmentPos/XLVaskUsagePagination.vue"),
|
||||
"utf8"
|
||||
);
|
||||
const xlvaskUsagePeriodConstantsSource = readFileSync(
|
||||
join(root, "src/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js"),
|
||||
"utf8"
|
||||
);
|
||||
const xlvaskUsageOrdersTableSource = readFileSync(
|
||||
join(root, "src/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue"),
|
||||
"utf8"
|
||||
@@ -367,6 +371,8 @@ describe("Invoice orders pagination contract", () => {
|
||||
expect(invoiceOrdersPaginationSource).toContain(
|
||||
"const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => {"
|
||||
);
|
||||
expect(invoiceOrdersPaginationSource).toContain("formatLocalDateOnly(newSelectionStartDate)");
|
||||
expect(invoiceOrdersPaginationSource).toContain("formatLocalDateOnly(newSelectionToDate)");
|
||||
expect(invoiceOrdersPaginationSource).toContain("date_from.value = formattedStartDate;");
|
||||
expect(invoiceOrdersPaginationSource).toContain("date_to.value = formattedEndDate;");
|
||||
expect(invoiceOrdersPaginationSource).toContain('setFilter("created_at-date_from", formattedStartDate, true);');
|
||||
@@ -477,7 +483,7 @@ describe("Periode tab contract", () => {
|
||||
expect(periodViewSelfWashSource).toContain(':load-all-at-once="true"');
|
||||
expect(xlvaskUsagePaginationSource).toContain("inheritPeriodFilters");
|
||||
expect(xlvaskUsagePaginationSource).toContain("loadAllAtOnce");
|
||||
expect(xlvaskUsagePaginationSource).toContain("const SELFWASH_PERIOD_ALL_LIMIT = 10000");
|
||||
expect(xlvaskUsagePeriodConstantsSource).toContain("export const SELFWASH_PERIOD_ALL_LIMIT = 10000");
|
||||
expect(xlvaskUsagePaginationSource).toContain("setMetaItemsPerPage(SELFWASH_PERIOD_ALL_LIMIT, false)");
|
||||
expect(xlvaskUsagePaginationSource).toContain(
|
||||
"const shouldShowLocalFilters = computed(() => !props.inheritPeriodFilters)"
|
||||
@@ -494,6 +500,7 @@ describe("Periode tab contract", () => {
|
||||
expect(periodRightSource).toContain("getCachedSelfWashCounts");
|
||||
expect(periodRightSource).toContain("setCachedSelfWashCounts");
|
||||
expect(periodRightSource).toContain("'/modules/xlvask/services/usage/orders'");
|
||||
expect(periodRightSource).toContain("limit: SELFWASH_PERIOD_ALL_LIMIT");
|
||||
expect(periodRightSource).toContain("StartTime-date_from:${dates.computed.formattedStartDate.value}");
|
||||
expect(periodRightSource).toContain("StartTime-date_to:${dates.computed.formattedEndDate.value}");
|
||||
expect(periodRightSource).toContain("if (type.name === 'self_wash')");
|
||||
@@ -502,6 +509,15 @@ describe("Periode tab contract", () => {
|
||||
expect(periodRightSource).not.toContain("showProgress: false");
|
||||
});
|
||||
|
||||
it("keeps Selvvask usage pagination scoped to the period view", () => {
|
||||
expect(xlvaskUsagePaginationSource).toContain("usePaginatedList");
|
||||
expect(xlvaskUsagePaginationSource).toContain("provide(PaginatedListKey, paginatedList)");
|
||||
expect(xlvaskUsageOrdersTableSource).toContain("usePaginatedListInstance");
|
||||
expect(xlvaskUsageOrdersTableSource).toContain(
|
||||
"const { orderBy, orderDirection, setOrder, loadList } = usePaginatedListInstance();"
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps Selvvask usage entries wrapped inside the period content area", () => {
|
||||
expect(xlvaskUsageOrdersTableSource).toContain("xlvask-usage-card-primary");
|
||||
expect(xlvaskUsageOrdersTableSource).toContain("xlvask-usage-card-status");
|
||||
|
||||
Reference in New Issue
Block a user