Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7180bbd6d4 | ||
|
|
96ccc01d08 | ||
|
|
2108dd6c94 | ||
|
|
ca2003f2d6 | ||
|
|
acbbac588f | ||
|
|
f75ff97042 | ||
|
|
4aa7af9716 | ||
|
|
d2682da3cc | ||
|
|
aa1d2ab623 | ||
|
|
929333d264 | ||
|
|
d2cbf823d8 | ||
|
|
ba580e43e6 | ||
|
|
3c49cec213 | ||
|
|
c237fce38d | ||
|
|
8acecc61cd | ||
|
|
63149b7597 | ||
|
|
a331eeadcb | ||
|
|
33e109b131 | ||
|
|
68b4328c72 | ||
|
|
dd8a0a1952 | ||
|
|
b58c1eb9ec |
+240
-12
@@ -6,6 +6,39 @@ on:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: "What to run for a manual dispatch."
|
||||
required: true
|
||||
type: choice
|
||||
default: full
|
||||
options:
|
||||
- full
|
||||
- targeted
|
||||
- targeted-then-full
|
||||
target_specs:
|
||||
description: "Comma- or newline-separated Playwright spec paths under tests/e2e."
|
||||
required: false
|
||||
type: string
|
||||
default: "tests/e2e/superuser-department-overview.spec.js"
|
||||
target_projects:
|
||||
description: "JSON array of Playwright projects for targeted mode."
|
||||
required: false
|
||||
type: string
|
||||
default: '["chromium-desktop","chromium-mobile","chromium-tablet","webkit-mobile","webkit-desktop"]'
|
||||
target_grep:
|
||||
description: "Optional Playwright grep pattern for targeted mode."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
runner:
|
||||
description: "Runner pool for this manually dispatched test run"
|
||||
required: false
|
||||
default: "self-hosted"
|
||||
type: choice
|
||||
options:
|
||||
- self-hosted
|
||||
- github-hosted
|
||||
schedule:
|
||||
- cron: "0 2 * * *"
|
||||
|
||||
@@ -13,16 +46,22 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: frontend-tests-${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}
|
||||
group: frontend-tests-${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.head_ref || github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Repository variables used as CI runner and credit controls:
|
||||
# - FRONTEND_CI_STANDARD_RUNNER: JSON runs-on value for format/build/unit jobs.
|
||||
# - FRONTEND_CI_E2E_RUNNER: JSON runs-on value for Playwright jobs.
|
||||
# - FRONTEND_CI_PR_E2E_MAX_PARALLEL: numeric Playwright PR job parallelism.
|
||||
# - FRONTEND_CI_FULL_E2E_MAX_PARALLEL: numeric full-suite job parallelism.
|
||||
# GitHub-hosted example: ["ubuntu-22.04"], with PR parallelism 2 and full parallelism 1.
|
||||
jobs:
|
||||
format-tests:
|
||||
# CI runs on the repository's self-hosted runner pool.
|
||||
runs-on: [self-hosted, Linux, X64, pleno, frontend]
|
||||
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Repair self-hosted workspace permissions
|
||||
if: ${{ contains(vars.FRONTEND_CI_STANDARD_RUNNER || 'self-hosted', 'self-hosted') }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||
@@ -55,10 +94,11 @@ jobs:
|
||||
|
||||
build-and-unit:
|
||||
needs: format-tests
|
||||
runs-on: [self-hosted, Linux, X64, pleno, frontend]
|
||||
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Repair self-hosted workspace permissions
|
||||
if: ${{ contains(vars.FRONTEND_CI_STANDARD_RUNNER || 'self-hosted', 'self-hosted') }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||
@@ -97,15 +137,195 @@ jobs:
|
||||
env:
|
||||
VITEST_BATCH_SIZE: 5
|
||||
|
||||
e2e-pr:
|
||||
if: github.event_name != 'schedule'
|
||||
e2e-targeted:
|
||||
if: >
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
(inputs.mode == 'targeted' || inputs.mode == 'targeted-then-full')
|
||||
needs: build-and-unit
|
||||
name: E2E-targeted-${{ matrix.project }}
|
||||
# Use GitHub-hosted runners to avoid self-hosted desktop contention and sleep/power events.
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 35
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
project: ${{ fromJSON(inputs.target_projects || '["chromium-desktop"]') }}
|
||||
env:
|
||||
MATRIX_PROJECT: ${{ matrix.project }}
|
||||
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-targeted-${{ matrix.project }}
|
||||
PLAYWRIGHT_REPORTER_MODE: line-html
|
||||
PLAYWRIGHT_WORKERS: 1
|
||||
PLAYWRIGHT_VIDEO_MODE: on-first-retry
|
||||
TARGET_GREP: ${{ inputs.target_grep }}
|
||||
TARGET_SPECS: ${{ inputs.target_specs }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
steps:
|
||||
- name: Normalize workspace permissions
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
|
||||
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
|
||||
if [[ -n "$foreign_entry" ]]; then
|
||||
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
|
||||
rm -rf "$trash" 2>/dev/null || true
|
||||
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
|
||||
mkdir -p "$GITHUB_WORKSPACE"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Run targeted Playwright specs in container
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "$MATRIX_PROJECT" in
|
||||
chromium-mobile) project_offset=1 ;;
|
||||
chromium-desktop) project_offset=2 ;;
|
||||
chromium-tablet) project_offset=3 ;;
|
||||
webkit-mobile) project_offset=31 ;;
|
||||
webkit-desktop) project_offset=32 ;;
|
||||
webkit-tablet) project_offset=33 ;;
|
||||
firefox-mobile) project_offset=61 ;;
|
||||
firefox-desktop) project_offset=62 ;;
|
||||
firefox-tablet) project_offset=63 ;;
|
||||
*) echo "Unsupported Playwright project: $MATRIX_PROJECT" >&2; exit 1 ;;
|
||||
esac
|
||||
port_seed=$((20000 + (RUN_ID % 20000) + project_offset))
|
||||
lock_root="${PLAYWRIGHT_PORT_LOCK_ROOT:-/tmp/pleno-playwright-port-locks}"
|
||||
mkdir -p "$lock_root"
|
||||
chmod 1777 "$lock_root" 2>/dev/null || true
|
||||
find "$lock_root" -mindepth 1 -maxdepth 1 -type d -mmin +360 -exec rmdir {} \; 2>/dev/null || true
|
||||
playwright_port_lock=""
|
||||
playwright_dev_port=""
|
||||
for ((candidate = port_seed; candidate < port_seed + 1000; candidate += 1)); do
|
||||
lock_dir="${lock_root}/${candidate}.lock"
|
||||
if ! mkdir "$lock_dir" 2>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
if ss -H -ltn "sport = :${candidate}" 2>/dev/null | grep -q .; then
|
||||
rmdir "$lock_dir" || true
|
||||
continue
|
||||
fi
|
||||
playwright_port_lock="$lock_dir"
|
||||
playwright_dev_port="$candidate"
|
||||
break
|
||||
done
|
||||
if [[ -z "$playwright_dev_port" ]]; then
|
||||
echo "Unable to find a free Playwright dev-server port." >&2
|
||||
exit 1
|
||||
fi
|
||||
trap 'if [[ -n "${playwright_port_lock:-}" ]]; then rmdir "$playwright_port_lock" 2>/dev/null || true; fi' EXIT
|
||||
if docker info >/dev/null 2>&1; then
|
||||
docker_cmd=(docker)
|
||||
elif sudo -n docker info >/dev/null 2>&1; then
|
||||
docker_cmd=(sudo docker)
|
||||
else
|
||||
echo "Docker is not available to the runner user, and sudo docker is not available." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p output/playwright
|
||||
scripts/ci/runner-diagnostics.sh "before targeted Playwright ${MATRIX_PROJECT}" -- "${docker_cmd[@]}"
|
||||
SYSTEMD_INHIBIT_REASON="Frontend targeted Playwright ${MATRIX_PROJECT}" \
|
||||
scripts/ci/with-systemd-inhibit.sh "${docker_cmd[@]}" run --rm --ipc=host --network host \
|
||||
--volume "$PWD:/source:ro" \
|
||||
--volume "$PWD/output/playwright:/work/output/playwright" \
|
||||
--workdir /work \
|
||||
--env HOME=/tmp \
|
||||
--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_PROJECT="$MATRIX_PROJECT" \
|
||||
--env TARGET_GREP="$TARGET_GREP" \
|
||||
--env TARGET_SPECS="$TARGET_SPECS" \
|
||||
mcr.microsoft.com/playwright:v1.58.2-noble \
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf -
|
||||
git config --global --add safe.directory /work
|
||||
install_dependencies() {
|
||||
local attempt
|
||||
for attempt in 1 2 3; do
|
||||
if npm ci --legacy-peer-deps --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$attempt" == "3" ]]; then
|
||||
return 1
|
||||
fi
|
||||
echo "npm ci failed on attempt ${attempt}; retrying..." >&2
|
||||
sleep 20
|
||||
done
|
||||
}
|
||||
install_dependencies
|
||||
ulimit -n 16384 || true
|
||||
mapfile -t spec_args < <(printf "%s\n" "$TARGET_SPECS" | tr "," "\n" | sed "s/^[[:space:]]*//;s/[[:space:]]*$//;/^$/d")
|
||||
if [[ "${#spec_args[@]}" -eq 0 && -z "${TARGET_GREP:-}" ]]; then
|
||||
echo "Provide at least one spec path or grep pattern." >&2
|
||||
exit 1
|
||||
fi
|
||||
for spec_path in "${spec_args[@]}"; do
|
||||
if [[ "$spec_path" == /* || "$spec_path" == *".."* || "$spec_path" != tests/e2e/* ]]; then
|
||||
echo "Targeted spec must stay under tests/e2e: $spec_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$spec_path" ]]; then
|
||||
echo "Targeted spec does not exist: $spec_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
args=("${spec_args[@]}")
|
||||
if [[ -n "${TARGET_GREP:-}" ]]; then
|
||||
args+=(--grep "$TARGET_GREP")
|
||||
fi
|
||||
args+=(--project="$MATRIX_PROJECT")
|
||||
npx playwright test "${args[@]}"
|
||||
'
|
||||
|
||||
- name: Runner diagnostics after Playwright failure
|
||||
if: failure() || cancelled()
|
||||
continue-on-error: true
|
||||
run: scripts/ci/runner-diagnostics.sh "after targeted Playwright ${{ matrix.project }}"
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: failure() || cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-targeted-${{ matrix.project }}
|
||||
path: |
|
||||
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}-*
|
||||
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
e2e-pr:
|
||||
if: >
|
||||
always() &&
|
||||
github.event_name != 'schedule' &&
|
||||
needs.build-and-unit.result == 'success' &&
|
||||
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted') &&
|
||||
(
|
||||
github.event_name != 'workflow_dispatch' ||
|
||||
inputs.mode == 'full' ||
|
||||
needs.e2e-targeted.result == 'success'
|
||||
)
|
||||
needs: [build-and-unit, e2e-targeted]
|
||||
name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.FRONTEND_CI_E2E_RUNNER || '["self-hosted","Linux","X64","pleno","frontend","docker"]') }}
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
max-parallel: ${{ fromJSON(vars.FRONTEND_CI_PR_E2E_MAX_PARALLEL || '2') }}
|
||||
matrix:
|
||||
suite: [core, changed]
|
||||
project: [chromium-desktop, chromium-mobile]
|
||||
@@ -116,6 +336,7 @@ jobs:
|
||||
PLAYWRIGHT_VIDEO_MODE: on-first-retry
|
||||
steps:
|
||||
- name: Repair self-hosted workspace permissions
|
||||
if: ${{ contains(vars.FRONTEND_CI_E2E_RUNNER || 'self-hosted', 'self-hosted') }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||
@@ -291,15 +512,21 @@ jobs:
|
||||
if: >
|
||||
always() &&
|
||||
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch) &&
|
||||
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted') &&
|
||||
needs.build-and-unit.result == 'success' &&
|
||||
(github.event_name == 'schedule' || needs.e2e-pr.result == 'success')
|
||||
needs: [build-and-unit, e2e-pr]
|
||||
(github.event_name == 'schedule' || needs.e2e-pr.result == 'success') &&
|
||||
(
|
||||
github.event_name != 'workflow_dispatch' ||
|
||||
inputs.mode == 'full' ||
|
||||
needs.e2e-targeted.result == 'success'
|
||||
)
|
||||
needs: [build-and-unit, e2e-pr, e2e-targeted]
|
||||
name: E2E-full-${{ matrix.browser_label }}-${{ matrix.device }}-${{ matrix.role }}
|
||||
runs-on: [self-hosted, Linux, X64, pleno, frontend, docker]
|
||||
runs-on: ${{ fromJSON(vars.FRONTEND_CI_E2E_RUNNER || '["self-hosted","Linux","X64","pleno","frontend","docker"]') }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
max-parallel: ${{ fromJSON(vars.FRONTEND_CI_FULL_E2E_MAX_PARALLEL || '1') }}
|
||||
matrix:
|
||||
browser: [chromium, webkit, firefox]
|
||||
device: [mobile, desktop, tablet]
|
||||
@@ -321,6 +548,7 @@ jobs:
|
||||
PLAYWRIGHT_VIDEO_MODE: off
|
||||
steps:
|
||||
- name: Repair self-hosted workspace permissions
|
||||
if: ${{ contains(vars.FRONTEND_CI_E2E_RUNNER || 'self-hosted', 'self-hosted') }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||
|
||||
@@ -29,8 +29,8 @@ export const sourceMappings = [
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "backoffice",
|
||||
patterns: [/^src\/views\/backoffice\//u],
|
||||
name: "limited-backoffice",
|
||||
patterns: [/^src\/views\/backoffice\//u, /^src\/services\/limitedBackoffice\.js$/u],
|
||||
specs: ["tests/e2e/limited-backoffice.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
@@ -96,8 +96,8 @@ export const sourceMappings = [
|
||||
{
|
||||
name: "pos",
|
||||
patterns: [
|
||||
/\/pos[/-]/iu,
|
||||
/POS/iu,
|
||||
/(?:^|[/_.-])pos(?:[/_.-]|$)/iu,
|
||||
/(?:^|\/)(?:POS|Pos)[A-Z][^/]*\.(?:vue|js|ts)$/u,
|
||||
/^src\/assets\/pos\.css$/u,
|
||||
/^src\/components\/displays\/boxes\/ProductBox\.vue$/u,
|
||||
/^src\/features\/customer\/customerProductRules\.js$/u,
|
||||
@@ -120,12 +120,22 @@ export const sourceMappings = [
|
||||
{
|
||||
name: "admin-department-notifications",
|
||||
patterns: [
|
||||
/^src\/views\/dashboards\/departmentDashboard\/modules\/notifications\/DepartmentNotifications\.vue$/u,
|
||||
/^src\/components\/displays\/department\/notifications\//u,
|
||||
/^src\/components\/displays\/pagination\/models\/DepartmentPos\/NotificationsPhonePagination\.vue$/u,
|
||||
],
|
||||
specs: ["tests/e2e/admin-department-notifications.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "admin-daily-report",
|
||||
patterns: [
|
||||
/^src\/views\/dashboards\/departmentDashboard\/modules\/daily-report\//u,
|
||||
/^src\/components\/session\/token\/SessionUser\/Objects\/DepartmentDailyReports\.vue$/u,
|
||||
],
|
||||
specs: ["tests/e2e/admin-daily-report.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "limited-backoffice",
|
||||
patterns: [/^src\/views\/backoffice\/LimitedBackoffice/u],
|
||||
|
||||
@@ -126,6 +126,8 @@ const dropdownRoot = ref(null);
|
||||
const dropdownContent = ref(null);
|
||||
const isDropdownOpen = ref(false);
|
||||
const dropdownInstanceId = Math.random().toString(36).substring(2, 15);
|
||||
const dropdownPlacement = ref("bottom");
|
||||
const isDropdownPlacementLocked = ref(false);
|
||||
const shouldOpenDropdownUp = ref(false);
|
||||
const dropdownMaxHeight = ref(null);
|
||||
const isDesktopFlyoutLayout = ref(false);
|
||||
@@ -140,6 +142,7 @@ const previewRequestsInFlight = new Set();
|
||||
const generatedObjectUrls = new Set();
|
||||
let previewStateGeneration = 0;
|
||||
let contentResizeObserver = null;
|
||||
let dropdownLayoutUpdateId = 0;
|
||||
const desktopFlyoutMinViewportWidth = 1400;
|
||||
const desktopFlyoutRootPanelWidthRem = 15;
|
||||
const desktopFlyoutSubmenuWidthRem = 17;
|
||||
@@ -230,6 +233,8 @@ const dropdownContentStyle = computed(() => {
|
||||
});
|
||||
|
||||
const resetDropdownLayout = () => {
|
||||
dropdownPlacement.value = "bottom";
|
||||
isDropdownPlacementLocked.value = false;
|
||||
shouldOpenDropdownUp.value = false;
|
||||
dropdownMaxHeight.value = null;
|
||||
isFixedPosition.value = false;
|
||||
@@ -291,7 +296,35 @@ const getViewportInsets = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const getDropdownPlacementSpaces = (triggerRect, viewportInsets = getViewportInsets()) => ({
|
||||
bottom: Math.max(window.innerHeight - triggerRect.bottom - viewportInsets.bottom, 0),
|
||||
top: Math.max(triggerRect.top - viewportInsets.top, 0),
|
||||
});
|
||||
|
||||
const resolveDropdownPlacement = (triggerRect, menuHeight, viewportInsets = getViewportInsets()) => {
|
||||
const spaces = getDropdownPlacementSpaces(triggerRect, viewportInsets);
|
||||
|
||||
return menuHeight > spaces.bottom && spaces.top > spaces.bottom ? "top" : "bottom";
|
||||
};
|
||||
|
||||
const getDropdownAvailableHeight = (triggerRect, viewportInsets = getViewportInsets()) => {
|
||||
const spaces = getDropdownPlacementSpaces(triggerRect, viewportInsets);
|
||||
|
||||
return dropdownPlacement.value === "top" ? spaces.top : spaces.bottom;
|
||||
};
|
||||
|
||||
const lockDropdownPlacement = (triggerRect, menuHeight, viewportInsets = getViewportInsets()) => {
|
||||
if (!isDropdownPlacementLocked.value) {
|
||||
dropdownPlacement.value = resolveDropdownPlacement(triggerRect, menuHeight, viewportInsets);
|
||||
isDropdownPlacementLocked.value = true;
|
||||
}
|
||||
|
||||
shouldOpenDropdownUp.value = dropdownPlacement.value === "top";
|
||||
};
|
||||
|
||||
const updateDropdownLayout = async () => {
|
||||
const updateId = ++dropdownLayoutUpdateId;
|
||||
|
||||
if (!isDropdownOpen.value || !dropdownRoot.value || !dropdownContent.value) {
|
||||
resetDropdownLayout();
|
||||
return;
|
||||
@@ -299,35 +332,41 @@ const updateDropdownLayout = async () => {
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownRoot.value || !dropdownContent.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerElement = dropdownRoot.value.querySelector(".dropdown-trigger");
|
||||
const triggerRect = (triggerElement ?? dropdownRoot.value).getBoundingClientRect();
|
||||
syncDesktopFlyoutState(triggerRect);
|
||||
syncDesktopFlyoutPosition();
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (!dropdownContent.value) {
|
||||
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownContent.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportInsets = getViewportInsets();
|
||||
const viewportTop = viewportInsets.top;
|
||||
const viewportBottom = window.innerHeight - viewportInsets.bottom;
|
||||
const spaceBelow = Math.max(window.innerHeight - triggerRect.bottom - viewportInsets.bottom, 0);
|
||||
const spaceAbove = Math.max(triggerRect.top - viewportInsets.top, 0);
|
||||
const menuHeight = Math.ceil(dropdownContent.value.scrollHeight);
|
||||
const openUpward = menuHeight > spaceBelow && spaceAbove > spaceBelow;
|
||||
const availableHeight = openUpward ? spaceAbove : spaceBelow;
|
||||
lockDropdownPlacement(triggerRect, menuHeight, viewportInsets);
|
||||
const availableHeight = getDropdownAvailableHeight(triggerRect, viewportInsets);
|
||||
const nextMaxHeight = availableHeight > 0 && menuHeight > availableHeight ? Math.floor(availableHeight) : null;
|
||||
|
||||
shouldOpenDropdownUp.value = openUpward;
|
||||
dropdownMaxHeight.value = nextMaxHeight;
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownContent.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncDesktopFlyoutPosition();
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (!dropdownContent.value) {
|
||||
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownContent.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -342,6 +381,12 @@ const updateDropdownLayout = async () => {
|
||||
const boundedHeight = Math.floor(renderedMenuRect.height - overflowAbove - overflowBelow);
|
||||
dropdownMaxHeight.value = boundedHeight > 0 ? boundedHeight : 1;
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncDesktopFlyoutPosition();
|
||||
};
|
||||
|
||||
@@ -1465,7 +1510,7 @@ onMounted(() => {
|
||||
|
||||
contentResizeObserver = new ResizeObserver(() => {
|
||||
if (isDropdownOpen.value) {
|
||||
syncDesktopFlyoutPosition();
|
||||
void updateDropdownLayout();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2399,7 +2444,7 @@ watch(
|
||||
() => {
|
||||
if (isDropdownOpen.value) {
|
||||
void nextTick(() => {
|
||||
syncDesktopFlyoutPosition();
|
||||
void updateDropdownLayout();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2507,7 +2552,6 @@ const syncDesktopFlyoutPosition = () => {
|
||||
|
||||
const triggerRect = triggerEl.getBoundingClientRect();
|
||||
const contentHeight = dropdownContentEl.scrollHeight;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const contentRect = dropdownContentEl.getBoundingClientRect();
|
||||
|
||||
@@ -2537,12 +2581,13 @@ const syncDesktopFlyoutPosition = () => {
|
||||
|
||||
const viewportInsets = getViewportInsets();
|
||||
const viewportTop = viewportInsets.top;
|
||||
const viewportBottom = viewportHeight - viewportInsets.bottom;
|
||||
let top = triggerRect.bottom;
|
||||
|
||||
if (top + contentHeight > viewportBottom) {
|
||||
top = Math.max(viewportTop, triggerRect.top - contentHeight);
|
||||
}
|
||||
const effectiveContentHeight = dropdownMaxHeight.value
|
||||
? Math.min(contentHeight, dropdownMaxHeight.value)
|
||||
: contentHeight;
|
||||
const top =
|
||||
dropdownPlacement.value === "top"
|
||||
? Math.max(viewportTop, triggerRect.top - effectiveContentHeight)
|
||||
: triggerRect.bottom;
|
||||
|
||||
const nextFixedStyles = {
|
||||
top: `${top}px`,
|
||||
@@ -2558,18 +2603,8 @@ const syncDesktopFlyoutPosition = () => {
|
||||
fixedPositionStyles.value = nextFixedStyles;
|
||||
}
|
||||
} else {
|
||||
const contentRect = dropdownContentEl.getBoundingClientRect();
|
||||
const contentBottom = contentRect.top + contentRect.height;
|
||||
|
||||
if (contentBottom > viewportHeight) {
|
||||
const nextTop = `${triggerRect.top - contentRect.height}px`;
|
||||
if (dropdownContentEl.style.top !== nextTop) {
|
||||
dropdownContentEl.style.top = nextTop;
|
||||
}
|
||||
} else {
|
||||
if (dropdownContentEl.style.top !== "") {
|
||||
dropdownContentEl.style.top = "";
|
||||
}
|
||||
if (dropdownContentEl.style.top !== "") {
|
||||
dropdownContentEl.style.top = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+2
-1
@@ -38,7 +38,8 @@ if (router.currentRoute.value.params.departmentId) {
|
||||
<TableLabeledPagination :label="SessionUser.objects.department_notification_sms.meta.title">
|
||||
<template #buttons="{ loadList }">
|
||||
<button
|
||||
class="button is-info is-small"
|
||||
class="button is-primary is-small"
|
||||
data-testid="department-notification-sms-add-button"
|
||||
@click="SessionUser.objects.department_notification_sms.showCreateObjectForm(() => loadList(), {department: parseInt(router.currentRoute.value.params.departmentId)})"
|
||||
>
|
||||
<span class="icon is-small">
|
||||
|
||||
@@ -112,13 +112,24 @@ if (router.currentRoute.value.params.departmentId) {
|
||||
}
|
||||
const isImportLoading = ref(false);
|
||||
|
||||
const buildImportUsageParams = () => {
|
||||
if (!props.inheritPeriodFilters) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
...(props.initialDateFrom ? { dateFrom: props.initialDateFrom } : {}),
|
||||
...(props.initialDateTo ? { dateTo: props.initialDateTo } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
const importUsageButton = async () => {
|
||||
isImportLoading.value = true;
|
||||
try {
|
||||
await SessionUser.request(
|
||||
'/modules/xlvask/tasks/import-usage',
|
||||
'GET',
|
||||
{},
|
||||
buildImportUsageParams(),
|
||||
function (error) {
|
||||
// This function is called when the request fails
|
||||
console.error("Failed to import orders.", error);
|
||||
|
||||
@@ -62,7 +62,7 @@ if (props.autoLoad) {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<UsersTable :objects="list" />
|
||||
<UsersTable :objects="list" @migrated="loadList" />
|
||||
</TableLabeledPagination>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,21 +1,136 @@
|
||||
<script setup>
|
||||
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps(['objects']);
|
||||
import { ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
|
||||
const { loadList } = usePaginatedListInstance();
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||
import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue";
|
||||
|
||||
const redirect = (path) => {
|
||||
window.location = path;
|
||||
}
|
||||
|
||||
const limitedBackofficeTemplates = ref([]);
|
||||
const templatesLoading = ref(false);
|
||||
const templatesLoadError = ref(false);
|
||||
const applyingTemplateKey = ref("");
|
||||
|
||||
const canApplyLimitedBackofficeTemplates = computed(() => SessionUser.hasPermission("add_role_permission"));
|
||||
|
||||
const loadLimitedBackofficeTemplates = async () => {
|
||||
if (!canApplyLimitedBackofficeTemplates.value || templatesLoading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
templatesLoading.value = true;
|
||||
templatesLoadError.value = false;
|
||||
try {
|
||||
const templates = await SessionUser.objects.roles.functions.getLimitedBackofficePermissionTemplates();
|
||||
limitedBackofficeTemplates.value = Array.isArray(templates) ? templates : [];
|
||||
} catch (error) {
|
||||
console.error("Failed to load limited backoffice permission templates", error);
|
||||
templatesLoadError.value = true;
|
||||
} finally {
|
||||
templatesLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const templateTranslation = (template, field) => {
|
||||
const key = `templates.limited_backoffice.roles.${template?.key}.${field}`;
|
||||
const translated = t(key);
|
||||
return translated === key ? template?.[field] || template?.key || "" : translated;
|
||||
};
|
||||
|
||||
const templateLabel = (template) => templateTranslation(template, "label");
|
||||
|
||||
const rolePermissions = (role) => (Array.isArray(role?.permissions) ? role.permissions : []);
|
||||
const templatePermissions = (template) => (Array.isArray(template?.permissions) ? template.permissions : []);
|
||||
|
||||
const missingTemplatePermissions = (role, template) => {
|
||||
const currentPermissions = new Set(rolePermissions(role));
|
||||
return templatePermissions(template).filter((permission) => permission && !currentPermissions.has(permission));
|
||||
};
|
||||
|
||||
const roleTemplateActionKey = (role, template) => `${role?.id}:${template?.key}`;
|
||||
const isApplyingTemplate = (role, template) => applyingTemplateKey.value === roleTemplateActionKey(role, template);
|
||||
const isTemplateActionDisabled = (role, template) =>
|
||||
templatesLoading.value || Boolean(applyingTemplateKey.value) || missingTemplatePermissions(role, template).length === 0;
|
||||
|
||||
const applyLimitedBackofficeTemplate = async (role, template) => {
|
||||
const missingPermissions = missingTemplatePermissions(role, template);
|
||||
const label = templateLabel(template);
|
||||
|
||||
if (missingPermissions.length === 0) {
|
||||
await Swal.fire({
|
||||
icon: "success",
|
||||
title: t("roles.templates.up_to_date_title"),
|
||||
text: t("roles.templates.up_to_date_text", { role: role.name, template: label }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmation = await Swal.fire({
|
||||
icon: "question",
|
||||
title: t("roles.templates.confirm_title"),
|
||||
text: t("roles.templates.confirm_text", {
|
||||
count: missingPermissions.length,
|
||||
role: role.name,
|
||||
template: label,
|
||||
}),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: t("roles.templates.confirm_button"),
|
||||
cancelButtonText: t("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmation.isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyingTemplateKey.value = roleTemplateActionKey(role, template);
|
||||
try {
|
||||
const results = await Promise.allSettled(
|
||||
missingPermissions.map((permission) =>
|
||||
SessionUser.objects.roles.functions.permissions.add(role.id, permission)
|
||||
)
|
||||
);
|
||||
const failed = results.filter((result) => result.status === "rejected");
|
||||
|
||||
if (failed.length > 0) {
|
||||
throw failed[0].reason;
|
||||
}
|
||||
|
||||
await Swal.fire({
|
||||
icon: "success",
|
||||
title: t("roles.templates.success_title"),
|
||||
text: t("roles.templates.success_text", {
|
||||
count: missingPermissions.length,
|
||||
role: role.name,
|
||||
template: label,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to apply limited backoffice permission template", error);
|
||||
await Swal.fire({
|
||||
icon: "error",
|
||||
title: t("roles.templates.error_title"),
|
||||
text: t("roles.templates.error_text", { role: role.name, template: label }),
|
||||
});
|
||||
} finally {
|
||||
applyingTemplateKey.value = "";
|
||||
await loadList();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(loadLimitedBackofficeTemplates);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -66,6 +181,46 @@ const redirect = (path) => {
|
||||
icon="fas fa-clone"
|
||||
:click-action="() => SessionUser.objects.roles.functions.cloneObject(object.id).then(() => loadList())"
|
||||
></ActionSettingsWheelItem>
|
||||
|
||||
<div
|
||||
v-if="canApplyLimitedBackofficeTemplates"
|
||||
class="role-limited-backoffice-template-actions"
|
||||
:data-testid="`role-limited-backoffice-template-actions-${object.id}`"
|
||||
>
|
||||
<ActionSettingsWheelItemLabel
|
||||
:label="$t('roles.templates.section')"
|
||||
icon="fas fa-id-badge"
|
||||
></ActionSettingsWheelItemLabel>
|
||||
<ActionSettingsWheelItem
|
||||
v-if="templatesLoading"
|
||||
:label="$t('roles.templates.loading')"
|
||||
icon="fas fa-spinner"
|
||||
disabled
|
||||
></ActionSettingsWheelItem>
|
||||
<ActionSettingsWheelItem
|
||||
v-else-if="templatesLoadError"
|
||||
:label="$t('roles.templates.load_error')"
|
||||
icon="fas fa-sync-alt"
|
||||
:click-action="loadLimitedBackofficeTemplates"
|
||||
></ActionSettingsWheelItem>
|
||||
<ActionSettingsWheelItem
|
||||
v-else-if="limitedBackofficeTemplates.length === 0"
|
||||
:label="$t('roles.templates.unavailable')"
|
||||
icon="fas fa-ban"
|
||||
disabled
|
||||
></ActionSettingsWheelItem>
|
||||
<template v-else>
|
||||
<ActionSettingsWheelItem
|
||||
v-for="template in limitedBackofficeTemplates"
|
||||
:key="template.key"
|
||||
:label="$t('roles.templates.apply', { template: templateLabel(template) })"
|
||||
:icon="isApplyingTemplate(object, template) ? 'fas fa-spinner' : 'fas fa-id-badge'"
|
||||
:disabled="isTemplateActionDisabled(object, template)"
|
||||
:test-id="`role-template-${object.id}-${template.key}`"
|
||||
:click-action="() => applyLimitedBackofficeTemplate(object, template)"
|
||||
></ActionSettingsWheelItem>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</div>
|
||||
@@ -85,4 +240,4 @@ const redirect = (path) => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||
import { showEditUserForm } from "@/components/forms/superUser/editUserForm.vue";
|
||||
import {
|
||||
getLimitedBackofficeDepartments,
|
||||
getLimitedBackofficeRoles,
|
||||
limitedBackofficeErrorMessage,
|
||||
migrateLimitedBackofficeEmployee,
|
||||
unwrapLimitedBackofficeResponse,
|
||||
} from "@/services/limitedBackoffice.js";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
defineProps({
|
||||
objects: {
|
||||
@@ -10,11 +18,110 @@ defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["migrated"]);
|
||||
|
||||
const editUser = (user) => {
|
||||
showEditUserForm(user.id, user.customer_number, user.display_name, user.group_id, {
|
||||
limitedBackofficeManaged: Boolean(user.limited_backoffice_managed),
|
||||
});
|
||||
};
|
||||
|
||||
const escapeHtml = (value) =>
|
||||
String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
|
||||
const canMigrateToLimitedBackoffice = (user) =>
|
||||
Number(user?.customer_number) === 0 && !Boolean(user?.limited_backoffice_managed);
|
||||
|
||||
const migrateUser = async (user) => {
|
||||
try {
|
||||
const [departmentsResponse, rolesResponse] = await Promise.all([
|
||||
getLimitedBackofficeDepartments(),
|
||||
getLimitedBackofficeRoles(),
|
||||
]);
|
||||
const departments = unwrapLimitedBackofficeResponse(departmentsResponse) || [];
|
||||
const roles = unwrapLimitedBackofficeResponse(rolesResponse) || [];
|
||||
|
||||
if (!Array.isArray(departments) || departments.length === 0 || !Array.isArray(roles) || roles.length === 0) {
|
||||
throw new Error("Limited backoffice roles or departments are unavailable.");
|
||||
}
|
||||
|
||||
const roleOptions = roles
|
||||
.filter((role) => role?.key && role?.key !== "superuser")
|
||||
.map((role) => {
|
||||
const selected = role.key === "cashier" ? " selected" : "";
|
||||
return `<option value="${escapeHtml(role.key)}"${selected}>${escapeHtml(role.label || role.key)}</option>`;
|
||||
})
|
||||
.join("");
|
||||
const departmentOptions = departments
|
||||
.map(
|
||||
(department) => `
|
||||
<label class="checkbox is-block has-text-left mb-2">
|
||||
<input type="checkbox" class="limited-migration-department" value="${escapeHtml(department.id)}" />
|
||||
${escapeHtml(department.name || `Department ${department.id}`)}
|
||||
</label>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const result = await Swal.fire({
|
||||
title: "Migrate employee",
|
||||
html: `
|
||||
<div class="field has-text-left">
|
||||
<label class="label">Employee</label>
|
||||
<p>${escapeHtml(user.display_name || `#${user.id}`)}</p>
|
||||
</div>
|
||||
<div class="field has-text-left">
|
||||
<label class="label" for="limited-migration-role">Role</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="limited-migration-role">${roleOptions}</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field has-text-left">
|
||||
<label class="label">Departments</label>
|
||||
<div class="control">${departmentOptions}</div>
|
||||
</div>
|
||||
`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Migrate",
|
||||
preConfirm: async () => {
|
||||
const role = document.getElementById("limited-migration-role")?.value;
|
||||
const departmentIds = Array.from(document.querySelectorAll(".limited-migration-department:checked")).map(
|
||||
(checkbox) => Number(checkbox.value)
|
||||
);
|
||||
|
||||
if (!role || departmentIds.length === 0) {
|
||||
Swal.showValidationMessage("Select a role and at least one department.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await migrateLimitedBackofficeEmployee(user.id, {
|
||||
role_key: role,
|
||||
department_ids: departmentIds,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
Swal.showValidationMessage(limitedBackofficeErrorMessage(error, "Could not migrate employee."));
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await Swal.fire("Employee migrated");
|
||||
emit("migrated");
|
||||
}
|
||||
} catch (error) {
|
||||
await Swal.fire("Error", limitedBackofficeErrorMessage(error, "Could not migrate employee."), "error");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -47,6 +154,13 @@ const editUser = (user) => {
|
||||
:label="$t('global.edit')"
|
||||
:test-id="`superuser-user-edit-${user.id}`"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
v-if="canMigrateToLimitedBackoffice(user)"
|
||||
:click-action="() => migrateUser(user)"
|
||||
icon="fas fa-random"
|
||||
:label="$t('superuser.pages.employees.migrate_to_limited_backoffice')"
|
||||
:test-id="`superuser-user-migrate-limited-${user.id}`"
|
||||
/>
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
<script setup>
|
||||
|
||||
const props = defineProps(['invoice_id'])
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import { handleEconomicError } from "@/components/request/HandleEconomicError.vue";
|
||||
import { ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps(["invoice_id"]);
|
||||
|
||||
const hasInvoice = computed(() => {
|
||||
return props.invoice_id !== null && props.invoice_id !== undefined && String(props.invoice_id).trim() !== "";
|
||||
});
|
||||
|
||||
const getOrderPDF = async () => {
|
||||
await authenticatedRequest('/invoices/pdf?id=' + props.invoice_id,
|
||||
'GET',{
|
||||
}) .then((response) => {
|
||||
if (!hasInvoice.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await authenticatedRequest("/invoices/pdf", "GET", {
|
||||
id: props.invoice_id,
|
||||
}).then((response) => {
|
||||
// Open the PDF in a new tab
|
||||
window.open(response.data.data.url, '_blank');
|
||||
window.open(response.data.data.url, "_blank");
|
||||
}).catch((error) => {
|
||||
handleEconomicError(error);
|
||||
});
|
||||
@@ -19,9 +27,17 @@ const getOrderPDF = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadButtonWhileAwait class="is-dark" :loadFunction="getOrderPDF" icon="fas fa-file-pdf"> Hent faktura PDF </LoadButtonWhileAwait>
|
||||
<LoadButtonWhileAwait
|
||||
class="is-dark"
|
||||
:loadFunction="getOrderPDF"
|
||||
:disabled="!hasInvoice"
|
||||
actionKey="economic-invoice-pdf-download"
|
||||
icon="fas fa-file-pdf"
|
||||
>
|
||||
Hent faktura PDF
|
||||
</LoadButtonWhileAwait>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -164,6 +164,21 @@ const t = (key) => i18n.global.t(key);
|
||||
}
|
||||
);
|
||||
},
|
||||
setProductTarget: async ({ department_id, product_id, target_percentage }) => {
|
||||
const normalizedTarget = target_percentage === null || target_percentage === undefined || target_percentage === ""
|
||||
? null
|
||||
: Number(target_percentage);
|
||||
|
||||
return authenticatedRequest(
|
||||
"/departments/daily-reports/product-targets",
|
||||
"PUT",
|
||||
{
|
||||
department_id: parseInt(department_id, 10),
|
||||
product_id: parseInt(product_id, 10),
|
||||
target_percentage: normalizedTarget,
|
||||
}
|
||||
);
|
||||
},
|
||||
createComplaint: async ({
|
||||
department_id,
|
||||
customer_number = null,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
import Swal from "sweetalert2";
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
@@ -123,6 +122,10 @@ export const Roles = {
|
||||
id: id
|
||||
});
|
||||
},
|
||||
getLimitedBackofficePermissionTemplates: async () => {
|
||||
const response = await authenticatedRequest("/roles/limited-backoffice-permission-templates", "GET");
|
||||
return response?.data?.data ?? response?.data ?? [];
|
||||
},
|
||||
permissions: {
|
||||
add: async (roleId, permissionId) => {
|
||||
return authenticatedRequest("/roles/permissions", "POST", {
|
||||
@@ -161,4 +164,4 @@ export const Roles = {
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1270,6 +1270,11 @@
|
||||
"groups": {
|
||||
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
|
||||
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
|
||||
"products": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.products'}",
|
||||
"customers": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.customers'}",
|
||||
"vehicles": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.vehicles'}",
|
||||
"attachments": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.attachments'}",
|
||||
"scanner": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.scanner'}",
|
||||
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
|
||||
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
|
||||
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
|
||||
@@ -1321,6 +1326,66 @@
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
|
||||
@@ -1517,6 +1582,14 @@
|
||||
"subtitle_suffix": "@:{'words.generated.salg'} @:{'words.generated.af'} produktet",
|
||||
"title": "Produktsalg"
|
||||
},
|
||||
"product_targets": {
|
||||
"cancel_aria": "Annuller mål",
|
||||
"edit_aria": "Rediger målprocent",
|
||||
"input_label": "Målprocent",
|
||||
"save_aria": "Gem mål",
|
||||
"save_error": "Målet kunne ikke gemmes.",
|
||||
"validation_range": "Målprocenten skal være mellem 0 og 100."
|
||||
},
|
||||
"reports": {
|
||||
"bookings": {
|
||||
"completed_count": "@.capitalize:{'words.generated.antal'} @:{'words.generated.fuldførte'}",
|
||||
@@ -4975,6 +5048,22 @@
|
||||
"title": "Tilladelser for rolle {id}",
|
||||
"title_with_name": "Tilladelser for {name}"
|
||||
},
|
||||
"templates": {
|
||||
"apply": "Anvend {template}-skabelon",
|
||||
"confirm_button": "Anvend skabelon",
|
||||
"confirm_text": "Tilfoej {count} manglende tilladelser fra {template} til {role}?",
|
||||
"confirm_title": "Anvend tilladelsesskabelon?",
|
||||
"error_text": "En eller flere tilladelser fra {template} kunne ikke tilfoejes til {role}. Rollelisten blev genindlaest.",
|
||||
"error_title": "Kunne ikke anvende skabelon",
|
||||
"load_error": "Kunne ikke indlaese skabeloner. Proev igen",
|
||||
"loading": "Indlaeser skabeloner...",
|
||||
"section": "Begraenset backoffice-skabeloner",
|
||||
"success_text": "Tilfoejede {count} tilladelser fra {template} til {role}.",
|
||||
"success_title": "Skabelon anvendt",
|
||||
"unavailable": "Ingen skabeloner er tilgaengelige.",
|
||||
"up_to_date_text": "{role} har allerede alle tilladelser i {template}.",
|
||||
"up_to_date_title": "Rollen er opdateret"
|
||||
},
|
||||
"subtitle": "@.capitalize:{'words.generated.brugerroller'}"
|
||||
},
|
||||
"self_wash": {
|
||||
@@ -5384,6 +5473,7 @@
|
||||
},
|
||||
"employees": {
|
||||
"create": "@:{'words.generated.opret'} @:{'words.generated.ny'} @:{'words.generated.medarbejder'}",
|
||||
"migrate_to_limited_backoffice": "Migrer til begrænset backoffice",
|
||||
"subtitle": "@.capitalize:{'words.generated.her'} @:{'words.generated.kan'} @:{'words.generated.du'} @:{'words.generated.se'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.liste'} @:{'words.generated.over'} @:{'words.generated.alle'} @:{'words.generated.medarbejdere'} @:{'words.generated.i'} @:{'words.generated.systemet'}"
|
||||
},
|
||||
"orders": {
|
||||
@@ -6127,12 +6217,12 @@
|
||||
},
|
||||
"roles": {
|
||||
"viewer": {
|
||||
"label": "Viser",
|
||||
"description": "@.capitalize:{'words.generated.kan'} logge ind @:{'words.generated.og'} se data @:{'words.generated.for'} @:{'words.generated.tildelte'} @:{'words.generated.afdelinger'}."
|
||||
"label": "Deaktiveret",
|
||||
"description": "Holder medarbejderen registreret uden adgang til ordre-, booking- eller administrationsfunktioner."
|
||||
},
|
||||
"cashier": {
|
||||
"label": "Kassemedarbejder",
|
||||
"description": "@.capitalize:{'words.generated.kan'} arbejde med @:{'words.generated.ordrer'} @:{'words.generated.og'} ordrelinjer @:{'words.generated.for'} @:{'words.generated.tildelte'} @:{'words.generated.afdelinger'}."
|
||||
"description": "@.capitalize:{'words.generated.kan'} arbejde med @:{'words.generated.ordrer'}, ordrelinjer @:{'words.generated.og'} @:{'words.generated.bookinger'} @:{'words.generated.for'} @:{'words.generated.tildelte'} @:{'words.generated.afdelinger'}."
|
||||
},
|
||||
"booking_coordinator": {
|
||||
"label": "Bookingkoordinator",
|
||||
@@ -6167,6 +6257,11 @@
|
||||
"groups": {
|
||||
"account": "Konto",
|
||||
"orders": "Ordrer",
|
||||
"products": "Produkter",
|
||||
"customers": "Kunder",
|
||||
"vehicles": "Køretøjer",
|
||||
"attachments": "Vedhæftninger",
|
||||
"scanner": "Scannere",
|
||||
"bookings": "Bookinger",
|
||||
"time_bookings": "Tidsbookinger",
|
||||
"reports": "Rapporter",
|
||||
@@ -6218,6 +6313,66 @@
|
||||
"label": "Opkræv ordrer",
|
||||
"description": "Kan tage betaling eller opkræve en betalingsreservation for en ordre."
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "Afslut ordrer",
|
||||
"description": "Kan markere ordrer som afsluttede."
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "Se produktkatalog",
|
||||
"description": "Kan indlæse POS-kategorier, produkter, priser og tilvalg."
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "Se produktforslag",
|
||||
"description": "Kan se produktforslag baseret på ordrer og køretøjsdata."
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "Søg kunder",
|
||||
"description": "Kan søge og vælge kunder til POS-ordrer."
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "Se kundeoplysninger",
|
||||
"description": "Kan indlæse kundeoplysninger fra et kundenummer."
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "Se kundebemærkninger",
|
||||
"description": "Kan læse kundebemærkninger i POS."
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "Tilføj kundebemærkninger",
|
||||
"description": "Kan tilføje kundebemærkninger fra POS."
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "Se kundemarkeringer",
|
||||
"description": "Kan se kundemarkeringer, der påvirker ordrekrav og produktregler."
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "Søg køretøjer",
|
||||
"description": "Kan søge nummerplader og se køretøjsstatus."
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "Se køretøjsmatch",
|
||||
"description": "Kan se kundematch for køretøjer og forslag til ukendte køretøjer."
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "Se køretøjshistorik",
|
||||
"description": "Kan se seneste ordrehistorik for en nummerplade."
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "Se ordrevedhæftninger",
|
||||
"description": "Kan liste vedhæftninger på ordrer."
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "Tilføj ordrevedhæftninger",
|
||||
"description": "Kan uploade filer og vedhæfte vaskecertifikater til ordrer."
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "Download ordrevedhæftninger",
|
||||
"description": "Kan åbne og downloade ordrevedhæftninger."
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "Se nummerpladescanninger",
|
||||
"description": "Kan se seneste nummerpladescanninger og scannere for tildelte afdelinger."
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "Se afdelingsbookinger",
|
||||
"description": "Kan se bookinger for tildelte afdelinger."
|
||||
|
||||
@@ -1381,6 +1381,11 @@
|
||||
"groups": {
|
||||
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
|
||||
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
|
||||
"products": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.products'}",
|
||||
"customers": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.customers'}",
|
||||
"vehicles": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.vehicles'}",
|
||||
"attachments": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.attachments'}",
|
||||
"scanner": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.scanner'}",
|
||||
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
|
||||
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
|
||||
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
|
||||
@@ -1432,6 +1437,66 @@
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
|
||||
@@ -1628,6 +1693,14 @@
|
||||
"subtitle_suffix": "Produktverk?@:{'words.generated.ufe'}",
|
||||
"title": "@:{'templates.generated.compat.admin.daily_report.product_sales.subtitle_suffix'}"
|
||||
},
|
||||
"product_targets": {
|
||||
"cancel_aria": "Ziel abbrechen",
|
||||
"edit_aria": "Zielprozentsatz bearbeiten",
|
||||
"input_label": "Zielprozentsatz",
|
||||
"save_aria": "Ziel speichern",
|
||||
"save_error": "Das Ziel konnte nicht gespeichert werden.",
|
||||
"validation_range": "Der Zielprozentsatz muss zwischen 0 und 100 liegen."
|
||||
},
|
||||
"reports": {
|
||||
"bookings": {
|
||||
"completed_count": "@:{'words.generated.abgeschlossene'} @:{'words.generated.anzahl'}",
|
||||
@@ -3587,6 +3660,13 @@
|
||||
"opening_hours": {
|
||||
"closed": "@:{'templates.generated.compat.global.closed'}"
|
||||
},
|
||||
"pricing": {
|
||||
"custom_pricing_disabled": "Fallback aktiviert",
|
||||
"custom_pricing_enabled": "Nur eigene Preise",
|
||||
"custom_pricing_missing_price": "Fehlende Abteilungspreise werden zu 999999.",
|
||||
"custom_pricing_only": "Keine Fallback-Preise",
|
||||
"effective_department_price": "Effektiver Abteilungspreis"
|
||||
},
|
||||
"search_placeholder": "@.capitalize:{'words.generated.nach'} @:{'words.generated.abteilungsname'} @:{'words.generated.suchen'}",
|
||||
"select_department": "@:{'templates.generated.compat.nav.select_department'}",
|
||||
"subtitle": "@:{'words.generated.abteilungen'} @:{'words.generated.verwalten'}",
|
||||
@@ -4426,6 +4506,7 @@
|
||||
"departments": {
|
||||
"columns": {
|
||||
"archived": "Archiviert",
|
||||
"custom_pricing_only": "@:{'templates.generated.compat.departments.pricing.custom_pricing_only'}",
|
||||
"dimension": "Abmessung",
|
||||
"economic_department": "@:{'templates.generated.compat.objects.economic_departments.single'}",
|
||||
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
|
||||
@@ -5078,6 +5159,22 @@
|
||||
"title": "Berechtigungen fuer Rolle {id}",
|
||||
"title_with_name": "Berechtigungen fuer {name}"
|
||||
},
|
||||
"templates": {
|
||||
"apply": "{template}-Vorlage anwenden",
|
||||
"confirm_button": "Vorlage anwenden",
|
||||
"confirm_text": "{count} fehlende Berechtigungen aus {template} zu {role} hinzufuegen?",
|
||||
"confirm_title": "Berechtigungsvorlage anwenden?",
|
||||
"error_text": "Eine oder mehrere Berechtigungen aus {template} konnten {role} nicht hinzugefuegt werden. Die Rollenliste wurde neu geladen.",
|
||||
"error_title": "Vorlage konnte nicht angewendet werden",
|
||||
"load_error": "Vorlagen konnten nicht geladen werden. Erneut versuchen",
|
||||
"loading": "Vorlagen werden geladen...",
|
||||
"section": "Limited-Backoffice-Vorlagen",
|
||||
"success_text": "{count} Berechtigungen aus {template} zu {role} hinzugefuegt.",
|
||||
"success_title": "Vorlage angewendet",
|
||||
"unavailable": "Keine Vorlagen verfuegbar.",
|
||||
"up_to_date_text": "{role} hat bereits alle Berechtigungen in {template}.",
|
||||
"up_to_date_title": "Rolle ist aktuell"
|
||||
},
|
||||
"subtitle": "Benutzerrollen"
|
||||
},
|
||||
"self_wash": {
|
||||
@@ -5487,6 +5584,7 @@
|
||||
},
|
||||
"employees": {
|
||||
"create": "@.capitalize:{'words.generated.neuen'} @:{'words.generated.mitarbeiter'} @:{'words.generated.erstellen'}",
|
||||
"migrate_to_limited_backoffice": "Zu eingeschränktem Backoffice migrieren",
|
||||
"subtitle": "@.capitalize:{'words.generated.hier'} @:{'words.generated.sehen'} @.capitalize:{'words.generated.sie'} @:{'words.generated.eine'} @:{'words.generated.liste'} @:{'words.generated.aller'} @:{'words.generated.mitarbeiter'} @:{'words.generated.im'} @.capitalize:{'words.generated.system'}"
|
||||
},
|
||||
"orders": {
|
||||
@@ -6098,6 +6196,20 @@
|
||||
},
|
||||
"vehicles": {
|
||||
"add": "@:{'words.generated.fahrzeug'} @:{'words.generated.hinzuf'}?@:{'words.generated.gen'}",
|
||||
"add_modal": {
|
||||
"customer_label": "Kunde",
|
||||
"customer_placeholder": "Nach Kundenname oder Kundennummer suchen",
|
||||
"error": "Fahrzeug konnte nicht hinzugefuegt werden.",
|
||||
"no_customer_results": "Keine Kunden gefunden",
|
||||
"reference_placeholder": "Optionale Referenz",
|
||||
"registration_placeholder": "Kennzeichen",
|
||||
"selected_customer": "Ausgewaehlter Kunde",
|
||||
"submit": "Fahrzeug hinzufuegen",
|
||||
"title": "Fahrzeug hinzufuegen",
|
||||
"type_load_error": "Fahrzeugtypen konnten nicht geladen werden.",
|
||||
"type_placeholder": "Fahrzeugtyp auswaehlen",
|
||||
"validation_error": "Kunde, Kennzeichen und Fahrzeugtyp auswaehlen."
|
||||
},
|
||||
"brand": "@:{'templates.generated.compat.admin.pos.make'}",
|
||||
"color": "Farbe",
|
||||
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
|
||||
@@ -6216,12 +6328,12 @@
|
||||
},
|
||||
"roles": {
|
||||
"viewer": {
|
||||
"label": "Betrachter",
|
||||
"description": "@.capitalize:{'words.generated.kann'} sich anmelden @:{'words.generated.und'} Daten zugewiesener @:{'words.generated.abteilungen'} @:{'words.generated.anzeigen'}."
|
||||
"label": "Deaktiviert",
|
||||
"description": "Hält den Mitarbeiter registriert, ohne Zugriff auf Auftrags-, Buchungs- oder Verwaltungsfunktionen."
|
||||
},
|
||||
"cashier": {
|
||||
"label": "Kassierer",
|
||||
"description": "@.capitalize:{'words.generated.kann'} mit Aufträgen @:{'words.generated.und'} Auftragszeilen @:{'words.generated.fur'} @:{'words.generated.zugewiesene'} @:{'words.generated.abteilungen'} arbeiten."
|
||||
"description": "@.capitalize:{'words.generated.kann'} mit Aufträgen, Auftragszeilen @:{'words.generated.und'} @:{'words.generated.buchungen'} @:{'words.generated.fur'} @:{'words.generated.zugewiesene'} @:{'words.generated.abteilungen'} arbeiten."
|
||||
},
|
||||
"booking_coordinator": {
|
||||
"label": "Buchungskoordinator",
|
||||
@@ -6256,6 +6368,11 @@
|
||||
"groups": {
|
||||
"account": "Konto",
|
||||
"orders": "Aufträge",
|
||||
"products": "Produkte",
|
||||
"customers": "Kunden",
|
||||
"vehicles": "Fahrzeuge",
|
||||
"attachments": "Anhänge",
|
||||
"scanner": "Scanner",
|
||||
"bookings": "Buchungen",
|
||||
"time_bookings": "Zeitbuchungen",
|
||||
"reports": "Berichte",
|
||||
@@ -6307,6 +6424,66 @@
|
||||
"label": "Aufträge abrechnen",
|
||||
"description": "Kann Zahlungen annehmen oder eine Zahlungsreservierung für einen Auftrag belasten."
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "Aufträge abschließen",
|
||||
"description": "Kann Aufträge als abgeschlossen markieren."
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "Produktkatalog anzeigen",
|
||||
"description": "Kann POS-Kategorien, Produkte, Preise und Zusatzoptionen laden."
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "Produktempfehlungen anzeigen",
|
||||
"description": "Kann Produktempfehlungen auf Basis von Aufträgen und Fahrzeugdaten sehen."
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "Kunden suchen",
|
||||
"description": "Kann Kunden für POS-Aufträge suchen und auswählen."
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "Kundendetails anzeigen",
|
||||
"description": "Kann Kundendetails anhand einer Kundennummer laden."
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "Kundennotizen anzeigen",
|
||||
"description": "Kann Kundennotizen im POS lesen."
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "Kundennotizen hinzufügen",
|
||||
"description": "Kann Kundennotizen im POS hinzufügen."
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "Kundenmarkierungen anzeigen",
|
||||
"description": "Kann Kundenmarkierungen sehen, die Auftragsanforderungen und Produktregeln beeinflussen."
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "Fahrzeuge suchen",
|
||||
"description": "Kann Kennzeichen suchen und den Fahrzeugstatus sehen."
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "Fahrzeugtreffer anzeigen",
|
||||
"description": "Kann Kundentreffer für Fahrzeuge und Vorschläge zu unbekannten Fahrzeugen sehen."
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "Fahrzeughistorie anzeigen",
|
||||
"description": "Kann die letzte Auftragshistorie für ein Kennzeichen sehen."
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "Auftragsanhänge anzeigen",
|
||||
"description": "Kann Anhänge auf Aufträgen auflisten."
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "Auftragsanhänge hinzufügen",
|
||||
"description": "Kann Dateien hochladen und Waschzertifikate an Aufträge anhängen."
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "Auftragsanhänge herunterladen",
|
||||
"description": "Kann Auftragsanhänge öffnen und herunterladen."
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "Kennzeichenscans anzeigen",
|
||||
"description": "Kann aktuelle Kennzeichenscans und Scanner für zugewiesene Abteilungen sehen."
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "Abteilungsbuchungen anzeigen",
|
||||
"description": "Kann Buchungen für zugewiesene Abteilungen sehen."
|
||||
|
||||
@@ -1102,6 +1102,11 @@
|
||||
"groups": {
|
||||
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
|
||||
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
|
||||
"products": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.products'}",
|
||||
"customers": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.customers'}",
|
||||
"vehicles": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.vehicles'}",
|
||||
"attachments": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.attachments'}",
|
||||
"scanner": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.scanner'}",
|
||||
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
|
||||
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
|
||||
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
|
||||
@@ -1153,6 +1158,66 @@
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
|
||||
@@ -1349,6 +1414,14 @@
|
||||
"subtitle_suffix": "@:{'words.generated.product'} @:{'words.generated.sales'}",
|
||||
"title": "@.capitalize:{'words.generated.product'} @.capitalize:{'words.generated.sales'}"
|
||||
},
|
||||
"product_targets": {
|
||||
"cancel_aria": "Cancel target",
|
||||
"edit_aria": "Edit target percentage",
|
||||
"input_label": "Target percentage",
|
||||
"save_aria": "Save target",
|
||||
"save_error": "The target could not be saved.",
|
||||
"validation_range": "The target percentage must be between 0 and 100."
|
||||
},
|
||||
"reports": {
|
||||
"bookings": {
|
||||
"completed_count": "@.capitalize:{'words.generated.completed'} @:{'words.generated.count'}",
|
||||
@@ -4807,6 +4880,22 @@
|
||||
"title": "Role {id} permissions",
|
||||
"title_with_name": "{name} permissions"
|
||||
},
|
||||
"templates": {
|
||||
"apply": "Apply {template} template",
|
||||
"confirm_button": "Apply template",
|
||||
"confirm_text": "Add {count} missing permissions from {template} to {role}?",
|
||||
"confirm_title": "Apply permission template?",
|
||||
"error_text": "One or more permissions from {template} could not be added to {role}. The role list was reloaded.",
|
||||
"error_title": "Could not apply template",
|
||||
"load_error": "Could not load templates. Try again",
|
||||
"loading": "Loading templates...",
|
||||
"section": "Limited backoffice templates",
|
||||
"success_text": "Added {count} permissions from {template} to {role}.",
|
||||
"success_title": "Template applied",
|
||||
"unavailable": "No templates are available.",
|
||||
"up_to_date_text": "{role} already has every permission in {template}.",
|
||||
"up_to_date_title": "Role is up to date"
|
||||
},
|
||||
"subtitle": "@.capitalize:{'words.generated.user'} @:{'words.generated.roles'}"
|
||||
},
|
||||
"self_wash": {
|
||||
@@ -5216,6 +5305,7 @@
|
||||
},
|
||||
"employees": {
|
||||
"create": "@.capitalize:{'words.generated.create'} @:{'words.generated.new'} @:{'words.generated.employee'}",
|
||||
"migrate_to_limited_backoffice": "Migrate to limited backoffice",
|
||||
"subtitle": "@.capitalize:{'words.generated.here'} @:{'words.generated.you'} @:{'words.generated.can'} @:{'words.generated.see'} @:{'words.generated.a'} @:{'words.generated.list'} @:{'words.generated.of'} @:{'words.generated.all'} @:{'words.generated.employees'} @:{'words.generated.in'} @:{'words.replication.article.host_mention'} @:{'words.generated.system'}"
|
||||
},
|
||||
"orders": {
|
||||
@@ -5959,12 +6049,12 @@
|
||||
},
|
||||
"roles": {
|
||||
"viewer": {
|
||||
"label": "Viewer",
|
||||
"description": "@.capitalize:{'words.generated.can'} sign @:{'words.generated.in'} @:{'words.generated.and'} view @:{'words.generated.assigned'} @:{'words.generated.department'} data."
|
||||
"label": "Deactivated",
|
||||
"description": "Keeps the employee registered without order, booking, or management permissions."
|
||||
},
|
||||
"cashier": {
|
||||
"label": "Cashier",
|
||||
"description": "@.capitalize:{'words.generated.can'} work with @:{'words.generated.orders'} @:{'words.generated.and'} order lines @:{'words.generated.for'} @:{'words.generated.assigned'} @:{'words.generated.departments'}."
|
||||
"description": "@.capitalize:{'words.generated.can'} work with @:{'words.generated.orders'}, order lines @:{'words.generated.and'} @:{'words.generated.bookings'} @:{'words.generated.for'} @:{'words.generated.assigned'} @:{'words.generated.departments'}."
|
||||
},
|
||||
"booking_coordinator": {
|
||||
"label": "Booking coordinator",
|
||||
@@ -5999,6 +6089,11 @@
|
||||
"groups": {
|
||||
"account": "Account",
|
||||
"orders": "Orders",
|
||||
"products": "Products",
|
||||
"customers": "Customers",
|
||||
"vehicles": "Vehicles",
|
||||
"attachments": "Attachments",
|
||||
"scanner": "Scanners",
|
||||
"bookings": "Bookings",
|
||||
"time_bookings": "Time bookings",
|
||||
"reports": "Reports",
|
||||
@@ -6050,6 +6145,66 @@
|
||||
"label": "Charge orders",
|
||||
"description": "Can take payment or charge a payment intent for an order."
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "Complete orders",
|
||||
"description": "Can mark orders as completed."
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "View product catalog",
|
||||
"description": "Can load POS categories, products, prices, and addons."
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "View product recommendations",
|
||||
"description": "Can see product suggestions based on orders and vehicle data."
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "Search customers",
|
||||
"description": "Can search and select customers for POS orders."
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "View customer details",
|
||||
"description": "Can load customer details from a customer number."
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "View customer notes",
|
||||
"description": "Can read customer notes in POS."
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "Add customer notes",
|
||||
"description": "Can add customer notes from POS."
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "View customer flags",
|
||||
"description": "Can see customer flags that affect order requirements and product rules."
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "Search vehicles",
|
||||
"description": "Can search license plates and see vehicle status."
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "View vehicle matches",
|
||||
"description": "Can see vehicle customer matches and unknown vehicle suggestions."
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "View vehicle history",
|
||||
"description": "Can see recent order history for a license plate."
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "View order attachments",
|
||||
"description": "Can list attachments on orders."
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "Add order attachments",
|
||||
"description": "Can upload files and attach wash certificates to orders."
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "Download order attachments",
|
||||
"description": "Can open and download order attachments."
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "View plate scans",
|
||||
"description": "Can view recent license plate scans and scanners for assigned departments."
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "View department bookings",
|
||||
"description": "Can see bookings for assigned departments."
|
||||
|
||||
@@ -126,6 +126,14 @@
|
||||
"subtitle_suffix": "@:{'templates.generated.compat.admin.daily_report.product_sales.subtitle_suffix'}",
|
||||
"title": "@:{'templates.generated.compat.admin.daily_report.product_sales.title'}"
|
||||
},
|
||||
"product_targets": {
|
||||
"cancel_aria": "@:{'templates.generated.compat.admin.daily_report.product_targets.cancel_aria'}",
|
||||
"edit_aria": "@:{'templates.generated.compat.admin.daily_report.product_targets.edit_aria'}",
|
||||
"input_label": "@:{'templates.generated.compat.admin.daily_report.product_targets.input_label'}",
|
||||
"save_aria": "@:{'templates.generated.compat.admin.daily_report.product_targets.save_aria'}",
|
||||
"save_error": "@:{'templates.generated.compat.admin.daily_report.product_targets.save_error'}",
|
||||
"validation_range": "@:{'templates.generated.compat.admin.daily_report.product_targets.validation_range'}"
|
||||
},
|
||||
"reports": {
|
||||
"avg_revenue": {
|
||||
"label": "@:{'templates.generated.compat.department_reports.avg_revenue_per_wash'}",
|
||||
@@ -4365,6 +4373,22 @@
|
||||
"title": "@:{'templates.generated.compat.roles.permissions.title'}",
|
||||
"title_with_name": "@:{'templates.generated.compat.roles.permissions.title_with_name'}"
|
||||
},
|
||||
"templates": {
|
||||
"apply": "@:{'templates.generated.compat.roles.templates.apply'}",
|
||||
"confirm_button": "@:{'templates.generated.compat.roles.templates.confirm_button'}",
|
||||
"confirm_text": "@:{'templates.generated.compat.roles.templates.confirm_text'}",
|
||||
"confirm_title": "@:{'templates.generated.compat.roles.templates.confirm_title'}",
|
||||
"error_text": "@:{'templates.generated.compat.roles.templates.error_text'}",
|
||||
"error_title": "@:{'templates.generated.compat.roles.templates.error_title'}",
|
||||
"load_error": "@:{'templates.generated.compat.roles.templates.load_error'}",
|
||||
"loading": "@:{'templates.generated.compat.roles.templates.loading'}",
|
||||
"section": "@:{'templates.generated.compat.roles.templates.section'}",
|
||||
"success_text": "@:{'templates.generated.compat.roles.templates.success_text'}",
|
||||
"success_title": "@:{'templates.generated.compat.roles.templates.success_title'}",
|
||||
"unavailable": "@:{'templates.generated.compat.roles.templates.unavailable'}",
|
||||
"up_to_date_text": "@:{'templates.generated.compat.roles.templates.up_to_date_text'}",
|
||||
"up_to_date_title": "@:{'templates.generated.compat.roles.templates.up_to_date_title'}"
|
||||
},
|
||||
"subtitle": "@:{'templates.generated.compat.roles.subtitle'}",
|
||||
"title": "@:{'templates.generated.compat.common.roles'}"
|
||||
},
|
||||
@@ -4582,8 +4606,8 @@
|
||||
"overview": "Overview",
|
||||
"modules": "Modules",
|
||||
"branding": "Profile & Branding",
|
||||
"gateways": "Gateways",
|
||||
"stripe": "Stripe",
|
||||
"gateways": "@.capitalize:{'words.generated.gateways'}",
|
||||
"stripe": "@:{'words.generated.stripe'}",
|
||||
"pricing": "Pricing",
|
||||
"categories": "Categories"
|
||||
},
|
||||
@@ -4626,7 +4650,7 @@
|
||||
"department_id": "Department ID",
|
||||
"economic_department_id": "Economic department",
|
||||
"branding": "Branding",
|
||||
"created_at": "Created",
|
||||
"created_at": "@:common.created",
|
||||
"updated_at": "Updated"
|
||||
},
|
||||
"hardware": {
|
||||
@@ -4642,12 +4666,12 @@
|
||||
"quick_links": {
|
||||
"title": "Department tools",
|
||||
"subtitle": "Open the focused setup areas for this department",
|
||||
"modules": "Modules",
|
||||
"branding": "Branding",
|
||||
"gateways": "Gateways",
|
||||
"stripe": "Stripe",
|
||||
"pricing": "Pricing",
|
||||
"categories": "Categories"
|
||||
"modules": "@:superuser_dashboard.department_navigation.modules",
|
||||
"branding": "@:superuser_dashboard.department_overview.profile.branding",
|
||||
"gateways": "@:superuser_dashboard.department_navigation.gateways",
|
||||
"stripe": "@:superuser_dashboard.department_navigation.stripe",
|
||||
"pricing": "@:superuser_dashboard.department_navigation.pricing",
|
||||
"categories": "@:superuser_dashboard.department_navigation.categories"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_department": "A valid department is required",
|
||||
|
||||
@@ -1384,6 +1384,11 @@
|
||||
"groups": {
|
||||
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
|
||||
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
|
||||
"products": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.products'}",
|
||||
"customers": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.customers'}",
|
||||
"vehicles": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.vehicles'}",
|
||||
"attachments": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.attachments'}",
|
||||
"scanner": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.scanner'}",
|
||||
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
|
||||
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
|
||||
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
|
||||
@@ -1435,6 +1440,66 @@
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
|
||||
@@ -1631,6 +1696,14 @@
|
||||
"subtitle_suffix": "@:{'words.generated.produktsalg'}",
|
||||
"title": "@.capitalize:{'words.generated.produktsalg'}"
|
||||
},
|
||||
"product_targets": {
|
||||
"cancel_aria": "Avbryt mål",
|
||||
"edit_aria": "Rediger målprosent",
|
||||
"input_label": "Målprosent",
|
||||
"save_aria": "Lagre mål",
|
||||
"save_error": "Målet kunne ikke lagres.",
|
||||
"validation_range": "Målprosenten må være mellom 0 og 100."
|
||||
},
|
||||
"reports": {
|
||||
"bookings": {
|
||||
"completed_count": "@.capitalize:{'words.generated.fullført'} @:{'words.generated.telling'}",
|
||||
@@ -3590,6 +3663,13 @@
|
||||
"opening_hours": {
|
||||
"closed": "Stengt"
|
||||
},
|
||||
"pricing": {
|
||||
"custom_pricing_disabled": "Fallback aktiv",
|
||||
"custom_pricing_enabled": "Kun egendefinerte priser",
|
||||
"custom_pricing_missing_price": "Manglende avdelingspriser blir 999999.",
|
||||
"custom_pricing_only": "Ingen fallback-priser",
|
||||
"effective_department_price": "Effektiv avdelingspris"
|
||||
},
|
||||
"search_placeholder": "@.capitalize:{'words.generated.søk'} @:{'words.generated.etter'} @:{'words.generated.avdelingsnavn'}",
|
||||
"select_department": "@.capitalize:{'words.generated.velg'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.avdeling'}",
|
||||
"subtitle": "@.capitalize:{'words.generated.administrer'} @:{'words.generated.avdelinger'}",
|
||||
@@ -4429,6 +4509,7 @@
|
||||
"departments": {
|
||||
"columns": {
|
||||
"archived": "Arkivert",
|
||||
"custom_pricing_only": "@:{'templates.generated.compat.departments.pricing.custom_pricing_only'}",
|
||||
"dimension": "Dimensjon",
|
||||
"economic_department": "@.upper:{'words.generated.e'}-@:{'words.generated.økonomisk'} @:{'words.generated.avdeling'}",
|
||||
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
|
||||
@@ -5081,6 +5162,22 @@
|
||||
"title": "Tillatelser for rolle {id}",
|
||||
"title_with_name": "Tillatelser for {name}"
|
||||
},
|
||||
"templates": {
|
||||
"apply": "Bruk {template}-mal",
|
||||
"confirm_button": "Bruk mal",
|
||||
"confirm_text": "Legg til {count} manglende tillatelser fra {template} til {role}?",
|
||||
"confirm_title": "Bruke tillatelsesmal?",
|
||||
"error_text": "En eller flere tillatelser fra {template} kunne ikke legges til i {role}. Rollelisten ble lastet inn pa nytt.",
|
||||
"error_title": "Kunne ikke bruke mal",
|
||||
"load_error": "Kunne ikke laste maler. Prov igjen",
|
||||
"loading": "Laster maler...",
|
||||
"section": "Begrenset backoffice-maler",
|
||||
"success_text": "La til {count} tillatelser fra {template} i {role}.",
|
||||
"success_title": "Mal brukt",
|
||||
"unavailable": "Ingen maler er tilgjengelige.",
|
||||
"up_to_date_text": "{role} har allerede alle tillatelser i {template}.",
|
||||
"up_to_date_title": "Rollen er oppdatert"
|
||||
},
|
||||
"subtitle": "@.capitalize:{'words.generated.user'} roles"
|
||||
},
|
||||
"self_wash": {
|
||||
@@ -5490,6 +5587,7 @@
|
||||
},
|
||||
"employees": {
|
||||
"create": "@:{'words.generated.opprett'} @:{'words.generated.ny'} medarbeider",
|
||||
"migrate_to_limited_backoffice": "Migrer til begrenset backoffice",
|
||||
"subtitle": "@.capitalize:{'words.generated.her'} @:{'words.generated.kan'} @:{'words.generated.du'} @:{'words.generated.se'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.liste'} @:{'words.generated.over'} @:{'words.generated.alle'} @:{'words.generated.ansatte'} @:{'words.generated.i'} @:{'words.generated.systemet'}"
|
||||
},
|
||||
"orders": {
|
||||
@@ -6101,6 +6199,20 @@
|
||||
},
|
||||
"vehicles": {
|
||||
"add": "@:{'words.generated.lagg'} @:{'words.generated.till'} @:{'words.generated.fordon'}",
|
||||
"add_modal": {
|
||||
"customer_label": "Kunde",
|
||||
"customer_placeholder": "Søk etter kundenavn eller kundenummer",
|
||||
"error": "Kunne ikke legge til kjøretøy.",
|
||||
"no_customer_results": "Ingen kunder funnet",
|
||||
"reference_placeholder": "Valgfri referanse",
|
||||
"registration_placeholder": "Registreringsnummer",
|
||||
"selected_customer": "Valgt kunde",
|
||||
"submit": "Legg til kjøretøy",
|
||||
"title": "Legg til kjøretøy",
|
||||
"type_load_error": "Kunne ikke laste kjøretøytyper.",
|
||||
"type_placeholder": "Velg kjøretøytype",
|
||||
"validation_error": "Velg kunde, registreringsnummer og kjøretøytype."
|
||||
},
|
||||
"brand": "Märke",
|
||||
"color": "Färg",
|
||||
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
|
||||
@@ -6219,12 +6331,12 @@
|
||||
},
|
||||
"roles": {
|
||||
"viewer": {
|
||||
"label": "Viser",
|
||||
"description": "@.capitalize:{'words.generated.kan'} logge inn @:{'words.generated.og'} se data @:{'words.generated.for'} @:{'words.generated.tildelte'} @:{'words.generated.avdelinger'}."
|
||||
"label": "Deaktivert",
|
||||
"description": "Holder den ansatte registrert uten tilgang til ordre-, booking- eller administrasjonsfunksjoner."
|
||||
},
|
||||
"cashier": {
|
||||
"label": "Kasserer",
|
||||
"description": "@.capitalize:{'words.generated.kan'} arbeide med @:{'words.generated.ordre'} @:{'words.generated.og'} ordrelinjer @:{'words.generated.for'} @:{'words.generated.tildelte'} @:{'words.generated.avdelinger'}."
|
||||
"description": "@.capitalize:{'words.generated.kan'} arbeide med @:{'words.generated.ordre'}, ordrelinjer @:{'words.generated.og'} @:{'words.generated.bookinger'} @:{'words.generated.for'} @:{'words.generated.tildelte'} @:{'words.generated.avdelinger'}."
|
||||
},
|
||||
"booking_coordinator": {
|
||||
"label": "Bookingkoordinator",
|
||||
@@ -6259,6 +6371,11 @@
|
||||
"groups": {
|
||||
"account": "Konto",
|
||||
"orders": "Ordrer",
|
||||
"products": "Produkter",
|
||||
"customers": "Kunder",
|
||||
"vehicles": "Kjøretøy",
|
||||
"attachments": "Vedlegg",
|
||||
"scanner": "Skannere",
|
||||
"bookings": "Bookinger",
|
||||
"time_bookings": "Tidsbookinger",
|
||||
"reports": "Rapporter",
|
||||
@@ -6310,6 +6427,66 @@
|
||||
"label": "Belast ordrer",
|
||||
"description": "Kan ta betaling eller belaste en betalingsreservasjon for en ordre."
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "Fullfør ordrer",
|
||||
"description": "Kan markere ordrer som fullført."
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "Se produktkatalog",
|
||||
"description": "Kan laste POS-kategorier, produkter, priser og tillegg."
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "Se produktforslag",
|
||||
"description": "Kan se produktforslag basert på ordrer og kjøretøydata."
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "Søk kunder",
|
||||
"description": "Kan søke og velge kunder for POS-ordrer."
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "Se kundeopplysninger",
|
||||
"description": "Kan laste kundeopplysninger fra et kundenummer."
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "Se kundemerknader",
|
||||
"description": "Kan lese kundemerknader i POS."
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "Legg til kundemerknader",
|
||||
"description": "Kan legge til kundemerknader fra POS."
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "Se kundemarkeringer",
|
||||
"description": "Kan se kundemarkeringer som påvirker ordrekrav og produktregler."
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "Søk kjøretøy",
|
||||
"description": "Kan søke etter registreringsnummer og se kjøretøystatus."
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "Se kjøretøytreff",
|
||||
"description": "Kan se kundetreff for kjøretøy og forslag til ukjente kjøretøy."
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "Se kjøretøyhistorikk",
|
||||
"description": "Kan se nylig ordrehistorikk for et registreringsnummer."
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "Se ordrevedlegg",
|
||||
"description": "Kan liste vedlegg på ordrer."
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "Legg til ordrevedlegg",
|
||||
"description": "Kan laste opp filer og legge ved vaskesertifikater på ordrer."
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "Last ned ordrevedlegg",
|
||||
"description": "Kan åpne og laste ned ordrevedlegg."
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "Se nummerskiltskanninger",
|
||||
"description": "Kan se nylige nummerskiltskanninger og skannere for tildelte avdelinger."
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "Se avdelingsbookinger",
|
||||
"description": "Kan se bookinger for tildelte avdelinger."
|
||||
|
||||
@@ -1434,6 +1434,11 @@
|
||||
"groups": {
|
||||
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
|
||||
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
|
||||
"products": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.products'}",
|
||||
"customers": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.customers'}",
|
||||
"vehicles": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.vehicles'}",
|
||||
"attachments": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.attachments'}",
|
||||
"scanner": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.scanner'}",
|
||||
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
|
||||
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
|
||||
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
|
||||
@@ -1485,6 +1490,66 @@
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
|
||||
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
|
||||
@@ -1681,6 +1746,14 @@
|
||||
"subtitle_suffix": "@:{'words.generated.produktforsaljning'}",
|
||||
"title": "@.capitalize:{'words.generated.produktforsaljning'}"
|
||||
},
|
||||
"product_targets": {
|
||||
"cancel_aria": "Avbryt mål",
|
||||
"edit_aria": "Redigera målprocent",
|
||||
"input_label": "Målprocent",
|
||||
"save_aria": "Spara mål",
|
||||
"save_error": "Målet kunde inte sparas.",
|
||||
"validation_range": "Målprocenten måste vara mellan 0 och 100."
|
||||
},
|
||||
"reports": {
|
||||
"bookings": {
|
||||
"completed_count": "@.capitalize:{'words.generated.antal'} @:{'words.generated.slutforda'}",
|
||||
@@ -3640,6 +3713,13 @@
|
||||
"opening_hours": {
|
||||
"closed": "Stengt"
|
||||
},
|
||||
"pricing": {
|
||||
"custom_pricing_disabled": "Fallback aktiv",
|
||||
"custom_pricing_enabled": "Endast egna priser",
|
||||
"custom_pricing_missing_price": "Saknade avdelningspriser blir 999999.",
|
||||
"custom_pricing_only": "Inga fallback-priser",
|
||||
"effective_department_price": "Effektivt avdelningspris"
|
||||
},
|
||||
"search_placeholder": "@:{'words.generated.sok'} @:{'words.generated.efter'} @:{'words.generated.avdelningsnamn'}",
|
||||
"select_department": "@:{'words.generated.velg'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.avdeling'}",
|
||||
"subtitle": "@:{'words.generated.administrer'} @:{'words.generated.avdelinger'}",
|
||||
@@ -4479,6 +4559,7 @@
|
||||
"departments": {
|
||||
"columns": {
|
||||
"archived": "Arkiverad",
|
||||
"custom_pricing_only": "@:{'templates.generated.compat.departments.pricing.custom_pricing_only'}",
|
||||
"dimension": "Dimension",
|
||||
"economic_department": "@:{'templates.generated.compat.objects.economic_departments.single'}",
|
||||
"latitude": "Latitude",
|
||||
@@ -5131,6 +5212,22 @@
|
||||
"title": "Behorigheter for roll {id}",
|
||||
"title_with_name": "Behorigheter for {name}"
|
||||
},
|
||||
"templates": {
|
||||
"apply": "Anvand mallen {template}",
|
||||
"confirm_button": "Anvand mall",
|
||||
"confirm_text": "Lagg till {count} saknade behorigheter fran {template} till {role}?",
|
||||
"confirm_title": "Anvanda behorighetsmall?",
|
||||
"error_text": "En eller flera behorigheter fran {template} kunde inte laggas till i {role}. Rolllistan laddades om.",
|
||||
"error_title": "Kunde inte anvanda mall",
|
||||
"load_error": "Kunde inte ladda mallar. Forsok igen",
|
||||
"loading": "Laddar mallar...",
|
||||
"section": "Begransade backoffice-mallar",
|
||||
"success_text": "Lade till {count} behorigheter fran {template} i {role}.",
|
||||
"success_title": "Mall anvand",
|
||||
"unavailable": "Inga mallar ar tillgangliga.",
|
||||
"up_to_date_text": "{role} har redan alla behorigheter i {template}.",
|
||||
"up_to_date_title": "Rollen ar uppdaterad"
|
||||
},
|
||||
"subtitle": "@.capitalize:{'words.generated.user'} @:{'words.generated.roles'}"
|
||||
},
|
||||
"self_wash": {
|
||||
@@ -5540,6 +5637,7 @@
|
||||
},
|
||||
"employees": {
|
||||
"create": "@.capitalize:{'words.generated.skapa'} @:{'words.generated.ny'} @:{'words.generated.anstalld'}",
|
||||
"migrate_to_limited_backoffice": "Migrera till begränsad backoffice",
|
||||
"subtitle": "@.capitalize:{'words.generated.har_2'} @:{'words.generated.kan'} @:{'words.generated.du'} @:{'words.generated.se'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.lista'} @:{'words.generated.over'} @:{'words.generated.alla'} @:{'words.generated.anstallda'} @:{'words.generated.i'} @:{'words.generated.systemet'}"
|
||||
},
|
||||
"orders": {
|
||||
@@ -6151,6 +6249,20 @@
|
||||
},
|
||||
"vehicles": {
|
||||
"add": "@:{'words.generated.lagg'} @:{'words.generated.till'} @:{'words.generated.fordon'}",
|
||||
"add_modal": {
|
||||
"customer_label": "Kund",
|
||||
"customer_placeholder": "Sök efter kundnamn eller kundnummer",
|
||||
"error": "Det gick inte att lägga till fordon.",
|
||||
"no_customer_results": "Inga kunder hittades",
|
||||
"reference_placeholder": "Valfri referens",
|
||||
"registration_placeholder": "Registreringsnummer",
|
||||
"selected_customer": "Vald kund",
|
||||
"submit": "Lägg till fordon",
|
||||
"title": "Lägg till fordon",
|
||||
"type_load_error": "Det gick inte att läsa in fordonstyper.",
|
||||
"type_placeholder": "Välj fordonstyp",
|
||||
"validation_error": "Välj kund, registreringsnummer och fordonstyp."
|
||||
},
|
||||
"brand": "Märke",
|
||||
"color": "Färg",
|
||||
"delete_vehicle": "@.capitalize:{'words.generated.ta'} @:{'words.generated.bort'} @:{'words.generated.fordon'}",
|
||||
@@ -6269,12 +6381,12 @@
|
||||
},
|
||||
"roles": {
|
||||
"viewer": {
|
||||
"label": "Visare",
|
||||
"description": "@.capitalize:{'words.generated.kan'} logga in @:{'words.generated.och'} se data @:{'words.generated.for'} @:{'words.generated.tilldelade'} @:{'words.generated.avdelningar'}."
|
||||
"label": "Inaktiverad",
|
||||
"description": "Behåller medarbetaren registrerad utan åtkomst till order-, boknings- eller administrationsfunktioner."
|
||||
},
|
||||
"cashier": {
|
||||
"label": "Kassör",
|
||||
"description": "@.capitalize:{'words.generated.kan'} arbeta med @:{'words.generated.order'} @:{'words.generated.och'} orderrader @:{'words.generated.for'} @:{'words.generated.tilldelade'} @:{'words.generated.avdelningar'}."
|
||||
"description": "@.capitalize:{'words.generated.kan'} arbeta med @:{'words.generated.order'}, orderrader @:{'words.generated.och'} @:{'words.generated.bokningar'} @:{'words.generated.for'} @:{'words.generated.tilldelade'} @:{'words.generated.avdelningar'}."
|
||||
},
|
||||
"booking_coordinator": {
|
||||
"label": "Bokningskoordinator",
|
||||
@@ -6309,6 +6421,11 @@
|
||||
"groups": {
|
||||
"account": "Konto",
|
||||
"orders": "Order",
|
||||
"products": "Produkter",
|
||||
"customers": "Kunder",
|
||||
"vehicles": "Fordon",
|
||||
"attachments": "Bilagor",
|
||||
"scanner": "Skannrar",
|
||||
"bookings": "Bokningar",
|
||||
"time_bookings": "Tidsbokningar",
|
||||
"reports": "Rapporter",
|
||||
@@ -6360,6 +6477,66 @@
|
||||
"label": "Debitera order",
|
||||
"description": "Kan ta betalt eller debitera en betalningsreservation för en order."
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "Slutför ordrar",
|
||||
"description": "Kan markera ordrar som slutförda."
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "Visa produktkatalog",
|
||||
"description": "Kan läsa in POS-kategorier, produkter, priser och tillval."
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "Visa produktförslag",
|
||||
"description": "Kan se produktförslag baserade på ordrar och fordonsdata."
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "Sök kunder",
|
||||
"description": "Kan söka och välja kunder för POS-ordrar."
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "Visa kunduppgifter",
|
||||
"description": "Kan läsa in kunduppgifter från ett kundnummer."
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "Visa kundanteckningar",
|
||||
"description": "Kan läsa kundanteckningar i POS."
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "Lägg till kundanteckningar",
|
||||
"description": "Kan lägga till kundanteckningar från POS."
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "Visa kundmarkeringar",
|
||||
"description": "Kan se kundmarkeringar som påverkar orderkrav och produktregler."
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "Sök fordon",
|
||||
"description": "Kan söka registreringsnummer och se fordonsstatus."
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "Visa fordonsträffar",
|
||||
"description": "Kan se kundträffar för fordon och förslag på okända fordon."
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "Visa fordonshistorik",
|
||||
"description": "Kan se senaste orderhistorik för ett registreringsnummer."
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "Visa orderbilagor",
|
||||
"description": "Kan lista bilagor på ordrar."
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "Lägg till orderbilagor",
|
||||
"description": "Kan ladda upp filer och bifoga tvättcertifikat till ordrar."
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "Ladda ner orderbilagor",
|
||||
"description": "Kan öppna och ladda ner orderbilagor."
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "Visa nummerskyltsavläsningar",
|
||||
"description": "Kan se senaste nummerskyltsavläsningar och skannrar för tilldelade avdelningar."
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "Se avdelningsbokningar",
|
||||
"description": "Kan se bokningar för tilldelade avdelningar."
|
||||
|
||||
@@ -27,6 +27,14 @@
|
||||
"subtitle_suffix": "@:{'terms.glossary.salg'} @:{'terms.glossary.af'} produktet",
|
||||
"title": "Produktsalg"
|
||||
},
|
||||
"product_targets": {
|
||||
"cancel_aria": "Annuller mål",
|
||||
"edit_aria": "Rediger målprocent",
|
||||
"input_label": "Målprocent",
|
||||
"save_aria": "Gem mål",
|
||||
"save_error": "Målet kunne ikke gemmes.",
|
||||
"validation_range": "Målprocenten skal være mellem 0 og 100."
|
||||
},
|
||||
"reports": {
|
||||
"bookings": {
|
||||
"completed_count": "@.capitalize:{'terms.glossary.antal'} @:{'terms.glossary.fuldførte'}",
|
||||
|
||||
@@ -91,6 +91,22 @@
|
||||
"subtitle": "Rolletilladelser",
|
||||
"title": "Tilladelser for rolle {id}",
|
||||
"title_with_name": "Tilladelser for {name}"
|
||||
},
|
||||
"templates": {
|
||||
"apply": "Anvend {template}-skabelon",
|
||||
"confirm_button": "Anvend skabelon",
|
||||
"confirm_text": "Tilfoej {count} manglende tilladelser fra {template} til {role}?",
|
||||
"confirm_title": "Anvend tilladelsesskabelon?",
|
||||
"error_text": "En eller flere tilladelser fra {template} kunne ikke tilfoejes til {role}. Rollelisten blev genindlaest.",
|
||||
"error_title": "Kunne ikke anvende skabelon",
|
||||
"load_error": "Kunne ikke indlaese skabeloner. Proev igen",
|
||||
"loading": "Indlaeser skabeloner...",
|
||||
"section": "Begraenset backoffice-skabeloner",
|
||||
"success_text": "Tilfoejede {count} tilladelser fra {template} til {role}.",
|
||||
"success_title": "Skabelon anvendt",
|
||||
"unavailable": "Ingen skabeloner er tilgaengelige.",
|
||||
"up_to_date_text": "{role} har allerede alle tilladelser i {template}.",
|
||||
"up_to_date_title": "Rollen er opdateret"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
},
|
||||
"employees": {
|
||||
"create": "@:{'terms.glossary.opret'} @:{'terms.glossary.ny'} @:{'terms.glossary.medarbejder'}",
|
||||
"migrate_to_limited_backoffice": "Migrer til begrænset backoffice",
|
||||
"subtitle": "@.capitalize:{'terms.glossary.her'} @:{'terms.glossary.kan'} @:{'terms.glossary.du'} @:{'terms.glossary.se'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.liste'} @:{'terms.glossary.over'} @:{'terms.glossary.alle'} @:{'terms.glossary.medarbejdere'} @:{'terms.glossary.i'} @:{'terms.glossary.systemet'}"
|
||||
},
|
||||
"orders": {
|
||||
|
||||
@@ -28,16 +28,16 @@
|
||||
"title": "@:{'phrases.compat.limited_backoffice.forbidden.title'}",
|
||||
"department": "@:{'phrases.compat.limited_backoffice.forbidden.department'}"
|
||||
},
|
||||
"prices": {
|
||||
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
|
||||
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
|
||||
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
|
||||
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
|
||||
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
|
||||
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
|
||||
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
|
||||
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
|
||||
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
|
||||
"prices": {
|
||||
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
|
||||
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
|
||||
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
|
||||
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
|
||||
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
|
||||
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
|
||||
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
|
||||
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
|
||||
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
|
||||
"setup_required_title": "@:{'phrases.compat.limited_backoffice.prices.setup_required_title'}",
|
||||
"setup_required_message": "@:{'phrases.compat.limited_backoffice.prices.setup_required_message'}",
|
||||
"no_products": "@:{'phrases.compat.limited_backoffice.prices.no_products'}"
|
||||
@@ -126,6 +126,11 @@
|
||||
"groups": {
|
||||
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
|
||||
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
|
||||
"products": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.products'}",
|
||||
"customers": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.customers'}",
|
||||
"vehicles": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.vehicles'}",
|
||||
"attachments": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.attachments'}",
|
||||
"scanner": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.scanner'}",
|
||||
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
|
||||
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
|
||||
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
|
||||
@@ -177,6 +182,66 @@
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
|
||||
@@ -338,12 +403,12 @@
|
||||
},
|
||||
"roles": {
|
||||
"viewer": {
|
||||
"label": "Viser",
|
||||
"description": "@.capitalize:{'terms.glossary.kan'} logge ind @:{'terms.glossary.og'} se data @:{'terms.glossary.for'} @:{'terms.glossary.tildelte'} @:{'terms.glossary.afdelinger'}."
|
||||
"label": "Deaktiveret",
|
||||
"description": "Holder medarbejderen registreret uden adgang til ordre-, booking- eller administrationsfunktioner."
|
||||
},
|
||||
"cashier": {
|
||||
"label": "Kassemedarbejder",
|
||||
"description": "@.capitalize:{'terms.glossary.kan'} arbejde med @:{'terms.glossary.ordrer'} @:{'terms.glossary.og'} ordrelinjer @:{'terms.glossary.for'} @:{'terms.glossary.tildelte'} @:{'terms.glossary.afdelinger'}."
|
||||
"description": "@.capitalize:{'terms.glossary.kan'} arbejde med @:{'terms.glossary.ordrer'}, ordrelinjer @:{'terms.glossary.og'} @:{'terms.glossary.bookinger'} @:{'terms.glossary.for'} @:{'terms.glossary.tildelte'} @:{'terms.glossary.afdelinger'}."
|
||||
},
|
||||
"booking_coordinator": {
|
||||
"label": "Bookingkoordinator",
|
||||
@@ -378,6 +443,11 @@
|
||||
"groups": {
|
||||
"account": "Konto",
|
||||
"orders": "Ordrer",
|
||||
"products": "Produkter",
|
||||
"customers": "Kunder",
|
||||
"vehicles": "Køretøjer",
|
||||
"attachments": "Vedhæftninger",
|
||||
"scanner": "Scannere",
|
||||
"bookings": "Bookinger",
|
||||
"time_bookings": "Tidsbookinger",
|
||||
"reports": "Rapporter",
|
||||
@@ -429,6 +499,66 @@
|
||||
"label": "Opkræv ordrer",
|
||||
"description": "Kan tage betaling eller opkræve en betalingsreservation for en ordre."
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "Afslut ordrer",
|
||||
"description": "Kan markere ordrer som afsluttede."
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "Se produktkatalog",
|
||||
"description": "Kan indlæse POS-kategorier, produkter, priser og tilvalg."
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "Se produktforslag",
|
||||
"description": "Kan se produktforslag baseret på ordrer og køretøjsdata."
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "Søg kunder",
|
||||
"description": "Kan søge og vælge kunder til POS-ordrer."
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "Se kundeoplysninger",
|
||||
"description": "Kan indlæse kundeoplysninger fra et kundenummer."
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "Se kundebemærkninger",
|
||||
"description": "Kan læse kundebemærkninger i POS."
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "Tilføj kundebemærkninger",
|
||||
"description": "Kan tilføje kundebemærkninger fra POS."
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "Se kundemarkeringer",
|
||||
"description": "Kan se kundemarkeringer, der påvirker ordrekrav og produktregler."
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "Søg køretøjer",
|
||||
"description": "Kan søge nummerplader og se køretøjsstatus."
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "Se køretøjsmatch",
|
||||
"description": "Kan se kundematch for køretøjer og forslag til ukendte køretøjer."
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "Se køretøjshistorik",
|
||||
"description": "Kan se seneste ordrehistorik for en nummerplade."
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "Se ordrevedhæftninger",
|
||||
"description": "Kan liste vedhæftninger på ordrer."
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "Tilføj ordrevedhæftninger",
|
||||
"description": "Kan uploade filer og vedhæfte vaskecertifikater til ordrer."
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "Download ordrevedhæftninger",
|
||||
"description": "Kan åbne og downloade ordrevedhæftninger."
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "Se nummerpladescanninger",
|
||||
"description": "Kan se seneste nummerpladescanninger og scannere for tildelte afdelinger."
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "Se afdelingsbookinger",
|
||||
"description": "Kan se bookinger for tildelte afdelinger."
|
||||
|
||||
@@ -27,6 +27,14 @@
|
||||
"subtitle_suffix": "Produktverk?@:{'terms.glossary.ufe'}",
|
||||
"title": "@:{'phrases.compat.admin.daily_report.product_sales.subtitle_suffix'}"
|
||||
},
|
||||
"product_targets": {
|
||||
"cancel_aria": "Ziel abbrechen",
|
||||
"edit_aria": "Zielprozentsatz bearbeiten",
|
||||
"input_label": "Zielprozentsatz",
|
||||
"save_aria": "Ziel speichern",
|
||||
"save_error": "Das Ziel konnte nicht gespeichert werden.",
|
||||
"validation_range": "Der Zielprozentsatz muss zwischen 0 und 100 liegen."
|
||||
},
|
||||
"reports": {
|
||||
"bookings": {
|
||||
"completed_count": "@:{'terms.glossary.abgeschlossene'} @:{'terms.glossary.anzahl'}",
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
"opening_hours": {
|
||||
"closed": "@:{'phrases.compat.global.closed'}"
|
||||
},
|
||||
"pricing": {
|
||||
"custom_pricing_disabled": "Fallback aktiviert",
|
||||
"custom_pricing_enabled": "Nur eigene Preise",
|
||||
"custom_pricing_missing_price": "Fehlende Abteilungspreise werden zu 999999.",
|
||||
"custom_pricing_only": "Keine Fallback-Preise",
|
||||
"effective_department_price": "Effektiver Abteilungspreis"
|
||||
},
|
||||
"search_placeholder": "@.capitalize:{'terms.glossary.nach'} @:{'terms.glossary.abteilungsname'} @:{'terms.glossary.suchen'}",
|
||||
"select_department": "@:{'phrases.compat.nav.select_department'}",
|
||||
"subtitle": "@:{'terms.glossary.abteilungen'} @:{'terms.glossary.verwalten'}",
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
"departments": {
|
||||
"columns": {
|
||||
"archived": "Archiviert",
|
||||
"custom_pricing_only": "@:{'phrases.compat.departments.pricing.custom_pricing_only'}",
|
||||
"dimension": "Abmessung",
|
||||
"economic_department": "@:{'phrases.compat.objects.economic_departments.single'}",
|
||||
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
|
||||
|
||||
@@ -91,6 +91,22 @@
|
||||
"subtitle": "Rollenberechtigungen",
|
||||
"title": "Berechtigungen fuer Rolle {id}",
|
||||
"title_with_name": "Berechtigungen fuer {name}"
|
||||
},
|
||||
"templates": {
|
||||
"apply": "{template}-Vorlage anwenden",
|
||||
"confirm_button": "Vorlage anwenden",
|
||||
"confirm_text": "{count} fehlende Berechtigungen aus {template} zu {role} hinzufuegen?",
|
||||
"confirm_title": "Berechtigungsvorlage anwenden?",
|
||||
"error_text": "Eine oder mehrere Berechtigungen aus {template} konnten {role} nicht hinzugefuegt werden. Die Rollenliste wurde neu geladen.",
|
||||
"error_title": "Vorlage konnte nicht angewendet werden",
|
||||
"load_error": "Vorlagen konnten nicht geladen werden. Erneut versuchen",
|
||||
"loading": "Vorlagen werden geladen...",
|
||||
"section": "Limited-Backoffice-Vorlagen",
|
||||
"success_text": "{count} Berechtigungen aus {template} zu {role} hinzugefuegt.",
|
||||
"success_title": "Vorlage angewendet",
|
||||
"unavailable": "Keine Vorlagen verfuegbar.",
|
||||
"up_to_date_text": "{role} hat bereits alle Berechtigungen in {template}.",
|
||||
"up_to_date_title": "Rolle ist aktuell"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
},
|
||||
"employees": {
|
||||
"create": "@.capitalize:{'terms.glossary.neuen'} @:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.erstellen'}",
|
||||
"migrate_to_limited_backoffice": "Zu eingeschränktem Backoffice migrieren",
|
||||
"subtitle": "@.capitalize:{'terms.glossary.hier'} @:{'terms.glossary.sehen'} @.capitalize:{'terms.glossary.sie'} @:{'terms.glossary.eine'} @:{'terms.glossary.liste'} @:{'terms.glossary.aller'} @:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.im'} @.capitalize:{'terms.glossary.system'}"
|
||||
},
|
||||
"orders": {
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
"compat": {
|
||||
"vehicles": {
|
||||
"add": "@:{'terms.glossary.fahrzeug'} @:{'terms.glossary.hinzuf'}?@:{'terms.glossary.gen'}",
|
||||
"add_modal": {
|
||||
"customer_label": "Kunde",
|
||||
"customer_placeholder": "Nach Kundenname oder Kundennummer suchen",
|
||||
"error": "Fahrzeug konnte nicht hinzugefuegt werden.",
|
||||
"no_customer_results": "Keine Kunden gefunden",
|
||||
"reference_placeholder": "Optionale Referenz",
|
||||
"registration_placeholder": "Kennzeichen",
|
||||
"selected_customer": "Ausgewaehlter Kunde",
|
||||
"submit": "Fahrzeug hinzufuegen",
|
||||
"title": "Fahrzeug hinzufuegen",
|
||||
"type_load_error": "Fahrzeugtypen konnten nicht geladen werden.",
|
||||
"type_placeholder": "Fahrzeugtyp auswaehlen",
|
||||
"validation_error": "Kunde, Kennzeichen und Fahrzeugtyp auswaehlen."
|
||||
},
|
||||
"brand": "@:{'phrases.compat.admin.pos.make'}",
|
||||
"color": "Farbe",
|
||||
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
|
||||
|
||||
@@ -28,16 +28,16 @@
|
||||
"title": "@:{'phrases.compat.limited_backoffice.forbidden.title'}",
|
||||
"department": "@:{'phrases.compat.limited_backoffice.forbidden.department'}"
|
||||
},
|
||||
"prices": {
|
||||
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
|
||||
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
|
||||
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
|
||||
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
|
||||
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
|
||||
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
|
||||
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
|
||||
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
|
||||
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
|
||||
"prices": {
|
||||
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
|
||||
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
|
||||
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
|
||||
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
|
||||
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
|
||||
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
|
||||
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
|
||||
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
|
||||
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
|
||||
"setup_required_title": "@:{'phrases.compat.limited_backoffice.prices.setup_required_title'}",
|
||||
"setup_required_message": "@:{'phrases.compat.limited_backoffice.prices.setup_required_message'}",
|
||||
"no_products": "@:{'phrases.compat.limited_backoffice.prices.no_products'}"
|
||||
@@ -126,6 +126,11 @@
|
||||
"groups": {
|
||||
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
|
||||
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
|
||||
"products": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.products'}",
|
||||
"customers": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.customers'}",
|
||||
"vehicles": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.vehicles'}",
|
||||
"attachments": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.attachments'}",
|
||||
"scanner": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.scanner'}",
|
||||
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
|
||||
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
|
||||
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
|
||||
@@ -177,6 +182,66 @@
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
|
||||
@@ -338,12 +403,12 @@
|
||||
},
|
||||
"roles": {
|
||||
"viewer": {
|
||||
"label": "Betrachter",
|
||||
"description": "@.capitalize:{'terms.glossary.kann'} sich anmelden @:{'terms.glossary.und'} Daten zugewiesener @:{'terms.glossary.abteilungen'} @:{'terms.glossary.anzeigen'}."
|
||||
"label": "Deaktiviert",
|
||||
"description": "Hält den Mitarbeiter registriert, ohne Zugriff auf Auftrags-, Buchungs- oder Verwaltungsfunktionen."
|
||||
},
|
||||
"cashier": {
|
||||
"label": "Kassierer",
|
||||
"description": "@.capitalize:{'terms.glossary.kann'} mit Aufträgen @:{'terms.glossary.und'} Auftragszeilen @:{'terms.glossary.fur'} @:{'terms.glossary.zugewiesene'} @:{'terms.glossary.abteilungen'} arbeiten."
|
||||
"description": "@.capitalize:{'terms.glossary.kann'} mit Aufträgen, Auftragszeilen @:{'terms.glossary.und'} @:{'terms.glossary.buchungen'} @:{'terms.glossary.fur'} @:{'terms.glossary.zugewiesene'} @:{'terms.glossary.abteilungen'} arbeiten."
|
||||
},
|
||||
"booking_coordinator": {
|
||||
"label": "Buchungskoordinator",
|
||||
@@ -378,6 +443,11 @@
|
||||
"groups": {
|
||||
"account": "Konto",
|
||||
"orders": "Aufträge",
|
||||
"products": "Produkte",
|
||||
"customers": "Kunden",
|
||||
"vehicles": "Fahrzeuge",
|
||||
"attachments": "Anhänge",
|
||||
"scanner": "Scanner",
|
||||
"bookings": "Buchungen",
|
||||
"time_bookings": "Zeitbuchungen",
|
||||
"reports": "Berichte",
|
||||
@@ -429,6 +499,66 @@
|
||||
"label": "Aufträge abrechnen",
|
||||
"description": "Kann Zahlungen annehmen oder eine Zahlungsreservierung für einen Auftrag belasten."
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "Aufträge abschließen",
|
||||
"description": "Kann Aufträge als abgeschlossen markieren."
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "Produktkatalog anzeigen",
|
||||
"description": "Kann POS-Kategorien, Produkte, Preise und Zusatzoptionen laden."
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "Produktempfehlungen anzeigen",
|
||||
"description": "Kann Produktempfehlungen auf Basis von Aufträgen und Fahrzeugdaten sehen."
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "Kunden suchen",
|
||||
"description": "Kann Kunden für POS-Aufträge suchen und auswählen."
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "Kundendetails anzeigen",
|
||||
"description": "Kann Kundendetails anhand einer Kundennummer laden."
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "Kundennotizen anzeigen",
|
||||
"description": "Kann Kundennotizen im POS lesen."
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "Kundennotizen hinzufügen",
|
||||
"description": "Kann Kundennotizen im POS hinzufügen."
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "Kundenmarkierungen anzeigen",
|
||||
"description": "Kann Kundenmarkierungen sehen, die Auftragsanforderungen und Produktregeln beeinflussen."
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "Fahrzeuge suchen",
|
||||
"description": "Kann Kennzeichen suchen und den Fahrzeugstatus sehen."
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "Fahrzeugtreffer anzeigen",
|
||||
"description": "Kann Kundentreffer für Fahrzeuge und Vorschläge zu unbekannten Fahrzeugen sehen."
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "Fahrzeughistorie anzeigen",
|
||||
"description": "Kann die letzte Auftragshistorie für ein Kennzeichen sehen."
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "Auftragsanhänge anzeigen",
|
||||
"description": "Kann Anhänge auf Aufträgen auflisten."
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "Auftragsanhänge hinzufügen",
|
||||
"description": "Kann Dateien hochladen und Waschzertifikate an Aufträge anhängen."
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "Auftragsanhänge herunterladen",
|
||||
"description": "Kann Auftragsanhänge öffnen und herunterladen."
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "Kennzeichenscans anzeigen",
|
||||
"description": "Kann aktuelle Kennzeichenscans und Scanner für zugewiesene Abteilungen sehen."
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "Abteilungsbuchungen anzeigen",
|
||||
"description": "Kann Buchungen für zugewiesene Abteilungen sehen."
|
||||
|
||||
@@ -27,6 +27,14 @@
|
||||
"subtitle_suffix": "@:{'terms.glossary.product'} @:{'terms.glossary.sales'}",
|
||||
"title": "@.capitalize:{'terms.glossary.product'} @.capitalize:{'terms.glossary.sales'}"
|
||||
},
|
||||
"product_targets": {
|
||||
"cancel_aria": "Cancel target",
|
||||
"edit_aria": "Edit target percentage",
|
||||
"input_label": "Target percentage",
|
||||
"save_aria": "Save target",
|
||||
"save_error": "The target could not be saved.",
|
||||
"validation_range": "The target percentage must be between 0 and 100."
|
||||
},
|
||||
"reports": {
|
||||
"bookings": {
|
||||
"completed_count": "@.capitalize:{'terms.glossary.completed'} @:{'terms.glossary.count'}",
|
||||
|
||||
@@ -91,6 +91,22 @@
|
||||
"subtitle": "Role permissions",
|
||||
"title": "Role {id} permissions",
|
||||
"title_with_name": "{name} permissions"
|
||||
},
|
||||
"templates": {
|
||||
"apply": "Apply {template} template",
|
||||
"confirm_button": "Apply template",
|
||||
"confirm_text": "Add {count} missing permissions from {template} to {role}?",
|
||||
"confirm_title": "Apply permission template?",
|
||||
"error_text": "One or more permissions from {template} could not be added to {role}. The role list was reloaded.",
|
||||
"error_title": "Could not apply template",
|
||||
"load_error": "Could not load templates. Try again",
|
||||
"loading": "Loading templates...",
|
||||
"section": "Limited backoffice templates",
|
||||
"success_text": "Added {count} permissions from {template} to {role}.",
|
||||
"success_title": "Template applied",
|
||||
"unavailable": "No templates are available.",
|
||||
"up_to_date_text": "{role} already has every permission in {template}.",
|
||||
"up_to_date_title": "Role is up to date"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
},
|
||||
"employees": {
|
||||
"create": "@.capitalize:{'terms.glossary.create'} @:{'terms.glossary.new'} @:{'terms.glossary.employee'}",
|
||||
"migrate_to_limited_backoffice": "Migrate to limited backoffice",
|
||||
"subtitle": "@.capitalize:{'terms.glossary.here'} @:{'terms.glossary.you'} @:{'terms.glossary.can'} @:{'terms.glossary.see'} @:{'terms.glossary.a'} @:{'terms.glossary.list'} @:{'terms.glossary.of'} @:{'terms.glossary.all'} @:{'terms.glossary.employees'} @:{'terms.glossary.in'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.system'}"
|
||||
},
|
||||
"orders": {
|
||||
|
||||
@@ -28,16 +28,16 @@
|
||||
"title": "@:{'phrases.compat.limited_backoffice.forbidden.title'}",
|
||||
"department": "@:{'phrases.compat.limited_backoffice.forbidden.department'}"
|
||||
},
|
||||
"prices": {
|
||||
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
|
||||
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
|
||||
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
|
||||
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
|
||||
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
|
||||
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
|
||||
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
|
||||
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
|
||||
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
|
||||
"prices": {
|
||||
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
|
||||
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
|
||||
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
|
||||
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
|
||||
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
|
||||
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
|
||||
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
|
||||
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
|
||||
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
|
||||
"setup_required_title": "@:{'phrases.compat.limited_backoffice.prices.setup_required_title'}",
|
||||
"setup_required_message": "@:{'phrases.compat.limited_backoffice.prices.setup_required_message'}",
|
||||
"no_products": "@:{'phrases.compat.limited_backoffice.prices.no_products'}"
|
||||
@@ -126,6 +126,11 @@
|
||||
"groups": {
|
||||
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
|
||||
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
|
||||
"products": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.products'}",
|
||||
"customers": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.customers'}",
|
||||
"vehicles": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.vehicles'}",
|
||||
"attachments": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.attachments'}",
|
||||
"scanner": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.scanner'}",
|
||||
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
|
||||
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
|
||||
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
|
||||
@@ -177,6 +182,66 @@
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
|
||||
@@ -338,12 +403,12 @@
|
||||
},
|
||||
"roles": {
|
||||
"viewer": {
|
||||
"label": "Viewer",
|
||||
"description": "@.capitalize:{'terms.glossary.can'} sign @:{'terms.glossary.in'} @:{'terms.glossary.and'} view @:{'terms.glossary.assigned'} @:{'terms.glossary.department'} data."
|
||||
"label": "Deactivated",
|
||||
"description": "Keeps the employee registered without order, booking, or management permissions."
|
||||
},
|
||||
"cashier": {
|
||||
"label": "Cashier",
|
||||
"description": "@.capitalize:{'terms.glossary.can'} work with @:{'terms.glossary.orders'} @:{'terms.glossary.and'} order lines @:{'terms.glossary.for'} @:{'terms.glossary.assigned'} @:{'terms.glossary.departments'}."
|
||||
"description": "@.capitalize:{'terms.glossary.can'} work with @:{'terms.glossary.orders'}, order lines @:{'terms.glossary.and'} @:{'terms.glossary.bookings'} @:{'terms.glossary.for'} @:{'terms.glossary.assigned'} @:{'terms.glossary.departments'}."
|
||||
},
|
||||
"booking_coordinator": {
|
||||
"label": "Booking coordinator",
|
||||
@@ -378,6 +443,11 @@
|
||||
"groups": {
|
||||
"account": "Account",
|
||||
"orders": "Orders",
|
||||
"products": "Products",
|
||||
"customers": "Customers",
|
||||
"vehicles": "Vehicles",
|
||||
"attachments": "Attachments",
|
||||
"scanner": "Scanners",
|
||||
"bookings": "Bookings",
|
||||
"time_bookings": "Time bookings",
|
||||
"reports": "Reports",
|
||||
@@ -429,6 +499,66 @@
|
||||
"label": "Charge orders",
|
||||
"description": "Can take payment or charge a payment intent for an order."
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "Complete orders",
|
||||
"description": "Can mark orders as completed."
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "View product catalog",
|
||||
"description": "Can load POS categories, products, prices, and addons."
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "View product recommendations",
|
||||
"description": "Can see product suggestions based on orders and vehicle data."
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "Search customers",
|
||||
"description": "Can search and select customers for POS orders."
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "View customer details",
|
||||
"description": "Can load customer details from a customer number."
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "View customer notes",
|
||||
"description": "Can read customer notes in POS."
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "Add customer notes",
|
||||
"description": "Can add customer notes from POS."
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "View customer flags",
|
||||
"description": "Can see customer flags that affect order requirements and product rules."
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "Search vehicles",
|
||||
"description": "Can search license plates and see vehicle status."
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "View vehicle matches",
|
||||
"description": "Can see vehicle customer matches and unknown vehicle suggestions."
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "View vehicle history",
|
||||
"description": "Can see recent order history for a license plate."
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "View order attachments",
|
||||
"description": "Can list attachments on orders."
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "Add order attachments",
|
||||
"description": "Can upload files and attach wash certificates to orders."
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "Download order attachments",
|
||||
"description": "Can open and download order attachments."
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "View plate scans",
|
||||
"description": "Can view recent license plate scans and scanners for assigned departments."
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "View department bookings",
|
||||
"description": "Can see bookings for assigned departments."
|
||||
|
||||
@@ -34,6 +34,14 @@
|
||||
"subtitle_suffix": "@:{'phrases.compat.admin.daily_report.product_sales.subtitle_suffix'}",
|
||||
"title": "@:{'phrases.compat.admin.daily_report.product_sales.title'}"
|
||||
},
|
||||
"product_targets": {
|
||||
"cancel_aria": "@:{'phrases.compat.admin.daily_report.product_targets.cancel_aria'}",
|
||||
"edit_aria": "@:{'phrases.compat.admin.daily_report.product_targets.edit_aria'}",
|
||||
"input_label": "@:{'phrases.compat.admin.daily_report.product_targets.input_label'}",
|
||||
"save_aria": "@:{'phrases.compat.admin.daily_report.product_targets.save_aria'}",
|
||||
"save_error": "@:{'phrases.compat.admin.daily_report.product_targets.save_error'}",
|
||||
"validation_range": "@:{'phrases.compat.admin.daily_report.product_targets.validation_range'}"
|
||||
},
|
||||
"reports": {
|
||||
"avg_revenue": {
|
||||
"label": "@:{'phrases.compat.department_reports.avg_revenue_per_wash'}",
|
||||
|
||||
@@ -91,6 +91,22 @@
|
||||
"title": "@:{'phrases.compat.roles.permissions.title'}",
|
||||
"title_with_name": "@:{'phrases.compat.roles.permissions.title_with_name'}"
|
||||
},
|
||||
"templates": {
|
||||
"apply": "@:{'phrases.compat.roles.templates.apply'}",
|
||||
"confirm_button": "@:{'phrases.compat.roles.templates.confirm_button'}",
|
||||
"confirm_text": "@:{'phrases.compat.roles.templates.confirm_text'}",
|
||||
"confirm_title": "@:{'phrases.compat.roles.templates.confirm_title'}",
|
||||
"error_text": "@:{'phrases.compat.roles.templates.error_text'}",
|
||||
"error_title": "@:{'phrases.compat.roles.templates.error_title'}",
|
||||
"load_error": "@:{'phrases.compat.roles.templates.load_error'}",
|
||||
"loading": "@:{'phrases.compat.roles.templates.loading'}",
|
||||
"section": "@:{'phrases.compat.roles.templates.section'}",
|
||||
"success_text": "@:{'phrases.compat.roles.templates.success_text'}",
|
||||
"success_title": "@:{'phrases.compat.roles.templates.success_title'}",
|
||||
"unavailable": "@:{'phrases.compat.roles.templates.unavailable'}",
|
||||
"up_to_date_text": "@:{'phrases.compat.roles.templates.up_to_date_text'}",
|
||||
"up_to_date_title": "@:{'phrases.compat.roles.templates.up_to_date_title'}"
|
||||
},
|
||||
"subtitle": "@:{'phrases.compat.roles.subtitle'}",
|
||||
"title": "@:{'phrases.compat.common.roles'}"
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"overview": "Overview",
|
||||
"modules": "Modules",
|
||||
"branding": "Profile & Branding",
|
||||
"gateways": "Gateways",
|
||||
"stripe": "Stripe",
|
||||
"gateways": "@.capitalize:{'terms.glossary.gateways'}",
|
||||
"stripe": "@:{'terms.glossary.stripe'}",
|
||||
"pricing": "Pricing",
|
||||
"categories": "Categories"
|
||||
},
|
||||
@@ -88,7 +88,7 @@
|
||||
"department_id": "Department ID",
|
||||
"economic_department_id": "Economic department",
|
||||
"branding": "Branding",
|
||||
"created_at": "Created",
|
||||
"created_at": "@:common.created",
|
||||
"updated_at": "Updated"
|
||||
},
|
||||
"hardware": {
|
||||
@@ -104,12 +104,12 @@
|
||||
"quick_links": {
|
||||
"title": "Department tools",
|
||||
"subtitle": "Open the focused setup areas for this department",
|
||||
"modules": "Modules",
|
||||
"branding": "Branding",
|
||||
"gateways": "Gateways",
|
||||
"stripe": "Stripe",
|
||||
"pricing": "Pricing",
|
||||
"categories": "Categories"
|
||||
"modules": "@:superuser_dashboard.department_navigation.modules",
|
||||
"branding": "@:superuser_dashboard.department_overview.profile.branding",
|
||||
"gateways": "@:superuser_dashboard.department_navigation.gateways",
|
||||
"stripe": "@:superuser_dashboard.department_navigation.stripe",
|
||||
"pricing": "@:superuser_dashboard.department_navigation.pricing",
|
||||
"categories": "@:superuser_dashboard.department_navigation.categories"
|
||||
},
|
||||
"errors": {
|
||||
"invalid_department": "A valid department is required",
|
||||
|
||||
@@ -27,6 +27,14 @@
|
||||
"subtitle_suffix": "@:{'terms.glossary.produktsalg'}",
|
||||
"title": "@.capitalize:{'terms.glossary.produktsalg'}"
|
||||
},
|
||||
"product_targets": {
|
||||
"cancel_aria": "Avbryt mål",
|
||||
"edit_aria": "Rediger målprosent",
|
||||
"input_label": "Målprosent",
|
||||
"save_aria": "Lagre mål",
|
||||
"save_error": "Målet kunne ikke lagres.",
|
||||
"validation_range": "Målprosenten må være mellom 0 og 100."
|
||||
},
|
||||
"reports": {
|
||||
"bookings": {
|
||||
"completed_count": "@.capitalize:{'terms.glossary.fullført'} @:{'terms.glossary.telling'}",
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
"opening_hours": {
|
||||
"closed": "Stengt"
|
||||
},
|
||||
"pricing": {
|
||||
"custom_pricing_disabled": "Fallback aktiv",
|
||||
"custom_pricing_enabled": "Kun egendefinerte priser",
|
||||
"custom_pricing_missing_price": "Manglende avdelingspriser blir 999999.",
|
||||
"custom_pricing_only": "Ingen fallback-priser",
|
||||
"effective_department_price": "Effektiv avdelingspris"
|
||||
},
|
||||
"search_placeholder": "@.capitalize:{'terms.glossary.søk'} @:{'terms.glossary.etter'} @:{'terms.glossary.avdelingsnavn'}",
|
||||
"select_department": "@.capitalize:{'terms.glossary.velg'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.avdeling'}",
|
||||
"subtitle": "@.capitalize:{'terms.glossary.administrer'} @:{'terms.glossary.avdelinger'}",
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
"departments": {
|
||||
"columns": {
|
||||
"archived": "Arkivert",
|
||||
"custom_pricing_only": "@:{'phrases.compat.departments.pricing.custom_pricing_only'}",
|
||||
"dimension": "Dimensjon",
|
||||
"economic_department": "@.upper:{'terms.glossary.e'}-@:{'terms.glossary.økonomisk'} @:{'terms.glossary.avdeling'}",
|
||||
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
|
||||
|
||||
@@ -91,6 +91,22 @@
|
||||
"subtitle": "Rolletillatelser",
|
||||
"title": "Tillatelser for rolle {id}",
|
||||
"title_with_name": "Tillatelser for {name}"
|
||||
},
|
||||
"templates": {
|
||||
"apply": "Bruk {template}-mal",
|
||||
"confirm_button": "Bruk mal",
|
||||
"confirm_text": "Legg til {count} manglende tillatelser fra {template} til {role}?",
|
||||
"confirm_title": "Bruke tillatelsesmal?",
|
||||
"error_text": "En eller flere tillatelser fra {template} kunne ikke legges til i {role}. Rollelisten ble lastet inn pa nytt.",
|
||||
"error_title": "Kunne ikke bruke mal",
|
||||
"load_error": "Kunne ikke laste maler. Prov igjen",
|
||||
"loading": "Laster maler...",
|
||||
"section": "Begrenset backoffice-maler",
|
||||
"success_text": "La til {count} tillatelser fra {template} i {role}.",
|
||||
"success_title": "Mal brukt",
|
||||
"unavailable": "Ingen maler er tilgjengelige.",
|
||||
"up_to_date_text": "{role} har allerede alle tillatelser i {template}.",
|
||||
"up_to_date_title": "Rollen er oppdatert"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
},
|
||||
"employees": {
|
||||
"create": "@:{'terms.glossary.opprett'} @:{'terms.glossary.ny'} medarbeider",
|
||||
"migrate_to_limited_backoffice": "Migrer til begrenset backoffice",
|
||||
"subtitle": "@.capitalize:{'terms.glossary.her'} @:{'terms.glossary.kan'} @:{'terms.glossary.du'} @:{'terms.glossary.se'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.liste'} @:{'terms.glossary.over'} @:{'terms.glossary.alle'} @:{'terms.glossary.ansatte'} @:{'terms.glossary.i'} @:{'terms.glossary.systemet'}"
|
||||
},
|
||||
"orders": {
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
"compat": {
|
||||
"vehicles": {
|
||||
"add": "@:{'terms.glossary.lagg'} @:{'terms.glossary.till'} @:{'terms.glossary.fordon'}",
|
||||
"add_modal": {
|
||||
"customer_label": "Kunde",
|
||||
"customer_placeholder": "Søk etter kundenavn eller kundenummer",
|
||||
"error": "Kunne ikke legge til kjøretøy.",
|
||||
"no_customer_results": "Ingen kunder funnet",
|
||||
"reference_placeholder": "Valgfri referanse",
|
||||
"registration_placeholder": "Registreringsnummer",
|
||||
"selected_customer": "Valgt kunde",
|
||||
"submit": "Legg til kjøretøy",
|
||||
"title": "Legg til kjøretøy",
|
||||
"type_load_error": "Kunne ikke laste kjøretøytyper.",
|
||||
"type_placeholder": "Velg kjøretøytype",
|
||||
"validation_error": "Velg kunde, registreringsnummer og kjøretøytype."
|
||||
},
|
||||
"brand": "Märke",
|
||||
"color": "Färg",
|
||||
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
|
||||
|
||||
@@ -28,16 +28,16 @@
|
||||
"title": "@:{'phrases.compat.limited_backoffice.forbidden.title'}",
|
||||
"department": "@:{'phrases.compat.limited_backoffice.forbidden.department'}"
|
||||
},
|
||||
"prices": {
|
||||
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
|
||||
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
|
||||
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
|
||||
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
|
||||
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
|
||||
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
|
||||
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
|
||||
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
|
||||
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
|
||||
"prices": {
|
||||
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
|
||||
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
|
||||
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
|
||||
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
|
||||
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
|
||||
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
|
||||
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
|
||||
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
|
||||
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
|
||||
"setup_required_title": "@:{'phrases.compat.limited_backoffice.prices.setup_required_title'}",
|
||||
"setup_required_message": "@:{'phrases.compat.limited_backoffice.prices.setup_required_message'}",
|
||||
"no_products": "@:{'phrases.compat.limited_backoffice.prices.no_products'}"
|
||||
@@ -126,6 +126,11 @@
|
||||
"groups": {
|
||||
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
|
||||
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
|
||||
"products": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.products'}",
|
||||
"customers": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.customers'}",
|
||||
"vehicles": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.vehicles'}",
|
||||
"attachments": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.attachments'}",
|
||||
"scanner": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.scanner'}",
|
||||
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
|
||||
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
|
||||
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
|
||||
@@ -177,6 +182,66 @@
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
|
||||
@@ -338,12 +403,12 @@
|
||||
},
|
||||
"roles": {
|
||||
"viewer": {
|
||||
"label": "Viser",
|
||||
"description": "@.capitalize:{'terms.glossary.kan'} logge inn @:{'terms.glossary.og'} se data @:{'terms.glossary.for'} @:{'terms.glossary.tildelte'} @:{'terms.glossary.avdelinger'}."
|
||||
"label": "Deaktivert",
|
||||
"description": "Holder den ansatte registrert uten tilgang til ordre-, booking- eller administrasjonsfunksjoner."
|
||||
},
|
||||
"cashier": {
|
||||
"label": "Kasserer",
|
||||
"description": "@.capitalize:{'terms.glossary.kan'} arbeide med @:{'terms.glossary.ordre'} @:{'terms.glossary.og'} ordrelinjer @:{'terms.glossary.for'} @:{'terms.glossary.tildelte'} @:{'terms.glossary.avdelinger'}."
|
||||
"description": "@.capitalize:{'terms.glossary.kan'} arbeide med @:{'terms.glossary.ordre'}, ordrelinjer @:{'terms.glossary.og'} @:{'terms.glossary.bookinger'} @:{'terms.glossary.for'} @:{'terms.glossary.tildelte'} @:{'terms.glossary.avdelinger'}."
|
||||
},
|
||||
"booking_coordinator": {
|
||||
"label": "Bookingkoordinator",
|
||||
@@ -378,6 +443,11 @@
|
||||
"groups": {
|
||||
"account": "Konto",
|
||||
"orders": "Ordrer",
|
||||
"products": "Produkter",
|
||||
"customers": "Kunder",
|
||||
"vehicles": "Kjøretøy",
|
||||
"attachments": "Vedlegg",
|
||||
"scanner": "Skannere",
|
||||
"bookings": "Bookinger",
|
||||
"time_bookings": "Tidsbookinger",
|
||||
"reports": "Rapporter",
|
||||
@@ -429,6 +499,66 @@
|
||||
"label": "Belast ordrer",
|
||||
"description": "Kan ta betaling eller belaste en betalingsreservasjon for en ordre."
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "Fullfør ordrer",
|
||||
"description": "Kan markere ordrer som fullført."
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "Se produktkatalog",
|
||||
"description": "Kan laste POS-kategorier, produkter, priser og tillegg."
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "Se produktforslag",
|
||||
"description": "Kan se produktforslag basert på ordrer og kjøretøydata."
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "Søk kunder",
|
||||
"description": "Kan søke og velge kunder for POS-ordrer."
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "Se kundeopplysninger",
|
||||
"description": "Kan laste kundeopplysninger fra et kundenummer."
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "Se kundemerknader",
|
||||
"description": "Kan lese kundemerknader i POS."
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "Legg til kundemerknader",
|
||||
"description": "Kan legge til kundemerknader fra POS."
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "Se kundemarkeringer",
|
||||
"description": "Kan se kundemarkeringer som påvirker ordrekrav og produktregler."
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "Søk kjøretøy",
|
||||
"description": "Kan søke etter registreringsnummer og se kjøretøystatus."
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "Se kjøretøytreff",
|
||||
"description": "Kan se kundetreff for kjøretøy og forslag til ukjente kjøretøy."
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "Se kjøretøyhistorikk",
|
||||
"description": "Kan se nylig ordrehistorikk for et registreringsnummer."
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "Se ordrevedlegg",
|
||||
"description": "Kan liste vedlegg på ordrer."
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "Legg til ordrevedlegg",
|
||||
"description": "Kan laste opp filer og legge ved vaskesertifikater på ordrer."
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "Last ned ordrevedlegg",
|
||||
"description": "Kan åpne og laste ned ordrevedlegg."
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "Se nummerskiltskanninger",
|
||||
"description": "Kan se nylige nummerskiltskanninger og skannere for tildelte avdelinger."
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "Se avdelingsbookinger",
|
||||
"description": "Kan se bookinger for tildelte avdelinger."
|
||||
|
||||
@@ -27,6 +27,14 @@
|
||||
"subtitle_suffix": "@:{'terms.glossary.produktforsaljning'}",
|
||||
"title": "@.capitalize:{'terms.glossary.produktforsaljning'}"
|
||||
},
|
||||
"product_targets": {
|
||||
"cancel_aria": "Avbryt mål",
|
||||
"edit_aria": "Redigera målprocent",
|
||||
"input_label": "Målprocent",
|
||||
"save_aria": "Spara mål",
|
||||
"save_error": "Målet kunde inte sparas.",
|
||||
"validation_range": "Målprocenten måste vara mellan 0 och 100."
|
||||
},
|
||||
"reports": {
|
||||
"bookings": {
|
||||
"completed_count": "@.capitalize:{'terms.glossary.antal'} @:{'terms.glossary.slutforda'}",
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
"opening_hours": {
|
||||
"closed": "Stengt"
|
||||
},
|
||||
"pricing": {
|
||||
"custom_pricing_disabled": "Fallback aktiv",
|
||||
"custom_pricing_enabled": "Endast egna priser",
|
||||
"custom_pricing_missing_price": "Saknade avdelningspriser blir 999999.",
|
||||
"custom_pricing_only": "Inga fallback-priser",
|
||||
"effective_department_price": "Effektivt avdelningspris"
|
||||
},
|
||||
"search_placeholder": "@:{'terms.glossary.sok'} @:{'terms.glossary.efter'} @:{'terms.glossary.avdelningsnamn'}",
|
||||
"select_department": "@:{'terms.glossary.velg'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.avdeling'}",
|
||||
"subtitle": "@:{'terms.glossary.administrer'} @:{'terms.glossary.avdelinger'}",
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
"departments": {
|
||||
"columns": {
|
||||
"archived": "Arkiverad",
|
||||
"custom_pricing_only": "@:{'phrases.compat.departments.pricing.custom_pricing_only'}",
|
||||
"dimension": "Dimension",
|
||||
"economic_department": "@:{'phrases.compat.objects.economic_departments.single'}",
|
||||
"latitude": "Latitude",
|
||||
|
||||
@@ -91,6 +91,22 @@
|
||||
"subtitle": "Rollbehorigheter",
|
||||
"title": "Behorigheter for roll {id}",
|
||||
"title_with_name": "Behorigheter for {name}"
|
||||
},
|
||||
"templates": {
|
||||
"apply": "Anvand mallen {template}",
|
||||
"confirm_button": "Anvand mall",
|
||||
"confirm_text": "Lagg till {count} saknade behorigheter fran {template} till {role}?",
|
||||
"confirm_title": "Anvanda behorighetsmall?",
|
||||
"error_text": "En eller flera behorigheter fran {template} kunde inte laggas till i {role}. Rolllistan laddades om.",
|
||||
"error_title": "Kunde inte anvanda mall",
|
||||
"load_error": "Kunde inte ladda mallar. Forsok igen",
|
||||
"loading": "Laddar mallar...",
|
||||
"section": "Begransade backoffice-mallar",
|
||||
"success_text": "Lade till {count} behorigheter fran {template} i {role}.",
|
||||
"success_title": "Mall anvand",
|
||||
"unavailable": "Inga mallar ar tillgangliga.",
|
||||
"up_to_date_text": "{role} har redan alla behorigheter i {template}.",
|
||||
"up_to_date_title": "Rollen ar uppdaterad"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
},
|
||||
"employees": {
|
||||
"create": "@.capitalize:{'terms.glossary.skapa'} @:{'terms.glossary.ny'} @:{'terms.glossary.anstalld'}",
|
||||
"migrate_to_limited_backoffice": "Migrera till begränsad backoffice",
|
||||
"subtitle": "@.capitalize:{'terms.glossary.har_2'} @:{'terms.glossary.kan'} @:{'terms.glossary.du'} @:{'terms.glossary.se'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.lista'} @:{'terms.glossary.over'} @:{'terms.glossary.alla'} @:{'terms.glossary.anstallda'} @:{'terms.glossary.i'} @:{'terms.glossary.systemet'}"
|
||||
},
|
||||
"orders": {
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
"compat": {
|
||||
"vehicles": {
|
||||
"add": "@:{'terms.glossary.lagg'} @:{'terms.glossary.till'} @:{'terms.glossary.fordon'}",
|
||||
"add_modal": {
|
||||
"customer_label": "Kund",
|
||||
"customer_placeholder": "Sök efter kundnamn eller kundnummer",
|
||||
"error": "Det gick inte att lägga till fordon.",
|
||||
"no_customer_results": "Inga kunder hittades",
|
||||
"reference_placeholder": "Valfri referens",
|
||||
"registration_placeholder": "Registreringsnummer",
|
||||
"selected_customer": "Vald kund",
|
||||
"submit": "Lägg till fordon",
|
||||
"title": "Lägg till fordon",
|
||||
"type_load_error": "Det gick inte att läsa in fordonstyper.",
|
||||
"type_placeholder": "Välj fordonstyp",
|
||||
"validation_error": "Välj kund, registreringsnummer och fordonstyp."
|
||||
},
|
||||
"brand": "Märke",
|
||||
"color": "Färg",
|
||||
"delete_vehicle": "@.capitalize:{'terms.glossary.ta'} @:{'terms.glossary.bort'} @:{'terms.glossary.fordon'}",
|
||||
|
||||
@@ -28,16 +28,16 @@
|
||||
"title": "@:{'phrases.compat.limited_backoffice.forbidden.title'}",
|
||||
"department": "@:{'phrases.compat.limited_backoffice.forbidden.department'}"
|
||||
},
|
||||
"prices": {
|
||||
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
|
||||
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
|
||||
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
|
||||
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
|
||||
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
|
||||
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
|
||||
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
|
||||
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
|
||||
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
|
||||
"prices": {
|
||||
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
|
||||
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
|
||||
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
|
||||
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
|
||||
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
|
||||
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
|
||||
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
|
||||
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
|
||||
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
|
||||
"setup_required_title": "@:{'phrases.compat.limited_backoffice.prices.setup_required_title'}",
|
||||
"setup_required_message": "@:{'phrases.compat.limited_backoffice.prices.setup_required_message'}",
|
||||
"no_products": "@:{'phrases.compat.limited_backoffice.prices.no_products'}"
|
||||
@@ -126,6 +126,11 @@
|
||||
"groups": {
|
||||
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
|
||||
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
|
||||
"products": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.products'}",
|
||||
"customers": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.customers'}",
|
||||
"vehicles": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.vehicles'}",
|
||||
"attachments": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.attachments'}",
|
||||
"scanner": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.scanner'}",
|
||||
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
|
||||
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
|
||||
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
|
||||
@@ -177,6 +182,66 @@
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
|
||||
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
|
||||
@@ -338,12 +403,12 @@
|
||||
},
|
||||
"roles": {
|
||||
"viewer": {
|
||||
"label": "Visare",
|
||||
"description": "@.capitalize:{'terms.glossary.kan'} logga in @:{'terms.glossary.och'} se data @:{'terms.glossary.for'} @:{'terms.glossary.tilldelade'} @:{'terms.glossary.avdelningar'}."
|
||||
"label": "Inaktiverad",
|
||||
"description": "Behåller medarbetaren registrerad utan åtkomst till order-, boknings- eller administrationsfunktioner."
|
||||
},
|
||||
"cashier": {
|
||||
"label": "Kassör",
|
||||
"description": "@.capitalize:{'terms.glossary.kan'} arbeta med @:{'terms.glossary.order'} @:{'terms.glossary.och'} orderrader @:{'terms.glossary.for'} @:{'terms.glossary.tilldelade'} @:{'terms.glossary.avdelningar'}."
|
||||
"description": "@.capitalize:{'terms.glossary.kan'} arbeta med @:{'terms.glossary.order'}, orderrader @:{'terms.glossary.och'} @:{'terms.glossary.bokningar'} @:{'terms.glossary.for'} @:{'terms.glossary.tilldelade'} @:{'terms.glossary.avdelningar'}."
|
||||
},
|
||||
"booking_coordinator": {
|
||||
"label": "Bokningskoordinator",
|
||||
@@ -378,6 +443,11 @@
|
||||
"groups": {
|
||||
"account": "Konto",
|
||||
"orders": "Order",
|
||||
"products": "Produkter",
|
||||
"customers": "Kunder",
|
||||
"vehicles": "Fordon",
|
||||
"attachments": "Bilagor",
|
||||
"scanner": "Skannrar",
|
||||
"bookings": "Bokningar",
|
||||
"time_bookings": "Tidsbokningar",
|
||||
"reports": "Rapporter",
|
||||
@@ -429,6 +499,66 @@
|
||||
"label": "Debitera order",
|
||||
"description": "Kan ta betalt eller debitera en betalningsreservation för en order."
|
||||
},
|
||||
"complete_orders": {
|
||||
"label": "Slutför ordrar",
|
||||
"description": "Kan markera ordrar som slutförda."
|
||||
},
|
||||
"view_product_catalog": {
|
||||
"label": "Visa produktkatalog",
|
||||
"description": "Kan läsa in POS-kategorier, produkter, priser och tillval."
|
||||
},
|
||||
"view_product_recommendations": {
|
||||
"label": "Visa produktförslag",
|
||||
"description": "Kan se produktförslag baserade på ordrar och fordonsdata."
|
||||
},
|
||||
"search_customers": {
|
||||
"label": "Sök kunder",
|
||||
"description": "Kan söka och välja kunder för POS-ordrar."
|
||||
},
|
||||
"view_customer_details": {
|
||||
"label": "Visa kunduppgifter",
|
||||
"description": "Kan läsa in kunduppgifter från ett kundnummer."
|
||||
},
|
||||
"view_customer_notes": {
|
||||
"label": "Visa kundanteckningar",
|
||||
"description": "Kan läsa kundanteckningar i POS."
|
||||
},
|
||||
"add_customer_notes": {
|
||||
"label": "Lägg till kundanteckningar",
|
||||
"description": "Kan lägga till kundanteckningar från POS."
|
||||
},
|
||||
"view_customer_flags": {
|
||||
"label": "Visa kundmarkeringar",
|
||||
"description": "Kan se kundmarkeringar som påverkar orderkrav och produktregler."
|
||||
},
|
||||
"search_vehicles": {
|
||||
"label": "Sök fordon",
|
||||
"description": "Kan söka registreringsnummer och se fordonsstatus."
|
||||
},
|
||||
"view_vehicle_matches": {
|
||||
"label": "Visa fordonsträffar",
|
||||
"description": "Kan se kundträffar för fordon och förslag på okända fordon."
|
||||
},
|
||||
"view_vehicle_history": {
|
||||
"label": "Visa fordonshistorik",
|
||||
"description": "Kan se senaste orderhistorik för ett registreringsnummer."
|
||||
},
|
||||
"view_order_attachments": {
|
||||
"label": "Visa orderbilagor",
|
||||
"description": "Kan lista bilagor på ordrar."
|
||||
},
|
||||
"add_order_attachments": {
|
||||
"label": "Lägg till orderbilagor",
|
||||
"description": "Kan ladda upp filer och bifoga tvättcertifikat till ordrar."
|
||||
},
|
||||
"download_order_attachments": {
|
||||
"label": "Ladda ner orderbilagor",
|
||||
"description": "Kan öppna och ladda ner orderbilagor."
|
||||
},
|
||||
"view_plate_scans": {
|
||||
"label": "Visa nummerskyltsavläsningar",
|
||||
"description": "Kan se senaste nummerskyltsavläsningar och skannrar för tilldelade avdelningar."
|
||||
},
|
||||
"view_department_bookings": {
|
||||
"label": "Se avdelningsbokningar",
|
||||
"description": "Kan se bokningar för tilldelade avdelningar."
|
||||
|
||||
@@ -442,6 +442,12 @@ export const router = createRouter({
|
||||
},
|
||||
{
|
||||
name: 'limitedBackofficeEmployees',
|
||||
path: '/backoffice/departments/:departmentId/employees',
|
||||
component: LimitedBackofficeEmployees,
|
||||
meta: { middleware: authMiddleware, titleKey: 'templates.limited_backoffice.employees.title' }
|
||||
},
|
||||
{
|
||||
name: 'limitedBackofficeEmployeesLegacy',
|
||||
path: '/backoffice/employees',
|
||||
component: LimitedBackofficeEmployees,
|
||||
meta: { middleware: authMiddleware, titleKey: 'templates.limited_backoffice.employees.title' }
|
||||
|
||||
@@ -18,8 +18,10 @@ export const getLimitedBackofficeRoles = () => limitedBackofficeRequest("/roles"
|
||||
export const getLimitedBackofficeEmployees = ({ includeInactive = false } = {}) =>
|
||||
limitedBackofficeRequest("/employees", "GET", { include_inactive: includeInactive ? "true" : "false" });
|
||||
|
||||
export const createLimitedBackofficeEmployee = (payload) =>
|
||||
limitedBackofficeRequest("/employees", "POST", payload);
|
||||
export const createLimitedBackofficeEmployee = (payload) => limitedBackofficeRequest("/employees", "POST", payload);
|
||||
|
||||
export const migrateLimitedBackofficeEmployee = (employeeId, payload) =>
|
||||
limitedBackofficeRequest(`/employees/${encodeURIComponent(String(employeeId))}/migrate`, "POST", payload);
|
||||
|
||||
export const updateLimitedBackofficeEmployee = (employeeId, payload) =>
|
||||
limitedBackofficeRequest(`/employees/${encodeURIComponent(String(employeeId))}`, "PUT", payload);
|
||||
@@ -33,7 +35,4 @@ export const deactivateLimitedBackofficeEmployee = (employeeId) =>
|
||||
export const unwrapLimitedBackofficeResponse = (response) => response?.data?.data ?? response?.data ?? null;
|
||||
|
||||
export const limitedBackofficeErrorMessage = (error, fallback) =>
|
||||
error?.response?.data?.data?.message ||
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
fallback;
|
||||
error?.response?.data?.data?.message || error?.response?.data?.message || error?.message || fallback;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import LimitedBackofficeLayout from "@/views/backoffice/components/LimitedBackofficeLayout.vue";
|
||||
@@ -33,6 +34,14 @@ const loadQRCodeModule = () => {
|
||||
};
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const countryCodeLabels = {
|
||||
denmark: () => t("templates.limited_backoffice.employees.country_codes.denmark"),
|
||||
sweden: () => t("templates.limited_backoffice.employees.country_codes.sweden"),
|
||||
norway: () => t("templates.limited_backoffice.employees.country_codes.norway"),
|
||||
finland: () => t("templates.limited_backoffice.employees.country_codes.finland"),
|
||||
};
|
||||
const departments = ref([]);
|
||||
const roles = ref([]);
|
||||
const employees = ref([]);
|
||||
@@ -60,6 +69,19 @@ const form = ref({
|
||||
department_ids: [],
|
||||
});
|
||||
|
||||
const selectedDepartmentId = computed(() => {
|
||||
const departmentId = Number.parseInt(String(route.params.departmentId ?? ""), 10);
|
||||
return Number.isInteger(departmentId) && departmentId > 0 ? departmentId : null;
|
||||
});
|
||||
|
||||
const selectedDepartment = computed(
|
||||
() => departments.value.find((department) => Number(department.id) === selectedDepartmentId.value) || null
|
||||
);
|
||||
|
||||
const hasForbiddenDepartment = computed(
|
||||
() => selectedDepartmentId.value !== null && departments.value.length > 0 && !selectedDepartment.value
|
||||
);
|
||||
|
||||
const roleMessages = computed(() => ({
|
||||
viewer: {
|
||||
label: t("templates.limited_backoffice.roles.viewer.label"),
|
||||
@@ -90,6 +112,21 @@ const rolePermissionGroupMessages = computed(() => ({
|
||||
orders: {
|
||||
label: t("templates.limited_backoffice.role_permissions.groups.orders"),
|
||||
},
|
||||
products: {
|
||||
label: t("templates.limited_backoffice.role_permissions.groups.products"),
|
||||
},
|
||||
customers: {
|
||||
label: t("templates.limited_backoffice.role_permissions.groups.customers"),
|
||||
},
|
||||
vehicles: {
|
||||
label: t("templates.limited_backoffice.role_permissions.groups.vehicles"),
|
||||
},
|
||||
attachments: {
|
||||
label: t("templates.limited_backoffice.role_permissions.groups.attachments"),
|
||||
},
|
||||
scanner: {
|
||||
label: t("templates.limited_backoffice.role_permissions.groups.scanner"),
|
||||
},
|
||||
bookings: {
|
||||
label: t("templates.limited_backoffice.role_permissions.groups.bookings"),
|
||||
},
|
||||
@@ -132,6 +169,10 @@ const roleCapabilityMessages = computed(() => ({
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.delete_orders.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.delete_orders.description"),
|
||||
},
|
||||
complete_orders: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.complete_orders.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.complete_orders.description"),
|
||||
},
|
||||
view_order_items: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.view_order_items.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.view_order_items.description"),
|
||||
@@ -152,6 +193,64 @@ const roleCapabilityMessages = computed(() => ({
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.charge_orders.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.charge_orders.description"),
|
||||
},
|
||||
view_product_catalog: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.view_product_catalog.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.view_product_catalog.description"),
|
||||
},
|
||||
view_product_recommendations: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label"),
|
||||
description: t(
|
||||
"templates.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description"
|
||||
),
|
||||
},
|
||||
search_customers: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.search_customers.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.search_customers.description"),
|
||||
},
|
||||
view_customer_details: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.view_customer_details.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.view_customer_details.description"),
|
||||
},
|
||||
view_customer_notes: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.view_customer_notes.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.view_customer_notes.description"),
|
||||
},
|
||||
add_customer_notes: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.add_customer_notes.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.add_customer_notes.description"),
|
||||
},
|
||||
view_customer_flags: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.view_customer_flags.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.view_customer_flags.description"),
|
||||
},
|
||||
search_vehicles: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.search_vehicles.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.search_vehicles.description"),
|
||||
},
|
||||
view_vehicle_matches: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description"),
|
||||
},
|
||||
view_vehicle_history: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description"),
|
||||
},
|
||||
view_order_attachments: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.view_order_attachments.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.view_order_attachments.description"),
|
||||
},
|
||||
add_order_attachments: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.add_order_attachments.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.add_order_attachments.description"),
|
||||
},
|
||||
download_order_attachments: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.download_order_attachments.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.download_order_attachments.description"),
|
||||
},
|
||||
view_plate_scans: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.view_plate_scans.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.view_plate_scans.description"),
|
||||
},
|
||||
view_department_bookings: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.view_department_bookings.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.view_department_bookings.description"),
|
||||
@@ -182,7 +281,9 @@ const roleCapabilityMessages = computed(() => ({
|
||||
},
|
||||
create_time_booking_entries: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label"),
|
||||
description: t("templates.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description"),
|
||||
description: t(
|
||||
"templates.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description"
|
||||
),
|
||||
},
|
||||
edit_time_booking_entries: {
|
||||
label: t("templates.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label"),
|
||||
@@ -256,7 +357,7 @@ const countryCodeLabel = (countryCode) => {
|
||||
if (!option) {
|
||||
return "";
|
||||
}
|
||||
return `${option.flag} +${option.value} ${t(`templates.limited_backoffice.employees.country_codes.${option.labelKey}`)}`;
|
||||
return `${option.flag} +${option.value} ${countryCodeLabels[option.labelKey]?.() || ""}`;
|
||||
};
|
||||
|
||||
const formatEmployeePhone = (employee) => {
|
||||
@@ -272,11 +373,23 @@ const sanitizePhone = (event) => {
|
||||
|
||||
const absoluteLoginLink = (loginPath) => new URL(String(loginPath || ""), window.location.origin).toString();
|
||||
|
||||
const visibleEmployees = computed(() => {
|
||||
if (!selectedDepartmentId.value) {
|
||||
return employees.value;
|
||||
}
|
||||
|
||||
return employees.value.filter((employee) =>
|
||||
(employee.departments || []).some((department) => Number(department.id) === selectedDepartmentId.value)
|
||||
);
|
||||
});
|
||||
|
||||
const loadEmployees = async () => {
|
||||
const response = await getLimitedBackofficeEmployees({ includeInactive: includeInactive.value });
|
||||
employees.value = unwrapLimitedBackofficeResponse(response) || [];
|
||||
};
|
||||
|
||||
const defaultDepartmentIds = () => (selectedDepartment.value?.id ? [Number(selectedDepartment.value.id)] : []);
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
errorMessage.value = "";
|
||||
@@ -291,12 +404,21 @@ const loadData = async () => {
|
||||
if (!ROLE_KEYS.includes(form.value.role_key) || !roles.value.some((role) => role.key === form.value.role_key)) {
|
||||
form.value.role_key = roles.value[0]?.key || "viewer";
|
||||
}
|
||||
|
||||
const firstDepartment = departments.value[0] || null;
|
||||
if (!selectedDepartmentId.value && firstDepartment?.id) {
|
||||
await router.replace(`/backoffice/departments/${firstDepartment.id}/employees`);
|
||||
}
|
||||
|
||||
resetForm();
|
||||
if (hasForbiddenDepartment.value) {
|
||||
employees.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
await loadEmployees();
|
||||
} catch (error) {
|
||||
errorMessage.value = limitedBackofficeErrorMessage(
|
||||
error,
|
||||
t("templates.limited_backoffice.errors.load_employees")
|
||||
);
|
||||
errorMessage.value = limitedBackofficeErrorMessage(error, t("templates.limited_backoffice.errors.load_employees"));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -312,7 +434,7 @@ const resetForm = () => {
|
||||
phone: "",
|
||||
password: "",
|
||||
role_key: roles.value[0]?.key || "viewer",
|
||||
department_ids: [],
|
||||
department_ids: defaultDepartmentIds(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -382,10 +504,7 @@ const saveEmployee = async () => {
|
||||
resetForm();
|
||||
await loadEmployees();
|
||||
} catch (error) {
|
||||
errorMessage.value = limitedBackofficeErrorMessage(
|
||||
error,
|
||||
t("templates.limited_backoffice.errors.save_employee")
|
||||
);
|
||||
errorMessage.value = limitedBackofficeErrorMessage(error, t("templates.limited_backoffice.errors.save_employee"));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
@@ -479,16 +598,22 @@ const closeEmployeeLoginLink = () => {
|
||||
loginLinkCopyMessage.value = "";
|
||||
};
|
||||
|
||||
const changeDepartment = async (departmentId) => {
|
||||
await router.push(`/backoffice/departments/${departmentId}/employees`);
|
||||
};
|
||||
|
||||
const reloadEmployees = async () => {
|
||||
if (hasForbiddenDepartment.value) {
|
||||
employees.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
errorMessage.value = "";
|
||||
try {
|
||||
await loadEmployees();
|
||||
} catch (error) {
|
||||
errorMessage.value = limitedBackofficeErrorMessage(
|
||||
error,
|
||||
t("templates.limited_backoffice.errors.load_employees")
|
||||
);
|
||||
errorMessage.value = limitedBackofficeErrorMessage(error, t("templates.limited_backoffice.errors.load_employees"));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -497,12 +622,40 @@ const reloadEmployees = async () => {
|
||||
onMounted(() => {
|
||||
void loadData();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => selectedDepartmentId.value,
|
||||
async (nextDepartmentId, previousDepartmentId) => {
|
||||
if (nextDepartmentId === previousDepartmentId || loading.value || departments.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetForm();
|
||||
await reloadEmployees();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RestrictedPageWrapper :hasPermission="SessionUser.hasPermission('limited_backoffice_access')">
|
||||
<LimitedBackofficeLayout active-tab="employees" :departments="departments">
|
||||
<div class="columns is-variable is-5">
|
||||
<LimitedBackofficeLayout
|
||||
active-tab="employees"
|
||||
:departments="departments"
|
||||
:selected-department-id="selectedDepartmentId"
|
||||
:loading-departments="loading"
|
||||
show-department-switcher
|
||||
@change-department="changeDepartment"
|
||||
>
|
||||
<div
|
||||
v-if="hasForbiddenDepartment"
|
||||
class="notification is-danger is-light"
|
||||
data-testid="limited-employees-forbidden"
|
||||
>
|
||||
<strong>{{ t("templates.limited_backoffice.forbidden.title") }}</strong>
|
||||
<p>{{ t("templates.limited_backoffice.forbidden.department") }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="columns is-variable is-5">
|
||||
<div class="column is-5">
|
||||
<form class="box limited-employee-form" data-testid="limited-employee-form" @submit.prevent="saveEmployee">
|
||||
<h2 class="title is-5">
|
||||
@@ -667,7 +820,11 @@ onMounted(() => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="formAttempted && !isFormValid" class="notification is-danger is-light" data-testid="limited-employee-validation">
|
||||
<div
|
||||
v-if="formAttempted && !isFormValid"
|
||||
class="notification is-danger is-light"
|
||||
data-testid="limited-employee-validation"
|
||||
>
|
||||
{{ t("templates.limited_backoffice.employees.validation") }}
|
||||
</div>
|
||||
|
||||
@@ -726,12 +883,19 @@ onMounted(() => {
|
||||
{{ successMessage }}
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && employees.length === 0" class="notification is-light" data-testid="limited-employees-empty">
|
||||
<div
|
||||
v-if="!loading && visibleEmployees.length === 0"
|
||||
class="notification is-light"
|
||||
data-testid="limited-employees-empty"
|
||||
>
|
||||
{{ t("templates.limited_backoffice.employees.empty") }}
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && employees.length > 0" class="table-container">
|
||||
<table class="table is-fullwidth is-hoverable limited-employees-table" data-testid="limited-employees-table">
|
||||
<div v-if="!loading && visibleEmployees.length > 0" class="table-container">
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable limited-employees-table"
|
||||
data-testid="limited-employees-table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t("templates.limited_backoffice.employees.user_id") }}</th>
|
||||
@@ -739,11 +903,17 @@ onMounted(() => {
|
||||
<th>{{ t("templates.limited_backoffice.employees.role") }}</th>
|
||||
<th>{{ t("templates.limited_backoffice.employees.departments") }}</th>
|
||||
<th>{{ t("templates.limited_backoffice.employees.status") }}</th>
|
||||
<th class="limited-employees-table__actions">{{ t("templates.limited_backoffice.employees.actions") }}</th>
|
||||
<th class="limited-employees-table__actions">
|
||||
{{ t("templates.limited_backoffice.employees.actions") }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="employee in employees" :key="employee.id" :data-testid="`limited-employee-row-${employee.id}`">
|
||||
<tr
|
||||
v-for="employee in visibleEmployees"
|
||||
:key="employee.id"
|
||||
:data-testid="`limited-employee-row-${employee.id}`"
|
||||
>
|
||||
<td>
|
||||
<span class="tag is-light" :data-testid="`limited-employee-user-id-${employee.id}`">
|
||||
{{ employee.user_id || employee.id }}
|
||||
@@ -765,7 +935,11 @@ onMounted(() => {
|
||||
<p class="has-text-grey is-size-7">{{ roleDescription(employee.role) }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<span v-for="department in employee.departments" :key="department.id" class="tag is-light mr-1 mb-1">
|
||||
<span
|
||||
v-for="department in employee.departments"
|
||||
:key="department.id"
|
||||
class="tag is-light mr-1 mb-1"
|
||||
>
|
||||
{{ department.name }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -17,6 +17,10 @@ const departments = ref([]);
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref("");
|
||||
|
||||
const changeDepartment = async (departmentId) => {
|
||||
await router.push(`/backoffice/departments/${departmentId}/prices`);
|
||||
};
|
||||
|
||||
const loadDepartments = async () => {
|
||||
loading.value = true;
|
||||
errorMessage.value = "";
|
||||
@@ -38,10 +42,6 @@ const loadDepartments = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const changeDepartment = async (departmentId) => {
|
||||
await router.push(`/backoffice/departments/${departmentId}/prices`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
void loadDepartments();
|
||||
});
|
||||
|
||||
@@ -35,6 +35,12 @@ const pricesRoute = computed(() =>
|
||||
: "/backoffice"
|
||||
);
|
||||
|
||||
const employeesRoute = computed(() =>
|
||||
props.selectedDepartmentId
|
||||
? `/backoffice/departments/${encodeURIComponent(String(props.selectedDepartmentId))}/employees`
|
||||
: "/backoffice/employees"
|
||||
);
|
||||
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
key: "prices",
|
||||
@@ -44,7 +50,7 @@ const tabs = computed(() => [
|
||||
{
|
||||
key: "employees",
|
||||
label: t("templates.limited_backoffice.nav.employees"),
|
||||
to: "/backoffice/employees",
|
||||
to: employeesRoute.value,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -62,8 +68,37 @@ const selectDepartment = (event) => {
|
||||
<section class="section limited-backoffice" data-testid="limited-backoffice">
|
||||
<PageTitle
|
||||
:title="t('templates.limited_backoffice.title')"
|
||||
:subtitle="t('templates.limited_backoffice.subtitle')"
|
||||
/>
|
||||
>
|
||||
<template #buttons>
|
||||
<div v-if="props.showDepartmentSwitcher" class="field limited-backoffice__department-field">
|
||||
<label class="label is-sr-only" for="limited-backoffice-department">
|
||||
{{ t("templates.limited_backoffice.department") }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select
|
||||
id="limited-backoffice-department"
|
||||
:value="props.selectedDepartmentId || ''"
|
||||
:disabled="props.loadingDepartments || props.departments.length === 0"
|
||||
data-testid="limited-backoffice-department-select"
|
||||
@change="selectDepartment"
|
||||
>
|
||||
<option value="" disabled>
|
||||
{{ t("templates.limited_backoffice.select_department") }}
|
||||
</option>
|
||||
<option v-for="department in props.departments" :key="department.id" :value="department.id">
|
||||
{{ department.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p class="is-size-6 limited-backoffice__subtitle">
|
||||
{{ t("templates.limited_backoffice.subtitle") }}
|
||||
</p>
|
||||
</PageTitle>
|
||||
|
||||
<div class="limited-backoffice__toolbar">
|
||||
<div class="tabs is-toggle is-small limited-backoffice__tabs" data-testid="limited-backoffice-tabs">
|
||||
@@ -75,30 +110,6 @@ const selectDepartment = (event) => {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="props.showDepartmentSwitcher" class="field limited-backoffice__department-field">
|
||||
<label class="label" for="limited-backoffice-department">
|
||||
{{ t("templates.limited_backoffice.department") }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
id="limited-backoffice-department"
|
||||
:value="props.selectedDepartmentId || ''"
|
||||
:disabled="props.loadingDepartments || props.departments.length === 0"
|
||||
data-testid="limited-backoffice-department-select"
|
||||
@change="selectDepartment"
|
||||
>
|
||||
<option value="" disabled>
|
||||
{{ t("templates.limited_backoffice.select_department") }}
|
||||
</option>
|
||||
<option v-for="department in props.departments" :key="department.id" :value="department.id">
|
||||
{{ department.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
@@ -112,10 +123,8 @@ const selectDepartment = (event) => {
|
||||
|
||||
.limited-backoffice__toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
@@ -123,8 +132,12 @@ const selectDepartment = (event) => {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.limited-backoffice__subtitle {
|
||||
margin-top: -0.75rem;
|
||||
}
|
||||
|
||||
.limited-backoffice__department-field {
|
||||
min-width: min(100%, 260px);
|
||||
min-width: min(42vw, 280px);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -138,11 +151,18 @@ const selectDepartment = (event) => {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.limited-backoffice__tabs,
|
||||
.limited-backoffice__department-field {
|
||||
.limited-backoffice__tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.limited-backoffice__department-field {
|
||||
min-width: 9rem;
|
||||
}
|
||||
|
||||
.limited-backoffice__subtitle {
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
|
||||
.limited-backoffice__tabs :deep(ul) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
|
||||
import SubuserGrantSelector from "@/components/session/subuser/SubuserGrantSelector.vue";
|
||||
import {ref, onMounted, nextTick, computed, onUnmounted} from "vue";
|
||||
import {ref, onMounted, nextTick, computed, onUnmounted, watch} from "vue";
|
||||
import { IS_DEV } from '@/config.js';
|
||||
import {BLoading} from "buefy";
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
|
||||
/**
|
||||
* Subuser grant selection gate
|
||||
@@ -76,6 +77,59 @@ const canShowVehicleShortcut = computed(() => SessionUser.canAccessCustomerFeatu
|
||||
const canShowOrderShortcut = computed(() => SessionUser.canAccessCustomerFeature("orders", "list"));
|
||||
const canShowSelfServeShortcut = computed(() => SessionUser.canAccessCustomerFeature("selfserve", "list"));
|
||||
const canShowInvoiceShortcut = computed(() => canShowClassicCustomerShortcut());
|
||||
const hasInvoicesAvailable = ref(false);
|
||||
const isCheckingInvoiceAvailability = ref(true);
|
||||
const canDownloadInvoices = computed(() => canShowInvoiceShortcut.value && hasInvoicesAvailable.value && !isCheckingInvoiceAvailability.value);
|
||||
|
||||
const responseHasInvoices = (response: any) => {
|
||||
const total = Number(response?.data?.meta?.pagination?.total);
|
||||
if (Number.isFinite(total) && total > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Array.isArray(response?.data?.data) && response.data.data.length > 0;
|
||||
};
|
||||
|
||||
const resetInvoiceAvailability = () => {
|
||||
hasInvoicesAvailable.value = false;
|
||||
isCheckingInvoiceAvailability.value = false;
|
||||
};
|
||||
|
||||
const loadInvoiceAvailability = async () => {
|
||||
if (!canShowInvoiceShortcut.value) {
|
||||
resetInvoiceAvailability();
|
||||
return;
|
||||
}
|
||||
|
||||
isCheckingInvoiceAvailability.value = true;
|
||||
|
||||
try {
|
||||
const response = await authenticatedRequest("/user/invoices", "GET", {
|
||||
page: 1,
|
||||
limit: 1,
|
||||
order: "created_at:desc",
|
||||
});
|
||||
hasInvoicesAvailable.value = responseHasInvoices(response);
|
||||
} catch (_error) {
|
||||
hasInvoicesAvailable.value = false;
|
||||
} finally {
|
||||
isCheckingInvoiceAvailability.value = false;
|
||||
setEqualHeights();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
canShowInvoiceShortcut,
|
||||
(canShow) => {
|
||||
if (canShow) {
|
||||
void loadInvoiceAvailability();
|
||||
return;
|
||||
}
|
||||
|
||||
resetInvoiceAvailability();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const windowInnerWidth = ref(window.innerWidth);
|
||||
const updateWindowInnerWidth = () => {
|
||||
@@ -291,9 +345,26 @@ const isPermissionsLoading = computed(() => {
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="card-footer-item">
|
||||
<router-link to="/user/invoices" class="button is-link is-fullwidth" :class="classes.button" id="download-invoices-button">
|
||||
<router-link
|
||||
v-if="canDownloadInvoices"
|
||||
to="/user/invoices"
|
||||
class="button is-link is-fullwidth"
|
||||
:class="classes.button"
|
||||
id="download-invoices-button"
|
||||
>
|
||||
{{ $t('user_home.download_invoices') }}
|
||||
</router-link>
|
||||
<button
|
||||
v-else
|
||||
class="button is-link is-fullwidth"
|
||||
:class="[classes.button, { 'is-loading': isCheckingInvoiceAvailability }]"
|
||||
id="download-invoices-button"
|
||||
type="button"
|
||||
disabled
|
||||
aria-disabled="true"
|
||||
>
|
||||
{{ $t('user_home.download_invoices') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</WhiteBoxCard>
|
||||
|
||||
@@ -370,8 +370,12 @@ const isInvoiced = () => {
|
||||
return isInvoicedWithEconomic() || isInvoicedWithStripe();
|
||||
};
|
||||
|
||||
const isBookedWithEconomic = () => {
|
||||
return economicModule.value.invoice_id !== null;
|
||||
const hasEconomicInvoiceDownloadContext = () => {
|
||||
const module = economicModule.value || {};
|
||||
return (
|
||||
Object.prototype.hasOwnProperty.call(module, 'invoice_id') ||
|
||||
Object.prototype.hasOwnProperty.call(module, 'invoice_draft_id')
|
||||
);
|
||||
};
|
||||
|
||||
const isCompleted = () => {
|
||||
@@ -1376,7 +1380,11 @@ const isDisplayingReceipt = () => {
|
||||
|
||||
<template #rail-actions>
|
||||
<ButtonsBox class="pos-actions pos-actions--rail">
|
||||
<GetOrderInvoicePDFButton :invoice_id="economicModule.invoice_id" class="is-fullwidth" v-if="isBookedWithEconomic()" />
|
||||
<GetOrderInvoicePDFButton
|
||||
:invoice_id="economicModule.invoice_id"
|
||||
class="is-fullwidth"
|
||||
v-if="hasEconomicInvoiceDownloadContext()"
|
||||
/>
|
||||
<div
|
||||
v-if="isEconomicExportBlocked"
|
||||
class="message is-warning is-light"
|
||||
|
||||
+11
@@ -77,6 +77,15 @@ const normalizeMetricState = (metric, fallbackState = "ready", fallbackMessage =
|
||||
message: metric?.message || fallbackMessage,
|
||||
});
|
||||
|
||||
const normalizeNullableNumber = (value) => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = Number(value);
|
||||
return Number.isFinite(normalized) ? normalized : null;
|
||||
};
|
||||
|
||||
const resetOverviewState = () => {
|
||||
count_transactions.value = 0;
|
||||
count_products_sold.value = 0;
|
||||
@@ -149,6 +158,8 @@ const applyOverviewResponse = (overview) => {
|
||||
value: Number(product.value || 0),
|
||||
out_of: Number(product.out_of || 0),
|
||||
state: product.state || "ready",
|
||||
target_percentage: normalizeNullableNumber(product.target_percentage),
|
||||
target_department_id: normalizeNullableNumber(product.target_department_id),
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
+239
-4
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import { ref, watch, onMounted, computed, nextTick } from 'vue';
|
||||
import {BSkeleton} from "buefy";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -76,6 +76,18 @@ const props = defineProps({
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
canEditTarget: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
targetPercentage: {
|
||||
type: [Number, String],
|
||||
default: null,
|
||||
},
|
||||
saveTargetFunction: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
styleContent: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
@@ -84,6 +96,12 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
const percentageElement = ref<HTMLElement|null>(null);
|
||||
const targetInputElement = ref<HTMLInputElement|null>(null);
|
||||
const isTargetEditorOpen = ref(false);
|
||||
const targetInputValue = ref("");
|
||||
const isSavingTarget = ref(false);
|
||||
const targetError = ref("");
|
||||
|
||||
const getPercentage = () => {
|
||||
if (props.state !== "ready") {
|
||||
return null;
|
||||
@@ -93,6 +111,91 @@ const getPercentage = () => {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatPercentageValue = (value: number) => `${value.toFixed(1)}%`;
|
||||
const normalizedTargetPercentage = computed(() => {
|
||||
if (props.targetPercentage === null || props.targetPercentage === undefined || props.targetPercentage === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numericTarget = Number(props.targetPercentage);
|
||||
return Number.isFinite(numericTarget) ? numericTarget : null;
|
||||
});
|
||||
const hasTargetPercentage = computed(() => normalizedTargetPercentage.value !== null);
|
||||
const formattedTargetPercentage = computed(() => (
|
||||
normalizedTargetPercentage.value === null ? "" : formatPercentageValue(normalizedTargetPercentage.value)
|
||||
));
|
||||
const canUseTargetEditor = computed(() => Boolean(
|
||||
props.canEditTarget && props.saveTargetFunction && getPercentage() !== null && !props.isLoading
|
||||
));
|
||||
const percentageButtonTestid = computed(() => (props.dataTestid ? `${props.dataTestid}-percentage-button` : null));
|
||||
const targetButtonTestid = computed(() => (props.dataTestid ? `${props.dataTestid}-target-button` : null));
|
||||
const targetInputTestid = computed(() => (props.dataTestid ? `${props.dataTestid}-target-input` : null));
|
||||
const targetSaveTestid = computed(() => (props.dataTestid ? `${props.dataTestid}-target-save` : null));
|
||||
const targetCancelTestid = computed(() => (props.dataTestid ? `${props.dataTestid}-target-cancel` : null));
|
||||
const targetErrorTestid = computed(() => (props.dataTestid ? `${props.dataTestid}-target-error` : null));
|
||||
|
||||
const openTargetEditor = async () => {
|
||||
if (!canUseTargetEditor.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
targetInputValue.value = normalizedTargetPercentage.value === null
|
||||
? ""
|
||||
: String(normalizedTargetPercentage.value);
|
||||
targetError.value = "";
|
||||
isTargetEditorOpen.value = true;
|
||||
await nextTick();
|
||||
targetInputElement.value?.focus();
|
||||
autoAdjustedFontSizeTitle.value = determineTitleFontSize(props.title);
|
||||
};
|
||||
|
||||
const closeTargetEditor = () => {
|
||||
targetError.value = "";
|
||||
isTargetEditorOpen.value = false;
|
||||
nextTick(() => {
|
||||
autoAdjustedFontSizeTitle.value = determineTitleFontSize(props.title);
|
||||
});
|
||||
};
|
||||
|
||||
const parseTargetInputValue = () => {
|
||||
const rawValue = String(targetInputValue.value || "").trim().replace(",", ".");
|
||||
if (rawValue === "") {
|
||||
return { valid: true, value: null };
|
||||
}
|
||||
|
||||
const numericTarget = Number(rawValue);
|
||||
if (!Number.isFinite(numericTarget) || numericTarget < 0 || numericTarget > 100) {
|
||||
return { valid: false, value: null };
|
||||
}
|
||||
|
||||
return { valid: true, value: Math.round(numericTarget * 10) / 10 };
|
||||
};
|
||||
|
||||
const saveTarget = async () => {
|
||||
if (!canUseTargetEditor.value || isSavingTarget.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedTarget = parseTargetInputValue();
|
||||
if (!parsedTarget.valid) {
|
||||
targetError.value = t("admin.daily_report.product_targets.validation_range");
|
||||
return;
|
||||
}
|
||||
|
||||
isSavingTarget.value = true;
|
||||
targetError.value = "";
|
||||
try {
|
||||
await props.saveTargetFunction(parsedTarget.value);
|
||||
closeTargetEditor();
|
||||
} catch (error) {
|
||||
console.error("Failed to save daily report product target", error);
|
||||
targetError.value = t("admin.daily_report.product_targets.save_error");
|
||||
} finally {
|
||||
isSavingTarget.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getAveragePercentage = () => {
|
||||
return null; // TODO: Average percentage
|
||||
}
|
||||
@@ -166,6 +269,11 @@ watch(() => props.count, (newVal) => {
|
||||
autoAdjustedFontSize.value = determineFontSize(newVal);
|
||||
determineTitleFontSize(props.title);
|
||||
}, { immediate: true });
|
||||
watch(() => [props.targetPercentage, isTargetEditorOpen.value], () => {
|
||||
nextTick(() => {
|
||||
autoAdjustedFontSizeTitle.value = determineTitleFontSize(props.title);
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
onMounted(() => {
|
||||
autoAdjustedFontSizeTitle.value = determineTitleFontSize(props.title);
|
||||
@@ -180,12 +288,19 @@ const isUnavailable = computed(() => props.state === "unavailable");
|
||||
<template>
|
||||
<div class="card" @click="clickFunction" :data-testid="dataTestid || null">
|
||||
<div class="card-content" :style="styleContent">
|
||||
<div class="is-pulled-right has-text-right is-flex is-flex-direction-column is-4" style="gap: 4px;" v-if="getPercentage() !== null" ref="percentageElement">
|
||||
<button class="button is-small is-info" :class="{
|
||||
<div class="daily-report-count__percentage-stack has-text-right is-flex is-flex-direction-column is-4" v-if="getPercentage() !== null" ref="percentageElement">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-small is-info"
|
||||
:class="{
|
||||
'has-background-success has-text-black': getAveragePercentage() !== null && isAverage(),
|
||||
'has-background-danger has-text-black': getAveragePercentage() !== null && !isAverage(),
|
||||
'is-outlined': getAveragePercentage() === null
|
||||
}">
|
||||
}"
|
||||
:aria-label="canUseTargetEditor ? t('admin.daily_report.product_targets.edit_aria') : null"
|
||||
:data-testid="percentageButtonTestid"
|
||||
@click.stop="openTargetEditor"
|
||||
>
|
||||
<!--<span class="icon is-small">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</span>-->
|
||||
@@ -197,6 +312,75 @@ const isUnavailable = computed(() => props.state === "unavailable");
|
||||
</template>
|
||||
</button>
|
||||
|
||||
<form
|
||||
v-if="isTargetEditorOpen"
|
||||
class="daily-report-count__target-editor"
|
||||
@submit.prevent.stop="saveTarget"
|
||||
@click.stop
|
||||
>
|
||||
<div class="field has-addons daily-report-count__target-editor-controls">
|
||||
<div class="control">
|
||||
<input
|
||||
ref="targetInputElement"
|
||||
v-model="targetInputValue"
|
||||
class="input is-small daily-report-count__target-input"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
inputmode="decimal"
|
||||
:aria-label="t('admin.daily_report.product_targets.input_label')"
|
||||
:data-testid="targetInputTestid"
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button
|
||||
class="button is-small is-success"
|
||||
type="submit"
|
||||
:class="{ 'is-loading': isSavingTarget }"
|
||||
:aria-label="t('admin.daily_report.product_targets.save_aria')"
|
||||
:data-testid="targetSaveTestid"
|
||||
>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button
|
||||
class="button is-small"
|
||||
type="button"
|
||||
:aria-label="t('admin.daily_report.product_targets.cancel_aria')"
|
||||
:data-testid="targetCancelTestid"
|
||||
@click.stop="closeTargetEditor"
|
||||
>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-times"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="targetError"
|
||||
class="help is-danger daily-report-count__target-error"
|
||||
:data-testid="targetErrorTestid"
|
||||
>
|
||||
{{ targetError }}
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<button
|
||||
v-else-if="hasTargetPercentage"
|
||||
type="button"
|
||||
class="button is-small is-info is-outlined daily-report-count__target-button"
|
||||
:aria-label="canUseTargetEditor ? t('admin.daily_report.product_targets.edit_aria') : null"
|
||||
:data-testid="targetButtonTestid"
|
||||
@click.stop="openTargetEditor"
|
||||
>
|
||||
<i class="fas fa-bullseye daily-report-count__target-background-icon" aria-hidden="true"></i>
|
||||
<span class="daily-report-count__target-value">{{ formattedTargetPercentage }}</span>
|
||||
</button>
|
||||
|
||||
<button v-if="getAveragePercentage()" class="button is-small is-info is-outlined">
|
||||
<!--<span class="icon is-small">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
@@ -282,5 +466,56 @@ const isUnavailable = computed(() => props.state === "unavailable");
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.daily-report-count__percentage-stack {
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
position: absolute;
|
||||
right: 1.25rem;
|
||||
top: 1.25rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.daily-report-count__target-button {
|
||||
min-width: 64px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.daily-report-count__target-background-icon {
|
||||
font-size: 1.35rem;
|
||||
left: 50%;
|
||||
opacity: 0.16;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.daily-report-count__target-value {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.daily-report-count__target-editor {
|
||||
width: 154px;
|
||||
}
|
||||
|
||||
.daily-report-count__target-editor-controls {
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.daily-report-count__target-input {
|
||||
max-width: 68px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.daily-report-count__target-error {
|
||||
max-width: 154px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
+66
-1
@@ -1,9 +1,16 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { Colors } from "@/ThemeConfig.vue";
|
||||
import { daily_report_products } from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import {
|
||||
daily_report_products,
|
||||
refreshOverview,
|
||||
selected_department_ids,
|
||||
} from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
|
||||
import DepartmentDashboardDailyReportCount from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportCount.vue";
|
||||
|
||||
const SET_PRODUCT_TARGET_PERMISSION = "set_department_daily_report_product_targets";
|
||||
|
||||
const FALLBACK_PRODUCT_TITLES = {
|
||||
24: "Spot Free (Lastbil)",
|
||||
25: "Fælg flex pr. enhed",
|
||||
@@ -51,6 +58,8 @@ const productTile = computed(() => {
|
||||
out_of: 0,
|
||||
state: "ready",
|
||||
message: null,
|
||||
target_percentage: null,
|
||||
target_department_id: null,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -70,6 +79,59 @@ const resolvedTitle = computed(() => {
|
||||
|
||||
return FALLBACK_PRODUCT_TITLES[props.product_id] || null;
|
||||
});
|
||||
|
||||
const selectedDepartmentId = computed(() => {
|
||||
const selectedDepartmentIds = Array.isArray(selected_department_ids.value)
|
||||
? selected_department_ids.value.map((departmentId) => Number(departmentId)).filter((departmentId) => departmentId > 0)
|
||||
: [];
|
||||
|
||||
return selectedDepartmentIds.length === 1 ? selectedDepartmentIds[0] : null;
|
||||
});
|
||||
|
||||
const targetPercentage = computed(() => {
|
||||
const target = productTile.value?.target_percentage;
|
||||
if (target === null || target === undefined || target === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numericTarget = Number(target);
|
||||
return Number.isFinite(numericTarget) ? numericTarget : null;
|
||||
});
|
||||
|
||||
const canEditProductTarget = computed(() => Boolean(
|
||||
selectedDepartmentId.value !== null && SessionUser.hasPermission(SET_PRODUCT_TARGET_PERMISSION)
|
||||
));
|
||||
|
||||
const saveProductTarget = async (nextTargetPercentage) => {
|
||||
const departmentId = selectedDepartmentId.value;
|
||||
if (departmentId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await SessionUser.objects.department_daily_reports.functions.setProductTarget({
|
||||
department_id: departmentId,
|
||||
product_id: props.product_id,
|
||||
target_percentage: nextTargetPercentage,
|
||||
});
|
||||
const savedTarget = response?.data?.data?.target_percentage ?? nextTargetPercentage;
|
||||
const normalizedTarget = savedTarget === null || savedTarget === undefined || savedTarget === ""
|
||||
? null
|
||||
: Number(savedTarget);
|
||||
|
||||
daily_report_products.value = {
|
||||
...daily_report_products.value,
|
||||
[props.product_id]: {
|
||||
...productTile.value,
|
||||
product_id: props.product_id,
|
||||
target_percentage: Number.isFinite(normalizedTarget) ? normalizedTarget : null,
|
||||
target_department_id: Number.isFinite(normalizedTarget) ? departmentId : null,
|
||||
},
|
||||
};
|
||||
|
||||
await refreshOverview();
|
||||
|
||||
return response;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -83,6 +145,9 @@ const resolvedTitle = computed(() => {
|
||||
:isLoading="is_loading"
|
||||
:state="productTile.state || 'ready'"
|
||||
:unavailable-message="productTile.message || 'Ikke tilgængelig'"
|
||||
:can-edit-target="canEditProductTarget"
|
||||
:target-percentage="targetPercentage"
|
||||
:save-target-function="saveProductTarget"
|
||||
:style-content="styleContent"
|
||||
:data-testid="dataTestid"
|
||||
/>
|
||||
|
||||
+11
-11
@@ -14,6 +14,16 @@ import NotificationsPhonePagination
|
||||
:title="$t('admin.notifications.title')"
|
||||
:subtitle="$t('admin.notifications.subtitle')"
|
||||
>
|
||||
<header class="mb-5" data-testid="department-notifications-page-header">
|
||||
<h1 class="title is-3 mb-2" data-testid="department-notifications-title">
|
||||
{{ $t('admin.notifications.title') }}
|
||||
</h1>
|
||||
<p class="subtitle is-6 mb-0" data-testid="department-notifications-subtitle">
|
||||
<span>{{ $t('admin.notifications.sms_overview') }}</span>
|
||||
<br>
|
||||
<span>{{ $t('admin.notifications.sms_activated_description') }}</span>
|
||||
</p>
|
||||
</header>
|
||||
<NotFoundFallBackPageWrapper :exists="SessionUser.functions.getDepartmentIdFromUrl() && SessionUser.canAccessDepartment(SessionUser.functions.getDepartmentIdFromUrl())" :error="$t('admin.errors.select_department')">
|
||||
<element-tabs-box
|
||||
defaultActiveTab="sms"
|
||||
@@ -24,16 +34,6 @@ import NotificationsPhonePagination
|
||||
]"
|
||||
>
|
||||
<template #sms>
|
||||
<!-- What is this? -->
|
||||
<div class="message">
|
||||
<div class="message-header">
|
||||
<p>{{ $t('admin.notifications.sms_notifications') }}</p>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
<p>{{ $t('admin.notifications.sms_overview') }}</p>
|
||||
<p>{{ $t('admin.notifications.sms_activated_description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<NotificationsPhonePagination />
|
||||
</template>
|
||||
</element-tabs-box>
|
||||
@@ -44,4 +44,4 @@ import NotificationsPhonePagination
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { isCompactProject } from "./support/projects";
|
||||
|
||||
const adminPermissions = [
|
||||
"admin",
|
||||
"list_department_daily_reports",
|
||||
"list_bookings",
|
||||
"create_department_daily_report_complaints",
|
||||
"department_access_1",
|
||||
"department_access_2",
|
||||
@@ -26,8 +28,26 @@ const buildOverviewPayload = ({
|
||||
complaintsMessage = null,
|
||||
metrics = {},
|
||||
products = [
|
||||
{ product_id: 24, slug: "spot-free-lastbil", title: "Spot Free (Lastbil)", state: "ready", value: 3, out_of: 8 },
|
||||
{ product_id: 25, slug: "faelg-flex", title: "Fælg flex pr. enhed", state: "ready", value: 2, out_of: 8 },
|
||||
{
|
||||
product_id: 24,
|
||||
slug: "spot-free-lastbil",
|
||||
title: "Spot Free (Lastbil)",
|
||||
state: "ready",
|
||||
value: 3,
|
||||
out_of: 8,
|
||||
target_percentage: null,
|
||||
target_department_id: null,
|
||||
},
|
||||
{
|
||||
product_id: 25,
|
||||
slug: "faelg-flex",
|
||||
title: "Fælg flex pr. enhed",
|
||||
state: "ready",
|
||||
value: 2,
|
||||
out_of: 8,
|
||||
target_percentage: null,
|
||||
target_department_id: null,
|
||||
},
|
||||
{
|
||||
product_id: 27,
|
||||
slug: "extraordinary-10-min",
|
||||
@@ -35,6 +55,8 @@ const buildOverviewPayload = ({
|
||||
state: "ready",
|
||||
value: 1,
|
||||
out_of: 8,
|
||||
target_percentage: null,
|
||||
target_department_id: null,
|
||||
},
|
||||
{
|
||||
product_id: 26,
|
||||
@@ -43,8 +65,19 @@ const buildOverviewPayload = ({
|
||||
state: "ready",
|
||||
value: 4,
|
||||
out_of: 8,
|
||||
target_percentage: null,
|
||||
target_department_id: null,
|
||||
},
|
||||
{
|
||||
product_id: 21,
|
||||
slug: "undervognsskyl",
|
||||
title: "Undervognsskyl pr. enhed",
|
||||
state: "ready",
|
||||
value: 2,
|
||||
out_of: 8,
|
||||
target_percentage: null,
|
||||
target_department_id: null,
|
||||
},
|
||||
{ product_id: 21, slug: "undervognsskyl", title: "Undervognsskyl pr. enhed", state: "ready", value: 2, out_of: 8 },
|
||||
{
|
||||
product_id: 22,
|
||||
slug: "double-duty-kemi",
|
||||
@@ -52,6 +85,8 @@ const buildOverviewPayload = ({
|
||||
state: "ready",
|
||||
value: 5,
|
||||
out_of: 8,
|
||||
target_percentage: null,
|
||||
target_department_id: null,
|
||||
},
|
||||
],
|
||||
...overrides
|
||||
@@ -92,6 +127,9 @@ async function mockDailyReportDependencies(
|
||||
body: Record<string, unknown>
|
||||
) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>;
|
||||
complaintCustomerLookupHandler?: (url: URL) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>;
|
||||
productTargetHandler?: (
|
||||
body: Record<string, unknown>
|
||||
) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>;
|
||||
permissions?: string[];
|
||||
}
|
||||
| ((url: URL) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>)
|
||||
@@ -114,6 +152,16 @@ async function mockDailyReportDependencies(
|
||||
},
|
||||
}));
|
||||
const complaintCustomerLookupHandler = options.complaintCustomerLookupHandler || (async () => json({ data: [] }));
|
||||
const productTargetHandler =
|
||||
options.productTargetHandler ||
|
||||
(async (body) =>
|
||||
json({
|
||||
data: {
|
||||
department_id: body.department_id,
|
||||
product_id: body.product_id,
|
||||
target_percentage: body.target_percentage ?? null,
|
||||
},
|
||||
}));
|
||||
|
||||
await seedAuthenticatedState(page);
|
||||
await mockApi(page, {
|
||||
@@ -167,6 +215,12 @@ async function mockDailyReportDependencies(
|
||||
await route.fulfill(response);
|
||||
});
|
||||
|
||||
await page.route(/\/departments\/daily-reports\/product-targets$/i, async (route) => {
|
||||
const body = route.request().postDataJSON() as Record<string, unknown>;
|
||||
const response = await productTargetHandler(body);
|
||||
await route.fulfill(response);
|
||||
});
|
||||
|
||||
await page.route(/\/departments\/daily-reports(?:\/overview)?(\?.*)?$/i, async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
if (url.pathname.endsWith("/departments/daily-reports/overview")) {
|
||||
@@ -199,7 +253,7 @@ test.describe("Admin daily report", () => {
|
||||
await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23");
|
||||
|
||||
await expect(page.getByTestId("daily-report-page")).toBeVisible();
|
||||
await expect(page.locator("[data-testid^='daily-report-tile-']")).toHaveCount(17);
|
||||
await expect(page.locator(".card[data-testid^='daily-report-tile-']")).toHaveCount(17);
|
||||
await expect(page.getByTestId("daily-report-tile-bookings")).toContainText("4");
|
||||
await expect(page.getByTestId("daily-report-tile-complaints")).toContainText("2");
|
||||
await expect(page.getByTestId("daily-report-complaints-add-button")).toBeVisible();
|
||||
@@ -216,6 +270,81 @@ test.describe("Admin daily report", () => {
|
||||
await expect(page.getByTestId("daily-report-tile-double-duty-kemi")).toContainText("Tillæg for Specialsæbe - DD");
|
||||
});
|
||||
|
||||
test("lets permitted users set a target from an optional product percentage", async ({ page }) => {
|
||||
let targetPercentage: number | null = null;
|
||||
const targetRequests: Array<Record<string, unknown>> = [];
|
||||
|
||||
await mockDailyReportDependencies(page, {
|
||||
permissions: [...adminPermissions, "set_department_daily_report_product_targets"],
|
||||
overviewHandler: async () =>
|
||||
json(
|
||||
buildOverviewPayload({
|
||||
products: [
|
||||
{
|
||||
product_id: 24,
|
||||
slug: "spot-free-lastbil",
|
||||
title: "Spot Free (Lastbil)",
|
||||
state: "ready",
|
||||
value: 3,
|
||||
out_of: 8,
|
||||
target_percentage: targetPercentage,
|
||||
target_department_id: targetPercentage === null ? null : 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
productTargetHandler: async (body) => {
|
||||
targetRequests.push(body);
|
||||
targetPercentage = body.target_percentage === null ? null : Number(body.target_percentage);
|
||||
|
||||
return json({
|
||||
data: {
|
||||
department_id: body.department_id,
|
||||
product_id: body.product_id,
|
||||
target_percentage: targetPercentage,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23");
|
||||
await expect(page.getByTestId("daily-report-page")).toBeVisible();
|
||||
|
||||
const productTile = page.getByTestId("daily-report-tile-spot-free-lastbil");
|
||||
const percentageButton = page.getByTestId("daily-report-tile-spot-free-lastbil-percentage-button");
|
||||
await expect(percentageButton).toBeVisible();
|
||||
await expect(percentageButton).toHaveClass(/is-outlined/);
|
||||
await expect(page.locator('[data-testid="daily-report-tile-spot-free-lastbil-target-button"]')).toHaveCount(0);
|
||||
|
||||
await percentageButton.click();
|
||||
await page.getByTestId("daily-report-tile-spot-free-lastbil-target-input").fill("55.5");
|
||||
await page.getByTestId("daily-report-tile-spot-free-lastbil-target-save").click();
|
||||
|
||||
await expect.poll(() => targetRequests.length).toBe(1);
|
||||
expect(targetRequests[0]).toMatchObject({
|
||||
department_id: 1,
|
||||
product_id: 24,
|
||||
target_percentage: 55.5,
|
||||
});
|
||||
await expect(page.getByTestId("daily-report-tile-spot-free-lastbil-target-button")).toContainText("55.5%");
|
||||
await expect(productTile.locator(".daily-report-count__target-background-icon")).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("does not open the product target editor without permission", async ({ page }) => {
|
||||
await mockDailyReportDependencies(page, {
|
||||
overviewHandler: async () => json(buildOverviewPayload()),
|
||||
permissions: adminPermissions,
|
||||
});
|
||||
|
||||
await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23");
|
||||
await expect(page.getByTestId("daily-report-page")).toBeVisible();
|
||||
|
||||
await page.getByTestId("daily-report-tile-spot-free-lastbil-percentage-button").click();
|
||||
|
||||
await expect(page.locator('[data-testid="daily-report-tile-spot-free-lastbil-target-input"]')).toHaveCount(0);
|
||||
await expect(page.locator('[data-testid="daily-report-tile-spot-free-lastbil-target-button"]')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("renders rounded-up weather hours and keeps unknown productivity gray", async ({ page }) => {
|
||||
await mockDailyReportDependencies(page, {
|
||||
overviewHandler: async () => json(buildOverviewPayload()),
|
||||
|
||||
@@ -18,6 +18,8 @@ const json = (body: unknown, status = 200) => ({
|
||||
});
|
||||
const pageReadyTimeout = process.env.CI ? 30_000 : 15_000;
|
||||
|
||||
const pageBootstrapTimeoutMs = 45_000;
|
||||
|
||||
const setupNotificationSmsApi = async (page: Page) => {
|
||||
let listRequestCount = 0;
|
||||
const mutations: Array<{ method: string; body?: Record<string, unknown>; id?: number }> = [];
|
||||
@@ -118,6 +120,21 @@ test("@smoke department notification SMS active toggle and delete refresh the ta
|
||||
await page.goto("/admin/1/modules/notifications", { waitUntil: "domcontentloaded" });
|
||||
await expect.poll(() => notificationSmsApi.listRequestCount, { timeout: pageReadyTimeout }).toBeGreaterThan(0);
|
||||
|
||||
const pageTitle = page.getByTestId("department-notifications-title");
|
||||
await expect(pageTitle).toBeVisible({ timeout: pageBootstrapTimeoutMs });
|
||||
await expect(pageTitle).toContainText(/notifikationer|notifications/i);
|
||||
|
||||
const pageSubtitle = page.getByTestId("department-notifications-subtitle");
|
||||
await expect(pageSubtitle).toBeVisible();
|
||||
await expect(pageSubtitle).toContainText(/sms/i);
|
||||
await expect(page.locator(".message").filter({ hasText: /sms/i })).toHaveCount(0);
|
||||
|
||||
const addPhoneButton = page.getByTestId("department-notification-sms-add-button");
|
||||
await expect(addPhoneButton).toBeVisible();
|
||||
await expect(addPhoneButton).toHaveClass(/is-primary/);
|
||||
await expect(addPhoneButton).not.toHaveClass(/is-info/);
|
||||
await expect(addPhoneButton.locator(".fa-plus")).toBeVisible();
|
||||
|
||||
const row = page.locator("tr", { hasText: "Dispatch line" });
|
||||
await expect(row).toBeVisible({ timeout: pageReadyTimeout });
|
||||
const enabledToggle = page.getByTestId("department-notification-sms-enabled-toggle-501");
|
||||
|
||||
@@ -1844,7 +1844,7 @@ test("shows live Stripe payment status and cancellation for open card payment or
|
||||
await expect(page.getByTestId("pos-order-stripe-email-cancel")).toBeVisible();
|
||||
await expect(page.getByTestId("pos-order-print-receipt")).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: /Fuldfør ordre|Complete order/ })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: /Hent faktura|Get invoice/ })).toHaveCount(0);
|
||||
await expect(page.locator('[data-action-key="economic-invoice-pdf-download"]')).toBeDisabled();
|
||||
|
||||
await page.getByTestId("pos-order-stripe-email-cancel").click();
|
||||
await deleteStripeInvoiceRequest;
|
||||
@@ -2950,6 +2950,28 @@ test.describe("Admin POS Orders - desktop locked states", () => {
|
||||
await expectOrderTotal(page, 1372);
|
||||
});
|
||||
|
||||
test("disables invoice PDF download while the Economic invoice id is missing", async ({ page }) => {
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: POS_PERMISSIONS,
|
||||
edgeGateways: false,
|
||||
pos: createPosFixture({
|
||||
economicModuleOrdersByOrderId: {
|
||||
54518: {
|
||||
invoice_id: null,
|
||||
invoice_draft_id: 88012,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
await primeOperatorSession(page, "pos-orders-draft-invoice-token");
|
||||
await openOrderDetail(page);
|
||||
|
||||
const downloadButton = page.locator('[data-action-key="economic-invoice-pdf-download"]');
|
||||
await expect(downloadButton).toBeVisible();
|
||||
await expect(downloadButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test("hides item mutation controls when the order is already invoiced", async ({ page }) => {
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
@@ -2970,6 +2992,7 @@ test.describe("Admin POS Orders - desktop locked states", () => {
|
||||
await expect(page.getByTestId("pos-order-item-edit-9101")).toHaveCount(0);
|
||||
await expect(page.getByTestId("pos-order-item-delete-9101")).toHaveCount(0);
|
||||
await expect(page.getByTestId("pos-order-add-item")).toHaveCount(0);
|
||||
await expect(page.locator('[data-action-key="economic-invoice-pdf-download"]')).toBeEnabled();
|
||||
await expectOrderTotal(page, 1372);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -825,7 +825,7 @@ test.describe("Coolify infrastructure management", () => {
|
||||
await expect(actions.getByRole("button", { name: "Reconcile Coolify" })).toBeVisible();
|
||||
await expect(actions.getByRole("button", { name: "Coolify failover" })).toBeVisible();
|
||||
await actions.getByRole("button", { name: "Reconcile Coolify" }).click();
|
||||
expect(state.reconciledTargets.length).toBeGreaterThan(0);
|
||||
await expect.poll(() => state.reconciledTargets.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("resumes Coolify-backed provisioning after page reload", async ({ page }) => {
|
||||
@@ -1044,7 +1044,9 @@ test.describe("Coolify infrastructure management", () => {
|
||||
await actions.locator(".action-settings-wheel-trigger").click();
|
||||
await actions.getByRole("button", { name: /Rename|Omdøb/ }).click();
|
||||
|
||||
expect(state.renamedHosts).toContainEqual({ kind: "database", id: 2, label: "mariadb-manual-replica" });
|
||||
await expect
|
||||
.poll(() => state.renamedHosts)
|
||||
.toContainEqual({ kind: "database", id: 2, label: "mariadb-manual-replica" });
|
||||
await expect(page.getByTestId("replication-host-database-2")).toContainText("mariadb-manual-replica");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -350,6 +350,10 @@ async function readStoredToken(page: Page) {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForAppRootMounted(page: Page) {
|
||||
await expect.poll(async () => page.locator("#app > *").count(), { timeout: AUTH_TIMEOUT }).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
async function settleAuthenticatedNavigation(page: Page, targetPath: string, targetUrl: RegExp) {
|
||||
await expect.poll(() => readStoredToken(page), { timeout: AUTH_TIMEOUT }).not.toBeNull();
|
||||
const navigateToTarget = async () => {
|
||||
@@ -376,6 +380,8 @@ async function settleAuthenticatedNavigation(page: Page, targetPath: string, tar
|
||||
await navigateToTarget();
|
||||
await expect.poll(() => page.url(), { timeout: AUTH_TIMEOUT }).toMatch(targetUrl);
|
||||
}
|
||||
|
||||
await waitForAppRootMounted(page);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,12 @@ import { scanViewTranslationKeys, type ViewTranslationKeyUsage } from "./support
|
||||
const ACTIVE_LOCALES = ["da", "en", "sv", "de", "no"] as const;
|
||||
const GENERATED_LOCALES_DIRECTORY = path.join(process.cwd(), "src", "i18n", "generated");
|
||||
const REVIEWED_NON_LITERAL_CALLS = new Set([
|
||||
"src/views/backoffice/LimitedBackofficeEmployees.vue|t|return `${option.flag} +${option.value} ${t(`templates.limited_backoffice.employees.country_codes.${option.labelKey}`)}`;",
|
||||
"src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodFlagList.vue|t|const translated = t(key, params);",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|label: t(`roles.permissions.page_access.${permission}.label`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|description: t(`roles.permissions.page_access.${permission}.description`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|label: t(`roles.permissions.groups.${group.key}.label`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|description: t(`roles.permissions.groups.${group.key}.description`),",
|
||||
'src/views/dashboards/superUserDashboard/system/ReplicationManagement.vue|t|return t(`replication.status.${status || "unknown"}`);',
|
||||
"src/views/dashboards/superUserDashboard/system/ReplicationManagement.vue|te|if (key && te(key)) {",
|
||||
"src/views/dashboards/superUserDashboard/system/ReplicationManagement.vue|t|return t(key, params);",
|
||||
@@ -14,9 +19,18 @@ const REVIEWED_NON_LITERAL_CALLS = new Set([
|
||||
'src/views/dashboards/superUserDashboard/system/ReplicationManagement.vue|t|<li v-for="key in redisComposeInstructionKeys" :key="key">{{ t(key) }}</li>',
|
||||
'src/views/dashboards/superUserDashboard/system/ReplicationManagement.vue|t|<li v-for="key in minioComposeInstructionKeys" :key="key">{{ t(key) }}</li>',
|
||||
"src/views/dashboards/superUserDashboard/system/ReplicationManagement.vue|t|<td>{{ t(`replication.roles.${host.role}`) }}</td>",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|label: t(`roles.permissions.page_access.${permission}.label`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|description: t(`roles.permissions.page_access.${permission}.description`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|label: t(`roles.permissions.groups.${group.key}.label`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|description: t(`roles.permissions.groups.${group.key}.description`),",
|
||||
"src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue|t|const tr = (key, params = {}) => t(`configuration.release_manager.${key}`, params);",
|
||||
"src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue|t|const translated = t(fullKey, params);",
|
||||
"src/views/dashboards/superUserDashboard/ErrorReports.vue|t|const value = t(key);",
|
||||
"src/views/backoffice/LimitedBackofficeEmployees.vue|t|return `${option.flag} +${option.value} ${t(`templates.limited_backoffice.employees.country_codes.${option.labelKey}`)}`;",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|label: t(`roles.permissions.page_access.${permission}.label`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|description: t(`roles.permissions.page_access.${permission}.description`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|label: t(`roles.permissions.groups.${group.key}.label`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|description: t(`roles.permissions.groups.${group.key}.description`),",
|
||||
'src/views/dashboards/userDashboard/wash/components/LaneSelectionSection.vue|$t|:title="option.titleKey ? $t(option.titleKey) : null"',
|
||||
'src/views/dashboards/userDashboard/wash/components/LaneSelectionSection.vue|$t|:aria-label="option.titleKey ? $t(option.titleKey) : null"',
|
||||
"src/views/dashboards/userDashboard/wash/components/LaneSelectionSection.vue|$t|{{ $t(option.statusLabelKey) }}",
|
||||
@@ -24,6 +38,10 @@ const REVIEWED_NON_LITERAL_CALLS = new Set([
|
||||
"src/views/dashboards/userDashboard/wash/components/WashTypeSelector.vue|$t|{{ $t(option.statusLabelKey) }}",
|
||||
'src/views/dashboards/userDashboard/wash/MyWashStart.vue|$t|:vehicle-step-guidance="vehicleStepGuidanceKey ? $t(vehicleStepGuidanceKey) : null"',
|
||||
"src/views/dashboards/userDashboard/wash/MyWashStart.vue|$t|{{ $t(confirmActionLabelKey) }}",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|label: t(`roles.permissions.page_access.${permission}.label`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|description: t(`roles.permissions.page_access.${permission}.description`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|label: t(`roles.permissions.groups.${group.key}.label`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|description: t(`roles.permissions.groups.${group.key}.description`),",
|
||||
]);
|
||||
|
||||
const readGeneratedJson = (fileName: string) => {
|
||||
|
||||
@@ -1112,6 +1112,7 @@ test.describe("Invoicing period tab", () => {
|
||||
const usageOrderRequests = [];
|
||||
const fastLinkRequests = [];
|
||||
const automationAcceptRequests = [];
|
||||
const importUsageRequests = [];
|
||||
let automation = {
|
||||
id: 7101,
|
||||
status: "suggested",
|
||||
@@ -1133,6 +1134,15 @@ test.describe("Invoicing period tab", () => {
|
||||
|
||||
await openPeriodView(page);
|
||||
|
||||
await page.route("**/modules/xlvask/tasks/import-usage**", async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
importUsageRequests.push({
|
||||
dateFrom: url.searchParams.get("dateFrom"),
|
||||
dateTo: url.searchParams.get("dateTo"),
|
||||
});
|
||||
await route.fulfill(json({ data: "Usage logs imported" }));
|
||||
});
|
||||
|
||||
await page.route("**/modules/xlvask/services/usage/orders**", async (route) => {
|
||||
const path = new URL(route.request().url()).pathname;
|
||||
if (
|
||||
@@ -1285,6 +1295,12 @@ test.describe("Invoicing period tab", () => {
|
||||
request.filters.includes("StartTime-date_to:2026-03-31")
|
||||
)
|
||||
).toBe(true);
|
||||
await selfWashView.getByRole("button", { name: /import/i }).click();
|
||||
await expect.poll(() => importUsageRequests.length).toBe(1);
|
||||
expect(importUsageRequests[0]).toEqual({
|
||||
dateFrom: "2026-03-01",
|
||||
dateTo: "2026-03-31",
|
||||
});
|
||||
});
|
||||
|
||||
test("@smoke period view shows flags and saves automatic flag decisions", async ({ page }) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { API_HOST, seedAuthenticatedState } from "./support/network.js";
|
||||
import { isDesktopProject } from "./support/projects";
|
||||
|
||||
@@ -14,6 +14,7 @@ const limitedManagerPermissions = [
|
||||
"limited_backoffice_prices_manage",
|
||||
"limited_backoffice_employees_manage",
|
||||
"department_access_1",
|
||||
"department_access_2",
|
||||
];
|
||||
|
||||
const sessionData = {
|
||||
@@ -62,7 +63,10 @@ const adminLimitedSessionData = {
|
||||
permissions: ["user", "admin", "limited_backoffice_access", "department_access_1"],
|
||||
};
|
||||
|
||||
const assignedDepartments = [{ id: 1, name: "Assigned Depot", description: "", visible: true, archived: false }];
|
||||
const assignedDepartments = [
|
||||
{ id: 1, name: "Assigned Depot", description: "", visible: true, archived: false },
|
||||
{ id: 2, name: "Remote Depot", description: "", visible: true, archived: false },
|
||||
];
|
||||
|
||||
const pricePayload = {
|
||||
department: { id: 1, name: "Assigned Depot", description: "" },
|
||||
@@ -144,6 +148,7 @@ const rolePermissionGroups = {
|
||||
"view_orders",
|
||||
"create_orders",
|
||||
"edit_orders",
|
||||
"complete_orders",
|
||||
"view_order_items",
|
||||
"create_order_items",
|
||||
"update_order_lines",
|
||||
@@ -151,6 +156,43 @@ const rolePermissionGroups = {
|
||||
"charge_orders",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "products",
|
||||
capabilities: ["view_product_catalog", "view_product_recommendations"],
|
||||
},
|
||||
{
|
||||
key: "customers",
|
||||
capabilities: [
|
||||
"search_customers",
|
||||
"view_customer_details",
|
||||
"view_customer_notes",
|
||||
"add_customer_notes",
|
||||
"view_customer_flags",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "vehicles",
|
||||
capabilities: ["search_vehicles", "view_vehicle_matches", "view_vehicle_history"],
|
||||
},
|
||||
{
|
||||
key: "attachments",
|
||||
capabilities: ["view_order_attachments", "add_order_attachments", "download_order_attachments"],
|
||||
},
|
||||
{
|
||||
key: "scanner",
|
||||
capabilities: ["view_plate_scans"],
|
||||
},
|
||||
{
|
||||
key: "bookings",
|
||||
capabilities: [
|
||||
"view_department_bookings",
|
||||
"view_own_bookings",
|
||||
"update_bookings",
|
||||
"create_bookings",
|
||||
"mark_bookings_complete",
|
||||
"send_booking_confirmations",
|
||||
],
|
||||
},
|
||||
],
|
||||
booking_coordinator: [
|
||||
{ key: "account", capabilities: ["sign_in", "view_own_permissions"] },
|
||||
@@ -180,6 +222,7 @@ const rolePermissionGroups = {
|
||||
"create_orders",
|
||||
"edit_orders",
|
||||
"delete_orders",
|
||||
"complete_orders",
|
||||
"view_order_items",
|
||||
"create_order_items",
|
||||
"update_order_lines",
|
||||
@@ -187,6 +230,32 @@ const rolePermissionGroups = {
|
||||
"charge_orders",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "products",
|
||||
capabilities: ["view_product_catalog", "view_product_recommendations"],
|
||||
},
|
||||
{
|
||||
key: "customers",
|
||||
capabilities: [
|
||||
"search_customers",
|
||||
"view_customer_details",
|
||||
"view_customer_notes",
|
||||
"add_customer_notes",
|
||||
"view_customer_flags",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "vehicles",
|
||||
capabilities: ["search_vehicles", "view_vehicle_matches", "view_vehicle_history"],
|
||||
},
|
||||
{
|
||||
key: "attachments",
|
||||
capabilities: ["view_order_attachments", "add_order_attachments", "download_order_attachments"],
|
||||
},
|
||||
{
|
||||
key: "scanner",
|
||||
capabilities: ["view_plate_scans"],
|
||||
},
|
||||
{
|
||||
key: "bookings",
|
||||
capabilities: [
|
||||
@@ -209,6 +278,7 @@ const rolePermissionGroups = {
|
||||
"create_orders",
|
||||
"edit_orders",
|
||||
"delete_orders",
|
||||
"complete_orders",
|
||||
"view_order_items",
|
||||
"create_order_items",
|
||||
"update_order_lines",
|
||||
@@ -216,6 +286,32 @@ const rolePermissionGroups = {
|
||||
"charge_orders",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "products",
|
||||
capabilities: ["view_product_catalog", "view_product_recommendations"],
|
||||
},
|
||||
{
|
||||
key: "customers",
|
||||
capabilities: [
|
||||
"search_customers",
|
||||
"view_customer_details",
|
||||
"view_customer_notes",
|
||||
"add_customer_notes",
|
||||
"view_customer_flags",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "vehicles",
|
||||
capabilities: ["search_vehicles", "view_vehicle_matches", "view_vehicle_history"],
|
||||
},
|
||||
{
|
||||
key: "attachments",
|
||||
capabilities: ["view_order_attachments", "add_order_attachments", "download_order_attachments"],
|
||||
},
|
||||
{
|
||||
key: "scanner",
|
||||
capabilities: ["view_plate_scans"],
|
||||
},
|
||||
{
|
||||
key: "bookings",
|
||||
capabilities: [
|
||||
@@ -235,8 +331,18 @@ const rolePermissionGroups = {
|
||||
],
|
||||
};
|
||||
|
||||
const remotePricePayload = {
|
||||
...pricePayload,
|
||||
department: { id: 2, name: "Remote Depot", description: "" },
|
||||
};
|
||||
|
||||
const rolesPayload = [
|
||||
{ key: "viewer", label: "Viewer", description: "Can view.", permission_groups: rolePermissionGroups.viewer },
|
||||
{
|
||||
key: "viewer",
|
||||
label: "Deactivated",
|
||||
description: "No order, booking, or management access.",
|
||||
permission_groups: rolePermissionGroups.viewer,
|
||||
},
|
||||
{ key: "cashier", label: "Cashier", description: "Can sell.", permission_groups: rolePermissionGroups.cashier },
|
||||
{
|
||||
key: "booking_coordinator",
|
||||
@@ -263,7 +369,7 @@ const employeesPayload = [
|
||||
{
|
||||
id: 501,
|
||||
user_id: 501,
|
||||
customer_number: 900000501,
|
||||
customer_number: 0,
|
||||
display_name: "Casey Clerk",
|
||||
email: "casey@example.com",
|
||||
phone_country_code: 45,
|
||||
@@ -274,6 +380,17 @@ const employeesPayload = [
|
||||
created_at: "2026-01-01 00:00:00",
|
||||
updated_at: "2026-01-01 00:00:00",
|
||||
},
|
||||
{
|
||||
id: 502,
|
||||
customer_number: 0,
|
||||
display_name: "Riley Remote",
|
||||
email: "riley@example.com",
|
||||
active: true,
|
||||
role: { key: "viewer", label: "Deactivated", description: "No order, booking, or management access." },
|
||||
departments: [{ id: 2, name: "Remote Depot" }],
|
||||
created_at: "2026-01-01 00:00:00",
|
||||
updated_at: "2026-01-01 00:00:00",
|
||||
},
|
||||
];
|
||||
|
||||
async function seedLimitedBackofficeSession(page, token = "limited-backoffice-token") {
|
||||
@@ -350,6 +467,11 @@ async function mockLimitedBackofficeApi(page, authSessionData = sessionData, opt
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/limited-backoffice/departments/2/prices") && method === "GET") {
|
||||
await route.fulfill(json({ data: remotePricePayload }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/limited-backoffice/departments/1/prices") && method === "PUT") {
|
||||
const body = request.postDataJSON?.() || null;
|
||||
priceUpdateCalls.push(body);
|
||||
@@ -462,6 +584,20 @@ async function mockLimitedBackofficeApi(page, authSessionData = sessionData, opt
|
||||
};
|
||||
}
|
||||
|
||||
async function expectDepartmentSelectorInTitleRow(page: Page) {
|
||||
const headingBox = await page.getByRole("heading", { name: "Backoffice" }).boundingBox();
|
||||
const selectBox = await page.getByTestId("limited-backoffice-department-select").boundingBox();
|
||||
|
||||
expect(headingBox).not.toBeNull();
|
||||
expect(selectBox).not.toBeNull();
|
||||
if (!headingBox || !selectBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(selectBox.x).toBeGreaterThan(headingBox.x);
|
||||
expect(Math.abs(selectBox.y + selectBox.height / 2 - (headingBox.y + headingBox.height / 2))).toBeLessThan(48);
|
||||
}
|
||||
|
||||
test.describe("Limited backoffice", () => {
|
||||
test("shows the limited backoffice header shortcut only on department-scoped admin pages", async ({
|
||||
page,
|
||||
@@ -539,7 +675,9 @@ test.describe("Limited backoffice", () => {
|
||||
await page.goto("/backoffice/departments/1/prices");
|
||||
|
||||
await expect(page.getByTestId("limited-prices-title")).toBeVisible();
|
||||
await expectDepartmentSelectorInTitleRow(page);
|
||||
await expect(page.getByTestId("limited-backoffice-department-select")).toContainText("Assigned Depot");
|
||||
await expect(page.getByTestId("limited-backoffice-department-select")).toContainText("Remote Depot");
|
||||
await expect(page.getByTestId("limited-backoffice-department-select")).not.toContainText("Other Depot");
|
||||
await expect(page.getByTestId("limited-price-row-101")).toContainText("Truck wash");
|
||||
await expect(page.getByTestId("limited-price-input-101")).toHaveValue("125");
|
||||
@@ -718,12 +856,16 @@ test.describe("Limited backoffice", () => {
|
||||
await seedLimitedBackofficeSession(page);
|
||||
const api = await mockLimitedBackofficeApi(page);
|
||||
|
||||
await page.goto("/backoffice/employees");
|
||||
await page.goto("/backoffice/departments/1/employees");
|
||||
|
||||
await expect(page.getByTestId("limited-employees-title")).toBeVisible();
|
||||
await expectDepartmentSelectorInTitleRow(page);
|
||||
await expect(page.getByTestId("limited-employee-row-501")).toContainText("Casey Clerk");
|
||||
await expect(page.getByTestId("limited-employee-user-id-501")).toHaveText("501");
|
||||
await expect(page.getByTestId("limited-employee-phone-501")).toHaveText("+45 12345678");
|
||||
await expect(page.getByTestId("limited-employee-row-502")).toHaveCount(0);
|
||||
await expect(page.getByTestId("limited-employee-department-1")).toBeChecked();
|
||||
await expect(page.getByTestId("limited-employee-department-2")).not.toBeChecked();
|
||||
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");
|
||||
@@ -797,17 +939,36 @@ test.describe("Limited backoffice", () => {
|
||||
await expect(modal).toBeVisible();
|
||||
await expect(modal).toContainText("Role permissions");
|
||||
await expect(modal).toContainText("Every capability is limited to the departments selected for the employee.");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-viewer")).toContainText("Deactivated");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Cashier");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Orders");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Products");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Customers");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Vehicles");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Attachments");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Scanners");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Bookings");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("View orders");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Complete orders");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("View product catalog");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Search customers");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Search vehicles");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Add order attachments");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("View plate scans");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Create bookings");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Create order lines");
|
||||
await expect(page.getByTestId("limited-role-permissions-role-viewer")).toContainText("Selected");
|
||||
|
||||
for (const rawPermission of [
|
||||
"list_orders",
|
||||
"add_order",
|
||||
"fetch_order",
|
||||
"edit_order_items",
|
||||
"delete_order_items",
|
||||
"search_customers",
|
||||
"list_order_attachments",
|
||||
"list_products",
|
||||
"add_bookings",
|
||||
"department_timebookings_entries_get",
|
||||
"limited_backoffice_employees_manage",
|
||||
"department_access_1",
|
||||
@@ -834,15 +995,14 @@ test.describe("Limited backoffice", () => {
|
||||
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-departments").locator(".switch")).toHaveCount(2);
|
||||
await expect(page.getByTestId("limited-employee-department-1")).toBeChecked();
|
||||
await expect(page.getByTestId("limited-employee-department-2")).not.toBeChecked();
|
||||
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();
|
||||
|
||||
@@ -856,14 +1016,13 @@ test.describe("Limited backoffice", () => {
|
||||
role_key: "viewer",
|
||||
department_ids: [1],
|
||||
});
|
||||
await expect(page.getByTestId("limited-employee-row-902")).toContainText("No Phone Worker");
|
||||
await expect(page.getByTestId("limited-employee-row-903")).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();
|
||||
|
||||
@@ -877,8 +1036,8 @@ test.describe("Limited backoffice", () => {
|
||||
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");
|
||||
await expect(page.getByTestId("limited-employee-row-904")).toContainText("Phone Worker");
|
||||
await expect(page.getByTestId("limited-employee-phone-904")).toHaveText("+358 87654321");
|
||||
});
|
||||
|
||||
test("edits employee contact details without requiring a new password", async ({ page }) => {
|
||||
@@ -912,19 +1071,75 @@ test.describe("Limited backoffice", () => {
|
||||
await expect(page.getByTestId("limited-employee-phone-501")).toHaveText("+358 87654321");
|
||||
});
|
||||
|
||||
test("keeps department context across employee access navigation", async ({ page }, testInfo) => {
|
||||
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
||||
|
||||
await seedLimitedBackofficeSession(page);
|
||||
const api = await mockLimitedBackofficeApi(page);
|
||||
|
||||
await page.goto("/backoffice/employees");
|
||||
|
||||
await expect(page).toHaveURL(/\/backoffice\/departments\/1\/employees$/);
|
||||
await expect(page.getByTestId("limited-backoffice-tab-prices")).toHaveAttribute(
|
||||
"href",
|
||||
"/backoffice/departments/1/prices"
|
||||
);
|
||||
await expect(page.getByTestId("limited-backoffice-tab-employees")).toHaveAttribute(
|
||||
"href",
|
||||
"/backoffice/departments/1/employees"
|
||||
);
|
||||
|
||||
await page.getByTestId("limited-backoffice-department-select").selectOption("2");
|
||||
|
||||
await expect(page).toHaveURL(/\/backoffice\/departments\/2\/employees$/);
|
||||
await expect(page.getByTestId("limited-employee-row-501")).toHaveCount(0);
|
||||
await expect(page.getByTestId("limited-employee-row-502")).toContainText("Riley Remote");
|
||||
await expect(page.getByTestId("limited-employee-department-1")).not.toBeChecked();
|
||||
await expect(page.getByTestId("limited-employee-department-2")).toBeChecked();
|
||||
await expect(page.getByTestId("limited-backoffice-tab-prices")).toHaveAttribute(
|
||||
"href",
|
||||
"/backoffice/departments/2/prices"
|
||||
);
|
||||
|
||||
await page.getByTestId("limited-backoffice-tab-prices").click();
|
||||
await expect(page).toHaveURL(/\/backoffice\/departments\/2\/prices$/);
|
||||
await expect(page.getByTestId("limited-backoffice-tab-employees")).toHaveAttribute(
|
||||
"href",
|
||||
"/backoffice/departments/2/employees"
|
||||
);
|
||||
|
||||
expect(api.forbiddenCalls).toEqual([]);
|
||||
});
|
||||
|
||||
test("does not render data for a department outside the manager scope", async ({ page }, testInfo) => {
|
||||
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
||||
|
||||
await seedLimitedBackofficeSession(page);
|
||||
const api = await mockLimitedBackofficeApi(page);
|
||||
|
||||
await page.goto("/backoffice/departments/2/prices");
|
||||
await page.goto("/backoffice/departments/3/prices");
|
||||
|
||||
await expect(page.getByTestId("limited-prices-forbidden")).toBeVisible();
|
||||
await expect(page.locator("body")).not.toContainText("Secret Depot");
|
||||
await expect(page.getByTestId("limited-prices-table")).toHaveCount(0);
|
||||
|
||||
expect(api.calls.some((call) => call.includes("/limited-backoffice/departments/2/prices"))).toBe(false);
|
||||
expect(api.calls.some((call) => call.includes("/limited-backoffice/departments/3/prices"))).toBe(false);
|
||||
expect(api.forbiddenCalls).toEqual([]);
|
||||
});
|
||||
|
||||
test("does not render employee data for a department outside the manager scope", async ({ page }, testInfo) => {
|
||||
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
||||
|
||||
await seedLimitedBackofficeSession(page);
|
||||
const api = await mockLimitedBackofficeApi(page);
|
||||
|
||||
await page.goto("/backoffice/departments/3/employees");
|
||||
|
||||
await expect(page.getByTestId("limited-employees-forbidden")).toBeVisible();
|
||||
await expect(page.locator("body")).not.toContainText("Secret Depot");
|
||||
await expect(page.getByTestId("limited-employees-table")).toHaveCount(0);
|
||||
|
||||
expect(api.calls.some((call) => call.includes("/limited-backoffice/employees"))).toBe(false);
|
||||
expect(api.forbiddenCalls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 107 KiB |
@@ -2328,7 +2328,11 @@ test("superusers manage release settings, assignments, integrations, and sync op
|
||||
await expect(page.getByTestId("release-assignment-subject-manual-user-909")).toContainText("User #909");
|
||||
await expect(page.getByTestId("release-assignment-subject-manual-subuser-909")).toContainText("Subuser #909");
|
||||
await expect(page.getByTestId("release-assignment-subject-manual-customer-909")).toContainText("Customer #909");
|
||||
await page.getByTestId("release-assignment-subject-manual-subuser-909").click({ force: true });
|
||||
await page
|
||||
.getByTestId("release-assignment-subject-autocomplete")
|
||||
.getByRole("button", { name: /Subuser #909/ })
|
||||
.click();
|
||||
await expect(assignmentSubjectInput).toHaveValue("Subuser #909 - Use typed value subuser:909");
|
||||
await page.getByTestId("release-assignment-form").getByRole("button", { name: "Assign" }).click();
|
||||
expect(state.assignmentPayloads[state.assignmentPayloads.length - 1]).toMatchObject({
|
||||
subject_type: "subuser",
|
||||
|
||||
@@ -27,7 +27,7 @@ async function primeSession(page, { token, permissions, sessionData = {} }) {
|
||||
await seedAuthenticatedState(page, token);
|
||||
}
|
||||
|
||||
async function mockSelectedSubuserGrant(page, { customerNumber = 12345679, permissions = ["user"] } = {}) {
|
||||
async function mockSelectedSubuserGrant(page, { customerNumber = 12345679, permissions = ["SELFSERVE_LIST"] } = {}) {
|
||||
await page.route("**/subusers/me", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -249,6 +249,7 @@ test.describe("Self-serve wash", () => {
|
||||
},
|
||||
selfServe: true,
|
||||
});
|
||||
api.selfServe.commandResponseDelayMs = [500, 0];
|
||||
await primeSession(page, {
|
||||
token: "self-serve-user-token",
|
||||
permissions: ["user"],
|
||||
@@ -1045,7 +1046,7 @@ test.describe("Self-serve wash", () => {
|
||||
customerNumberInput: "777",
|
||||
});
|
||||
|
||||
await mockApi(page, {
|
||||
const api = await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["user"],
|
||||
sessionData: {
|
||||
@@ -1063,6 +1064,25 @@ test.describe("Self-serve wash", () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
api.selfServe.previewByKey["7:ZZ00000"] = {
|
||||
...api.selfServe.previewByKey["7:ZZ00000"],
|
||||
questions: [],
|
||||
tasks: [],
|
||||
};
|
||||
api.selfServe.summaryBySessionId[601] = {
|
||||
...api.selfServe.summaryBySessionId[601],
|
||||
questions: [],
|
||||
tasks: [],
|
||||
};
|
||||
api.selfServe.summaryByKey["7:ZZ00000"] = {
|
||||
session: api.selfServe.previewByKey["7:ZZ00000"].session,
|
||||
lane: api.selfServe.previewByKey["7:ZZ00000"].lane,
|
||||
questions: [],
|
||||
conditions: [],
|
||||
rules: [],
|
||||
tasks: [],
|
||||
events: [{ id: 4, type: "STARTED", created_at: "2026-01-01T11:01:00.000Z" }],
|
||||
};
|
||||
await primeSession(page, {
|
||||
token: "self-serve-start-retry-token",
|
||||
permissions: ["user"],
|
||||
@@ -1163,6 +1183,7 @@ test.describe("Self-serve wash", () => {
|
||||
authenticated: true,
|
||||
permissions: ["user"],
|
||||
selfServe: {
|
||||
commandResponseDelayMs: [500, 0],
|
||||
commandResponses: [
|
||||
{
|
||||
success: false,
|
||||
|
||||
@@ -43,11 +43,46 @@ const departments = [
|
||||
},
|
||||
];
|
||||
|
||||
const roleTemplatePermissions = [
|
||||
"user",
|
||||
"permissions_list_own",
|
||||
"list_departments",
|
||||
"list_orders",
|
||||
"fetch_order",
|
||||
"statistics_orders_new",
|
||||
];
|
||||
|
||||
const limitedBackofficePermissionTemplates = [
|
||||
{
|
||||
key: "viewer",
|
||||
label: "Deactivated",
|
||||
description: "Keeps the employee registered without order, booking, or management permissions.",
|
||||
permissions: ["user", "permissions_list_own"],
|
||||
},
|
||||
{
|
||||
key: "cashier",
|
||||
label: "Cashier",
|
||||
description: "Can work with POS orders for assigned departments.",
|
||||
permissions: roleTemplatePermissions,
|
||||
},
|
||||
];
|
||||
|
||||
type PermissionCall = {
|
||||
action: "add" | "remove";
|
||||
permission: string;
|
||||
};
|
||||
|
||||
const roleListEnvelope = (rows: Array<Record<string, unknown>>) => ({
|
||||
data: rows,
|
||||
meta: {
|
||||
pagination: {
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
total: rows.length,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const openGroupTab = async (page: Page, label: RegExp) => {
|
||||
await page.locator(".tabs").getByText(label).click();
|
||||
};
|
||||
@@ -147,6 +182,107 @@ const bootRolePermissionsPage = async (page: Page, routeOptions = {}) => {
|
||||
};
|
||||
|
||||
test.describe("Superuser role permissions", () => {
|
||||
test("applies limited backoffice templates from the role action wheel in add-only mode", async ({ page }) => {
|
||||
const role = {
|
||||
id: 2,
|
||||
name: "Operations manager",
|
||||
description: "Can manage operations.",
|
||||
created_at: "2026-07-06T08:00:00.000Z",
|
||||
permissions: ["user", "list_orders"],
|
||||
};
|
||||
const calls: PermissionCall[] = [];
|
||||
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "en");
|
||||
});
|
||||
await seedAuthenticatedState(page, "superuser-roles-template-token");
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["superuser", "user", "list_roles", "add_role_permission"],
|
||||
sessionData: {
|
||||
group_id: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await page.route(apiPathPattern("/roles/limited-backoffice-permission-templates"), async (route: Route) => {
|
||||
await route.fulfill(json({ data: limitedBackofficePermissionTemplates }));
|
||||
});
|
||||
|
||||
await page.route(apiPathPattern("/roles"), async (route: Route) => {
|
||||
await route.fulfill(json(roleListEnvelope([role])));
|
||||
});
|
||||
|
||||
await page.route(apiPathPattern("/roles/permissions"), async (route: Route) => {
|
||||
const body = route.request().postDataJSON();
|
||||
const permission = String(body.permission_id || "");
|
||||
calls.push({ action: "add", permission });
|
||||
if (!role.permissions.includes(permission)) {
|
||||
role.permissions = [...role.permissions, permission];
|
||||
}
|
||||
await route.fulfill(json({ data: role }));
|
||||
});
|
||||
|
||||
await page.goto("/superuser/roles", { waitUntil: "domcontentloaded" });
|
||||
await expect(page.getByRole("row", { name: /Operations manager/ })).toBeVisible();
|
||||
|
||||
const row = page.getByRole("row", { name: /Operations manager/ });
|
||||
await row.locator(".action-settings-wheel-trigger").click();
|
||||
await expect(page.getByTestId("role-limited-backoffice-template-actions-2")).toBeVisible();
|
||||
await page.getByTestId("role-template-2-cashier").click();
|
||||
|
||||
await expect(page.getByText("Apply permission template?")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Apply template" }).click();
|
||||
|
||||
await expect
|
||||
.poll(() => calls.map((call) => call.permission))
|
||||
.toEqual(["permissions_list_own", "list_departments", "fetch_order", "statistics_orders_new"]);
|
||||
await expect(page.getByText("Template applied")).toBeVisible();
|
||||
});
|
||||
|
||||
test("disables limited backoffice template actions when a role is already up to date", async ({ page }) => {
|
||||
const role = {
|
||||
id: 3,
|
||||
name: "Cashier role",
|
||||
description: "Current cashier permissions.",
|
||||
created_at: "2026-07-06T08:00:00.000Z",
|
||||
permissions: [...roleTemplatePermissions],
|
||||
};
|
||||
const calls: PermissionCall[] = [];
|
||||
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "en");
|
||||
});
|
||||
await seedAuthenticatedState(page, "superuser-roles-template-up-to-date-token");
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["superuser", "user", "list_roles", "add_role_permission"],
|
||||
sessionData: {
|
||||
group_id: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await page.route(apiPathPattern("/roles/limited-backoffice-permission-templates"), async (route: Route) => {
|
||||
await route.fulfill(json({ data: limitedBackofficePermissionTemplates }));
|
||||
});
|
||||
|
||||
await page.route(apiPathPattern("/roles"), async (route: Route) => {
|
||||
await route.fulfill(json(roleListEnvelope([role])));
|
||||
});
|
||||
|
||||
await page.route(apiPathPattern("/roles/permissions"), async (route: Route) => {
|
||||
calls.push({ action: "add", permission: String(route.request().postDataJSON().permission_id || "") });
|
||||
await route.fulfill(json({ data: role }));
|
||||
});
|
||||
|
||||
await page.goto("/superuser/roles", { waitUntil: "domcontentloaded" });
|
||||
const row = page.getByRole("row", { name: /Cashier role/ });
|
||||
await expect(row).toBeVisible();
|
||||
await row.locator(".action-settings-wheel-trigger").click();
|
||||
|
||||
await expect(page.getByTestId("role-template-3-cashier")).toBeDisabled();
|
||||
expect(calls).toEqual([]);
|
||||
});
|
||||
|
||||
test("@smoke @pr loads grouped permissions, searches, and toggles without a blank page", async ({ page }) => {
|
||||
const { calls } = await bootRolePermissionsPage(page);
|
||||
|
||||
|
||||
@@ -25,6 +25,44 @@ async function bootSuperuser(page, { permissions = ["superuser", "user"], edgeGa
|
||||
await primeSuperuserSession(page);
|
||||
}
|
||||
|
||||
async function waitForLayoutFrame(page) {
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function getActionMenuVerticalPlacement(trigger, menu) {
|
||||
const [triggerBox, menuBox] = await Promise.all([trigger.boundingBox(), menu.boundingBox()]);
|
||||
|
||||
expect(triggerBox).not.toBeNull();
|
||||
expect(menuBox).not.toBeNull();
|
||||
|
||||
const belowGap = menuBox.y - (triggerBox.y + triggerBox.height);
|
||||
const aboveGap = triggerBox.y - (menuBox.y + menuBox.height);
|
||||
const isBelowButton = belowGap >= -2 && belowGap <= 16;
|
||||
const isAboveButton = aboveGap >= -2 && aboveGap <= 16;
|
||||
|
||||
expect(isBelowButton || isAboveButton).toBe(true);
|
||||
expect(menuBox.y >= triggerBox.y + triggerBox.height - 2 || menuBox.y + menuBox.height <= triggerBox.y + 2).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
return isBelowButton ? "below" : "above";
|
||||
}
|
||||
|
||||
async function getSettledActionMenuVerticalPlacement(page, trigger, menu) {
|
||||
await waitForLayoutFrame(page);
|
||||
const firstPlacement = await getActionMenuVerticalPlacement(trigger, menu);
|
||||
await waitForLayoutFrame(page);
|
||||
const secondPlacement = await getActionMenuVerticalPlacement(trigger, menu);
|
||||
expect(secondPlacement).toBe(firstPlacement);
|
||||
|
||||
return secondPlacement;
|
||||
}
|
||||
|
||||
async function installSystemStatusMock(page, snapshot) {
|
||||
await page.route(/\/superuser\/system\/status(?:\?.*)?$/, async (route) => {
|
||||
await route.fulfill(
|
||||
@@ -815,6 +853,38 @@ test.describe("Superuser system status smoke", () => {
|
||||
await expect(page.getByTestId("database-status-card")).toContainText("8.0.36");
|
||||
});
|
||||
|
||||
test("@smoke @pr superuser replication action menu keeps stable vertical placement", async ({ page }) => {
|
||||
await bootSuperuser(page);
|
||||
await installReplicationMock(page);
|
||||
|
||||
await page.goto("/superuser/system/replication");
|
||||
|
||||
await expect(page.getByTestId("replication-management-page")).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
|
||||
await expect(page.getByTestId("replication-host-database-2")).toContainText("mysql-replica-1");
|
||||
|
||||
const replicaActions = page.getByTestId("replication-actions-database-2");
|
||||
const replicaActionTrigger = replicaActions.locator(".action-settings-wheel-trigger");
|
||||
const replicaActionMenu = replicaActions.locator(".dropdown-content");
|
||||
|
||||
await expect(replicaActionTrigger).toBeVisible();
|
||||
await replicaActionTrigger.click();
|
||||
await expect(replicaActions.locator(".dropdown-item-action:visible")).toHaveCount(4);
|
||||
const initialPlacement = await getSettledActionMenuVerticalPlacement(page, replicaActionTrigger, replicaActionMenu);
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(replicaActionTrigger).not.toHaveClass(/action-settings-wheel-trigger--active/);
|
||||
|
||||
await replicaActionTrigger.click();
|
||||
await expect(replicaActions.locator(".dropdown-item-action:visible")).toHaveCount(4);
|
||||
const reopenedPlacement = await getSettledActionMenuVerticalPlacement(
|
||||
page,
|
||||
replicaActionTrigger,
|
||||
replicaActionMenu
|
||||
);
|
||||
|
||||
expect(reopenedPlacement).toBe(initialPlacement);
|
||||
});
|
||||
|
||||
test("@smoke superuser replication management page renders topology and percentages", async ({ page }) => {
|
||||
await bootSuperuser(page);
|
||||
const replicationRequests = await installReplicationMock(page);
|
||||
@@ -863,15 +933,11 @@ test.describe("Superuser system status smoke", () => {
|
||||
await replicaActionTrigger.click();
|
||||
await expect(replicaActions.locator(".dropdown-item-action:visible")).toHaveCount(4);
|
||||
await expect(replicaActions.getByRole("button", { name: "Omdøb" })).toBeVisible();
|
||||
const [triggerBox, menuBox] = await Promise.all([
|
||||
replicaActionTrigger.boundingBox(),
|
||||
replicaActions.locator(".dropdown-content").boundingBox(),
|
||||
]);
|
||||
expect(triggerBox).not.toBeNull();
|
||||
expect(menuBox).not.toBeNull();
|
||||
const isMenuBelowButton = Math.abs(menuBox.y - (triggerBox.y + triggerBox.height)) <= 12;
|
||||
const isMenuAboveButton = Math.abs(menuBox.y + menuBox.height - triggerBox.y) <= 12;
|
||||
expect(isMenuBelowButton || isMenuAboveButton).toBe(true);
|
||||
await getSettledActionMenuVerticalPlacement(
|
||||
page,
|
||||
replicaActionTrigger,
|
||||
replicaActions.locator(".dropdown-content")
|
||||
);
|
||||
await replicaActions.getByRole("button", { name: "Test" }).click();
|
||||
expect(replicationRequests.hostTestKinds).toContain("database");
|
||||
const addHostTabs = page.getByTestId("replication-add-host-tabs");
|
||||
|
||||
@@ -35,6 +35,9 @@ const userEnvelope = (rows: Array<Record<string, unknown>>) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const usersApiPattern =
|
||||
/https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/users|localhost(?::\d+)?\/api\/users|127\.0\.0\.1(?::\d+)?\/api\/users)(?:\?.*)?$/i;
|
||||
|
||||
const overviewUser = {
|
||||
id: 11,
|
||||
customer_number: 12345,
|
||||
@@ -217,25 +220,22 @@ test.describe("Superuser employees list", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await page.route(
|
||||
/https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/users|localhost(?::\d+)?\/api\/users|127\.0\.0\.1(?::\d+)?\/api\/users)(?:\?.*)?$/i,
|
||||
async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
await page.route(usersApiPattern, async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
|
||||
if (request.method() !== "GET") {
|
||||
await route.fulfill(json({ data: { message: "OK" } }));
|
||||
return;
|
||||
}
|
||||
|
||||
const search = url.searchParams.get("search");
|
||||
searchesSeen.push(search);
|
||||
const normalizedSearch = String(search || "").toLowerCase();
|
||||
const rows = normalizedSearch.includes("anna") ? [users[0]] : users;
|
||||
|
||||
await route.fulfill(json(userEnvelope(rows)));
|
||||
if (request.method() !== "GET") {
|
||||
await route.fulfill(json({ data: { message: "OK" } }));
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
const search = url.searchParams.get("search");
|
||||
searchesSeen.push(search);
|
||||
const normalizedSearch = String(search || "").toLowerCase();
|
||||
const rows = normalizedSearch.includes("anna") ? [users[0]] : users;
|
||||
|
||||
await route.fulfill(json(userEnvelope(rows)));
|
||||
});
|
||||
|
||||
await page.goto("/superuser/users", { waitUntil: "domcontentloaded" });
|
||||
|
||||
@@ -258,6 +258,114 @@ test.describe("Superuser employees list", () => {
|
||||
await expect(page.getByTestId("action-settings-wheel-section-customer")).toBeVisible();
|
||||
expect(searchesSeen).toContain("Anna");
|
||||
});
|
||||
|
||||
test("migrates an existing employee account into limited backoffice", async ({ page }, testInfo) => {
|
||||
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
||||
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await seedAuthenticatedState(page, "superuser-users-migration-token");
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["superuser", "user"],
|
||||
sessionData: {
|
||||
group_id: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const legacyEmployee = {
|
||||
id: 13,
|
||||
customer_number: 0,
|
||||
display_name: "Legacy Clerk",
|
||||
group_id: 2,
|
||||
limited_backoffice_managed: false,
|
||||
};
|
||||
const managedEmployee = {
|
||||
id: 14,
|
||||
customer_number: 0,
|
||||
display_name: "Managed Clerk",
|
||||
group_id: 77,
|
||||
limited_backoffice_managed: true,
|
||||
};
|
||||
const customerUser = {
|
||||
id: 15,
|
||||
customer_number: 12345,
|
||||
display_name: "Customer Account",
|
||||
group_id: 2,
|
||||
limited_backoffice_managed: false,
|
||||
};
|
||||
const rows = [legacyEmployee, managedEmployee, customerUser];
|
||||
let usersRequests = 0;
|
||||
let migrationPayload: unknown = null;
|
||||
|
||||
await page.route(usersApiPattern, async (route) => {
|
||||
usersRequests += 1;
|
||||
await route.fulfill(json(userEnvelope(rows)));
|
||||
});
|
||||
await page.route(apiPathPattern("/limited-backoffice/departments"), async (route) => {
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: [
|
||||
{ id: 1, name: "Assigned Depot" },
|
||||
{ id: 2, name: "Remote Depot" },
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
await page.route(apiPathPattern("/limited-backoffice/roles"), async (route) => {
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: [
|
||||
{ key: "viewer", label: "Deactivated" },
|
||||
{ key: "cashier", label: "Cashier" },
|
||||
{ key: "department_admin", label: "Department admin" },
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
await page.route(apiPathPattern("/limited-backoffice/employees/13/migrate"), async (route) => {
|
||||
if (route.request().method() !== "POST") {
|
||||
await route.fulfill(json({ data: { message: "OK" } }));
|
||||
return;
|
||||
}
|
||||
|
||||
migrationPayload = await route.request().postDataJSON();
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: {
|
||||
id: 13,
|
||||
user_id: 13,
|
||||
customer_number: 0,
|
||||
display_name: "Legacy Clerk",
|
||||
role: { key: "cashier", label: "Cashier" },
|
||||
departments: [{ id: 1, name: "Assigned Depot" }],
|
||||
active: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
await page.goto("/superuser/users", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.getByTestId("superuser-users-row-13")).toContainText("Legacy Clerk");
|
||||
await page.locator('[data-testid="superuser-user-actions-13"] .action-settings-wheel-trigger').click();
|
||||
await expect(page.getByTestId("superuser-user-migrate-limited-13")).toBeVisible();
|
||||
await page.getByTestId("superuser-user-migrate-limited-13").click();
|
||||
|
||||
await expect(page.locator(".swal2-popup")).toContainText("Migrate employee");
|
||||
await page.locator(".limited-migration-department").first().check();
|
||||
await page.locator(".swal2-confirm").click();
|
||||
|
||||
await expect.poll(() => migrationPayload).toEqual({ role_key: "cashier", department_ids: [1] });
|
||||
await expect(page.locator(".swal2-popup")).toContainText("Employee migrated");
|
||||
await page.locator(".swal2-confirm").click();
|
||||
await expect.poll(() => usersRequests).toBeGreaterThan(1);
|
||||
|
||||
await page.locator('[data-testid="superuser-user-actions-14"] .action-settings-wheel-trigger').click();
|
||||
await expect(page.getByTestId("superuser-user-migrate-limited-14")).toHaveCount(0);
|
||||
|
||||
await page.locator('[data-testid="superuser-user-actions-15"] .action-settings-wheel-trigger').click();
|
||||
await expect(page.getByTestId("superuser-user-migrate-limited-15")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Superuser user overview", () => {
|
||||
|
||||
@@ -1317,6 +1317,7 @@ function createSelfServeFixture(overrides = {}) {
|
||||
answerRequests: [],
|
||||
commandResponse: { success: true },
|
||||
commandResponses: null,
|
||||
commandResponseDelayMs: 0,
|
||||
commandRequests: [],
|
||||
forceStopResponse: null,
|
||||
forceStopResponses: null,
|
||||
@@ -1483,6 +1484,14 @@ function buildEdgeGatewayOperationSummary(operations = []) {
|
||||
);
|
||||
}
|
||||
|
||||
function invalidateEdgeGatewayRuntimeSnapshot(gateway) {
|
||||
delete gateway.active_operation;
|
||||
delete gateway.recent_operations_summary;
|
||||
delete gateway.version_drift;
|
||||
delete gateway.diagnostics;
|
||||
delete gateway.error_state;
|
||||
}
|
||||
|
||||
function buildEdgeGatewayRuntimeFixture(gateway) {
|
||||
const relayHealth = (gateway.bindings || []).map((binding) => {
|
||||
const fallbackMode = binding.fallback_mode || "PREFER_LOCAL";
|
||||
@@ -1850,6 +1859,7 @@ function settleEdgeGatewayWork(edgeGatewayFixture, gatewayId) {
|
||||
: operation
|
||||
);
|
||||
delete edgeGatewayFixture.pendingDiscoveryByGatewayId[gatewayId];
|
||||
invalidateEdgeGatewayRuntimeSnapshot(gateway);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5401,6 +5411,7 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
|
||||
{ id: Date.now(), created_at: now, action: "GATEWAY_OPERATION_CANCELLED", actor_type: "USER" },
|
||||
...(gateway.audit_logs || []),
|
||||
];
|
||||
invalidateEdgeGatewayRuntimeSnapshot(gateway);
|
||||
Object.assign(gateway, buildEdgeGatewayRuntimeFixture(gateway));
|
||||
await route.fulfill(
|
||||
json({
|
||||
@@ -5567,6 +5578,7 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
|
||||
{ id: Date.now(), created_at: now, action: "GATEWAY_OPERATION_QUEUED", actor_type: "USER" },
|
||||
...(gateway.audit_logs || []),
|
||||
];
|
||||
invalidateEdgeGatewayRuntimeSnapshot(gateway);
|
||||
Object.assign(gateway, buildEdgeGatewayRuntimeFixture(gateway));
|
||||
await route.fulfill(
|
||||
json(
|
||||
@@ -6697,6 +6709,10 @@ export async function mockApi(page, options = {}) {
|
||||
Array.isArray(selfServe.commandResponses) && selfServe.commandResponses.length > 0
|
||||
? selfServe.commandResponses.shift()
|
||||
: selfServe.commandResponse;
|
||||
const commandResponseDelayMs = Array.isArray(selfServe.commandResponseDelayMs)
|
||||
? selfServe.commandResponseDelayMs.shift() || 0
|
||||
: selfServe.commandResponseDelayMs;
|
||||
await maybeDelayFixtureResponse(commandResponseDelayMs);
|
||||
await route.fulfill(json(commandResponse));
|
||||
return;
|
||||
}
|
||||
@@ -6724,6 +6740,51 @@ export async function mockApi(page, options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/user/invoices") && method === "GET") {
|
||||
const invoiceFixture = posFixture || { collectedInvoices: [] };
|
||||
const customerNumber = Number(options.sessionData?.customer_number || 0);
|
||||
const collectedInvoices = (invoiceFixture.collectedInvoices || []).filter((invoice) => {
|
||||
if (!customerNumber) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Number(invoice.customer_number) === customerNumber;
|
||||
});
|
||||
const page = Number(parsedUrl.searchParams.get("page") || 1);
|
||||
const limit = Number(parsedUrl.searchParams.get("limit") || collectedInvoices.length || 100);
|
||||
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: collectedInvoices.slice((page - 1) * limit, page * limit),
|
||||
meta: {
|
||||
pagination: {
|
||||
page,
|
||||
per_page: limit,
|
||||
total: collectedInvoices.length,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/invoices/pdf") && method === "GET") {
|
||||
const invoiceId = String(parsedUrl.searchParams.get("id") || "").trim();
|
||||
if (!invoiceId) {
|
||||
await route.fulfill(json({ message: "Invoice not found" }, 404));
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: {
|
||||
url: `https://pdf.example.test/invoices/${invoiceId}.pdf`,
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/collected-invoices") && method === "GET") {
|
||||
const invoiceFixture = posFixture || { collectedInvoices: [] };
|
||||
const filters = parseFilterExpressions(parsedUrl.searchParams.get("filters") || "");
|
||||
|
||||
+48
-22
@@ -1,20 +1,25 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import {
|
||||
userCredentials,
|
||||
subuserPhoneCredentials,
|
||||
subuserUsernameCredentials,
|
||||
operatorCredentials,
|
||||
customerRegistrationData,
|
||||
driverRegistrationData,
|
||||
invalidCredentials,
|
||||
loginAsUser,
|
||||
loginAsSubuserByPhone,
|
||||
loginAsSubuserByUsername,
|
||||
loginAsOperator,
|
||||
goToUserLogin,
|
||||
goToSubuserLogin,
|
||||
goToOperatorLogin,
|
||||
} from "./fixtures";
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { createPosFixture, mockApi, seedAuthenticatedState } from "./support/network.js";
|
||||
import { loginAsUser } from "./fixtures";
|
||||
|
||||
const USER_HOME_CUSTOMER_NUMBER = 12345679;
|
||||
|
||||
async function gotoUserHomeWithInvoices(page: Page, collectedInvoices: Array<Record<string, unknown>>) {
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
sessionData: {
|
||||
customer_number: USER_HOME_CUSTOMER_NUMBER,
|
||||
display_name: "E2E User",
|
||||
permissions: ["user"],
|
||||
},
|
||||
pos: createPosFixture({
|
||||
collectedInvoices,
|
||||
}),
|
||||
});
|
||||
await seedAuthenticatedState(page, "user-home-invoice-shortcut-token");
|
||||
await page.goto("/user", { waitUntil: "domcontentloaded" });
|
||||
await expect(page).toHaveURL(/\/user(?:\/)?(?:[?#].*)?$/);
|
||||
}
|
||||
|
||||
/** User home Tests */
|
||||
// User home page content tests
|
||||
@@ -41,9 +46,30 @@ test("[PAGES][User][/user] should display the download certificate button", asyn
|
||||
// Check if the "download certificate" element is visible
|
||||
await expect(page.locator("a#download-certificates-button")).toBeVisible();
|
||||
});
|
||||
// Download invoices button visibility test
|
||||
test("[PAGES][User][/user] should display the download invoices button", async ({ page }) => {
|
||||
await loginAsUser(page);
|
||||
// Check if the "download invoices" element is visible
|
||||
await expect(page.locator("a#download-invoices-button")).toBeVisible();
|
||||
test("[PAGES][User][/user] should disable the download invoices button when no invoice exists", async ({ page }) => {
|
||||
await gotoUserHomeWithInvoices(page, []);
|
||||
|
||||
const button = page.locator("button#download-invoices-button");
|
||||
await expect(button).toBeVisible();
|
||||
await expect(button).toBeDisabled();
|
||||
await expect(button).not.toHaveClass(/is-loading/);
|
||||
await expect(page.locator("a#download-invoices-button")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("[PAGES][User][/user] should enable the download invoices button when an invoice exists", async ({ page }) => {
|
||||
await gotoUserHomeWithInvoices(page, [
|
||||
{
|
||||
id: 7001,
|
||||
customer_number: USER_HOME_CUSTOMER_NUMBER,
|
||||
customer_name: "E2E User",
|
||||
total_net_amount: 1250,
|
||||
created_at: "2026-07-01",
|
||||
closed_at: "2026-07-01",
|
||||
},
|
||||
]);
|
||||
|
||||
const link = page.locator("a#download-invoices-button");
|
||||
await expect(link).toBeVisible();
|
||||
await expect(link).toHaveAttribute("href", "/user/invoices");
|
||||
await expect(page.locator("button#download-invoices-button")).toHaveCount(0);
|
||||
});
|
||||
|
||||
@@ -316,6 +316,26 @@ const flushMicrotasks = async () => {
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
const flushDropdownLayout = async () => {
|
||||
for (let index = 0; index < 8; index += 1) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const createRect = ({ left = 0, top = 0, width = 0, height = 0 }) => ({
|
||||
x: left,
|
||||
y: top,
|
||||
top,
|
||||
left,
|
||||
right: left + width,
|
||||
bottom: top + height,
|
||||
width,
|
||||
height,
|
||||
toJSON() {
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
const setDesktopFlyoutViewport = () => {
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
configurable: true,
|
||||
@@ -395,6 +415,95 @@ const mockMenuGeometry = () =>
|
||||
};
|
||||
});
|
||||
|
||||
const mockDropdownPlacementGeometry = ({
|
||||
contentHeight = 180,
|
||||
contentWidth = 240,
|
||||
triggerHeight = 40,
|
||||
triggerLeft = 880,
|
||||
triggerTop = 520,
|
||||
triggerWidth = 80,
|
||||
} = {}) => {
|
||||
let currentContentHeight = contentHeight;
|
||||
const originalScrollHeightDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight");
|
||||
|
||||
Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
|
||||
configurable: true,
|
||||
get() {
|
||||
if (this.classList?.contains("dropdown-content")) {
|
||||
return currentContentHeight;
|
||||
}
|
||||
|
||||
if (originalScrollHeightDescriptor?.get) {
|
||||
return originalScrollHeightDescriptor.get.call(this);
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
});
|
||||
|
||||
const rectSpy = vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function rectMock() {
|
||||
if (this.classList?.contains("dropdown-trigger")) {
|
||||
return createRect({
|
||||
left: triggerLeft,
|
||||
top: triggerTop,
|
||||
width: triggerWidth,
|
||||
height: triggerHeight,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.classList?.contains("dropdown-content")) {
|
||||
const dropdown = this.closest(".dropdown");
|
||||
const menu = this.closest(".dropdown-menu");
|
||||
const fixedTop = Number.parseFloat(menu?.style?.top || "");
|
||||
const maxHeight = Number.parseFloat(this.style?.maxHeight || "");
|
||||
const height = Number.isFinite(maxHeight) ? Math.min(currentContentHeight, maxHeight) : currentContentHeight;
|
||||
const top = Number.isFinite(fixedTop)
|
||||
? fixedTop
|
||||
: dropdown?.classList.contains("is-up")
|
||||
? triggerTop - height
|
||||
: triggerTop + triggerHeight;
|
||||
|
||||
return createRect({
|
||||
left: triggerLeft + triggerWidth - contentWidth,
|
||||
top,
|
||||
width: contentWidth,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.classList?.contains("action-wheel-clip-host")) {
|
||||
return createRect({
|
||||
left: 0,
|
||||
top: 480,
|
||||
width: 1280,
|
||||
height: 120,
|
||||
});
|
||||
}
|
||||
|
||||
return createRect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 1280,
|
||||
height: 900,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
restore() {
|
||||
rectSpy.mockRestore();
|
||||
|
||||
if (originalScrollHeightDescriptor) {
|
||||
Object.defineProperty(HTMLElement.prototype, "scrollHeight", originalScrollHeightDescriptor);
|
||||
} else {
|
||||
delete HTMLElement.prototype.scrollHeight;
|
||||
}
|
||||
},
|
||||
setContentHeight(nextHeight) {
|
||||
currentContentHeight = nextHeight;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const mountDesktopFlyoutButton = (props = {}) => {
|
||||
setDesktopFlyoutViewport();
|
||||
|
||||
@@ -416,6 +525,29 @@ const mountDesktopFlyoutButton = (props = {}) => {
|
||||
});
|
||||
};
|
||||
|
||||
const mountPositionedFlatDropdownButton = (options = {}) => {
|
||||
setCompactViewport();
|
||||
|
||||
return mount(ActionSettingsWheelButton, {
|
||||
props: {
|
||||
order_id: null,
|
||||
refreshFunction: vi.fn(() => Promise.resolve()),
|
||||
...(options.props || {}),
|
||||
},
|
||||
slots: {
|
||||
actions:
|
||||
options.actions || '<button type="button" class="dropdown-item dropdown-item-action">Custom action</button>',
|
||||
},
|
||||
attachTo: options.attachTo,
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
CustomerModal: { template: "<div />" },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const mountFlatDropdownButton = (props = {}) => {
|
||||
setCompactViewport();
|
||||
|
||||
@@ -564,6 +696,93 @@ describe("ActionSettingsWheelButton", () => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("uses the same vertical side when the dropdown is reopened with unchanged geometry", async () => {
|
||||
const geometry = mockDropdownPlacementGeometry({
|
||||
contentHeight: 420,
|
||||
triggerTop: 520,
|
||||
});
|
||||
const wrapper = mountPositionedFlatDropdownButton();
|
||||
await flushDropdownLayout();
|
||||
|
||||
const trigger = wrapper.get(".action-settings-wheel-trigger");
|
||||
|
||||
await trigger.trigger("click");
|
||||
await flushDropdownLayout();
|
||||
expect(wrapper.get(".dropdown").classes()).toContain("is-up");
|
||||
|
||||
await trigger.trigger("click");
|
||||
await flushDropdownLayout();
|
||||
expect(wrapper.get(".dropdown").classes()).not.toContain("is-active");
|
||||
|
||||
await trigger.trigger("click");
|
||||
await flushDropdownLayout();
|
||||
expect(wrapper.get(".dropdown").classes()).toContain("is-up");
|
||||
|
||||
wrapper.unmount();
|
||||
geometry.restore();
|
||||
});
|
||||
|
||||
it("keeps the initially selected side when the menu grows after opening", async () => {
|
||||
const geometry = mockDropdownPlacementGeometry({
|
||||
contentHeight: 120,
|
||||
triggerTop: 520,
|
||||
});
|
||||
const wrapper = mountPositionedFlatDropdownButton();
|
||||
await flushDropdownLayout();
|
||||
|
||||
await wrapper.get(".action-settings-wheel-trigger").trigger("click");
|
||||
await flushDropdownLayout();
|
||||
expect(wrapper.get(".dropdown").classes()).not.toContain("is-up");
|
||||
|
||||
geometry.setContentHeight(420);
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await flushDropdownLayout();
|
||||
|
||||
expect(wrapper.get(".dropdown").classes()).not.toContain("is-up");
|
||||
expect(wrapper.get(".dropdown-content").element.style.maxHeight).toBe("328px");
|
||||
|
||||
wrapper.unmount();
|
||||
geometry.restore();
|
||||
});
|
||||
|
||||
it("keeps fixed-position fallback on the chosen side and clears stale content top", async () => {
|
||||
const host = document.createElement("div");
|
||||
host.className = "action-wheel-clip-host";
|
||||
host.style.overflow = "hidden";
|
||||
document.body.appendChild(host);
|
||||
|
||||
const geometry = mockDropdownPlacementGeometry({
|
||||
contentHeight: 420,
|
||||
triggerTop: 520,
|
||||
});
|
||||
const wrapper = mountPositionedFlatDropdownButton({ attachTo: host });
|
||||
await flushDropdownLayout();
|
||||
|
||||
await wrapper.get(".action-settings-wheel-trigger").trigger("click");
|
||||
await flushDropdownLayout();
|
||||
|
||||
const dropdownContent = wrapper.get(".dropdown-content").element;
|
||||
const dropdownMenu = wrapper.get(".dropdown-menu").element;
|
||||
|
||||
expect(wrapper.get(".dropdown").classes()).toContain("is-up");
|
||||
expect(dropdownMenu.style.position).toBe("fixed");
|
||||
expect(dropdownMenu.style.top).toBe("100px");
|
||||
expect(dropdownContent.style.top).toBe("");
|
||||
|
||||
dropdownContent.style.top = "24px";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await flushDropdownLayout();
|
||||
|
||||
expect(wrapper.get(".dropdown").classes()).toContain("is-up");
|
||||
expect(dropdownMenu.style.position).toBe("fixed");
|
||||
expect(dropdownMenu.style.top).toBe("100px");
|
||||
expect(dropdownContent.style.top).toBe("");
|
||||
|
||||
wrapper.unmount();
|
||||
geometry.restore();
|
||||
host.remove();
|
||||
});
|
||||
|
||||
it("resolves customer user id and reloads customer rules only when the target changes in direct mode", async () => {
|
||||
const wrapper = mount(ActionSettingsWheelButton, {
|
||||
props: {
|
||||
|
||||
@@ -85,6 +85,18 @@ const makeOverview = ({
|
||||
state: "ready",
|
||||
value: 3,
|
||||
out_of: 9,
|
||||
target_percentage: "56.5",
|
||||
target_department_id: "2",
|
||||
},
|
||||
{
|
||||
product_id: 25,
|
||||
slug: "faelg-flex",
|
||||
title: "Fælg flex pr. enhed",
|
||||
state: "ready",
|
||||
value: 0,
|
||||
out_of: 9,
|
||||
target_percentage: 0,
|
||||
target_department_id: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -215,6 +227,15 @@ describe("DepartmentDailyReportObject behavior", () => {
|
||||
slug: "spot-free-lastbil",
|
||||
value: 3,
|
||||
out_of: 9,
|
||||
target_percentage: 56.5,
|
||||
target_department_id: 2,
|
||||
});
|
||||
expect(module.daily_report_products.value[25]).toMatchObject({
|
||||
slug: "faelg-flex",
|
||||
value: 0,
|
||||
out_of: 9,
|
||||
target_percentage: 0,
|
||||
target_department_id: 2,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
// @vitest-environment jsdom
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { nextTick } from "vue";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const dailyReportProductsState = vi.hoisted(() => ({ value: {} }));
|
||||
const selectedDepartmentIdsState = vi.hoisted(() => ({ value: [] }));
|
||||
const refreshOverviewMock = vi.hoisted(() => vi.fn());
|
||||
const hasPermissionMock = vi.hoisted(() => vi.fn());
|
||||
const setProductTargetMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue", () => ({
|
||||
daily_report_products: dailyReportProductsState,
|
||||
selected_department_ids: selectedDepartmentIdsState,
|
||||
refreshOverview: refreshOverviewMock,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
SessionUser: {
|
||||
hasPermission: hasPermissionMock,
|
||||
objects: {
|
||||
department_daily_reports: {
|
||||
functions: {
|
||||
setProductTarget: setProductTargetMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("vue-i18n", async (importOriginal) => {
|
||||
@@ -24,6 +44,12 @@ import DepartmentDashboardDailyReportProductSales from "@/views/dashboards/depar
|
||||
describe("DepartmentDashboardDailyReportProductSales behavior", () => {
|
||||
beforeEach(() => {
|
||||
dailyReportProductsState.value = {};
|
||||
selectedDepartmentIdsState.value = [];
|
||||
refreshOverviewMock.mockReset();
|
||||
hasPermissionMock.mockReset();
|
||||
hasPermissionMock.mockReturnValue(false);
|
||||
setProductTargetMock.mockReset();
|
||||
setProductTargetMock.mockResolvedValue({ data: { data: { target_percentage: null } } });
|
||||
});
|
||||
|
||||
it("renders the API title when the overview payload includes one", () => {
|
||||
@@ -79,4 +105,71 @@ describe("DepartmentDashboardDailyReportProductSales behavior", () => {
|
||||
|
||||
expect(missingTileWrapper.text()).toContain("Tillæg for Specialsæbe - DD");
|
||||
});
|
||||
|
||||
it("shows a saved target percentage below the product percentage", () => {
|
||||
dailyReportProductsState.value = {
|
||||
24: {
|
||||
title: "Spot Free (Lastbil)",
|
||||
value: 3,
|
||||
out_of: 8,
|
||||
state: "ready",
|
||||
message: null,
|
||||
target_percentage: 62.5,
|
||||
target_department_id: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = mount(DepartmentDashboardDailyReportProductSales, {
|
||||
props: {
|
||||
product_id: 24,
|
||||
subtitle: "Dagens salg af produktet",
|
||||
dataTestid: "daily-report-tile-spot-free-lastbil",
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.get('[data-testid="daily-report-tile-spot-free-lastbil-target-button"]').text()).toContain("62.5%");
|
||||
});
|
||||
|
||||
it("opens the target editor from the percentage button and saves through the daily report API", async () => {
|
||||
selectedDepartmentIdsState.value = [1];
|
||||
hasPermissionMock.mockImplementation((permission) => permission === "set_department_daily_report_product_targets");
|
||||
setProductTargetMock.mockResolvedValue({ data: { data: { target_percentage: 70 } } });
|
||||
dailyReportProductsState.value = {
|
||||
24: {
|
||||
title: "Spot Free (Lastbil)",
|
||||
value: 3,
|
||||
out_of: 8,
|
||||
state: "ready",
|
||||
message: null,
|
||||
target_percentage: null,
|
||||
target_department_id: null,
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = mount(DepartmentDashboardDailyReportProductSales, {
|
||||
props: {
|
||||
product_id: 24,
|
||||
subtitle: "Dagens salg af produktet",
|
||||
dataTestid: "daily-report-tile-spot-free-lastbil",
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.get('[data-testid="daily-report-tile-spot-free-lastbil-percentage-button"]').trigger("click");
|
||||
await nextTick();
|
||||
await wrapper.get('[data-testid="daily-report-tile-spot-free-lastbil-target-input"]').setValue("70");
|
||||
await wrapper.find("form").trigger("submit");
|
||||
await nextTick();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(setProductTargetMock).toHaveBeenCalledWith({
|
||||
department_id: 1,
|
||||
product_id: 24,
|
||||
target_percentage: 70,
|
||||
});
|
||||
expect(refreshOverviewMock).toHaveBeenCalledTimes(1);
|
||||
expect(dailyReportProductsState.value[24]).toMatchObject({
|
||||
target_percentage: 70,
|
||||
target_department_id: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// @vitest-environment jsdom
|
||||
import { mount, flushPromises } from "@vue/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
authenticatedRequest: vi.fn(),
|
||||
handleEconomicError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/session/authenticatedRequest.vue", () => ({
|
||||
authenticatedRequest: mocks.authenticatedRequest,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/request/HandleEconomicError.vue", () => ({
|
||||
handleEconomicError: mocks.handleEconomicError,
|
||||
}));
|
||||
|
||||
import GetOrderInvoicePDFButton from "@/components/search/economic/getOrderInvoicePDFButton.vue";
|
||||
|
||||
const LoadButtonWhileAwaitStub = {
|
||||
props: ["disabled", "loadFunction", "actionKey"],
|
||||
template: `
|
||||
<button
|
||||
type="button"
|
||||
:disabled="disabled"
|
||||
:data-action-key="actionKey"
|
||||
@click="loadFunction"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
`,
|
||||
};
|
||||
|
||||
describe("GetOrderInvoicePDFButton", () => {
|
||||
beforeEach(() => {
|
||||
mocks.authenticatedRequest.mockReset();
|
||||
mocks.handleEconomicError.mockReset();
|
||||
vi.stubGlobal("open", vi.fn());
|
||||
});
|
||||
|
||||
it.each([null, undefined, ""])("disables the PDF download when invoice id is %s", async (invoiceId) => {
|
||||
const wrapper = mount(GetOrderInvoicePDFButton, {
|
||||
props: {
|
||||
invoice_id: invoiceId,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
LoadButtonWhileAwait: LoadButtonWhileAwaitStub,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const button = wrapper.get('[data-action-key="economic-invoice-pdf-download"]');
|
||||
expect(button.attributes("disabled")).toBeDefined();
|
||||
|
||||
await button.trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.authenticatedRequest).not.toHaveBeenCalled();
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requests and opens the invoice PDF when invoice id exists", async () => {
|
||||
mocks.authenticatedRequest.mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
url: "https://pdf.example.test/invoices/99101.pdf",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mount(GetOrderInvoicePDFButton, {
|
||||
props: {
|
||||
invoice_id: 99101,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
LoadButtonWhileAwait: LoadButtonWhileAwaitStub,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const button = wrapper.get('[data-action-key="economic-invoice-pdf-download"]');
|
||||
expect(button.attributes("disabled")).toBeUndefined();
|
||||
|
||||
await button.trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.authenticatedRequest).toHaveBeenCalledWith("/invoices/pdf", "GET", {
|
||||
id: 99101,
|
||||
});
|
||||
expect(window.open).toHaveBeenCalledWith("https://pdf.example.test/invoices/99101.pdf", "_blank");
|
||||
});
|
||||
});
|
||||
@@ -30,32 +30,76 @@ describe("Playwright full E2E workflow grouping", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps full-suite runner pressure bounded and diagnosable", () => {
|
||||
it("keeps self-hosted full-suite runner pressure bounded while hosted dispatch can fan out", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source).toMatch(/e2e-full:[\s\S]*?max-parallel: 2/u);
|
||||
expect(source).toContain("FRONTEND_CI_STANDARD_RUNNER");
|
||||
expect(source).toContain("FRONTEND_CI_E2E_RUNNER");
|
||||
expect(source).toContain("FRONTEND_CI_PR_E2E_MAX_PARALLEL");
|
||||
expect(source).toContain("FRONTEND_CI_FULL_E2E_MAX_PARALLEL");
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?max-parallel: \$\{\{ fromJSON\(vars\.FRONTEND_CI_PR_E2E_MAX_PARALLEL/u);
|
||||
expect(source).toMatch(/e2e-full:[\s\S]*?max-parallel: \$\{\{ fromJSON\(vars\.FRONTEND_CI_FULL_E2E_MAX_PARALLEL/u);
|
||||
expect(source).toMatch(/e2e-full:[\s\S]*?PLAYWRIGHT_WORKERS: 1/u);
|
||||
expect(source).toMatch(/e2e-full:[\s\S]*?PLAYWRIGHT_VIDEO_MODE: off/u);
|
||||
expect(source).toContain("scripts/ci/with-systemd-inhibit.sh");
|
||||
expect(source).toContain("scripts/ci/runner-diagnostics.sh");
|
||||
});
|
||||
|
||||
it("diffs pull request changed-area tests against the PR head instead of the merge commit", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source).toContain("PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}");
|
||||
expect(source).toMatch(
|
||||
/if \[\[ "\$EVENT_NAME" == "pull_request" && -n "\$PR_BASE_SHA" \]\]; then[\s\S]*?head_ref="\$PR_HEAD_SHA"/u
|
||||
);
|
||||
expect(source).toContain('echo "head=$head_ref" >> "$GITHUB_OUTPUT"');
|
||||
});
|
||||
|
||||
it("keeps PR E2E runner pressure bounded and diagnosable", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?runs-on: ubuntu-latest/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?max-parallel: 4/u);
|
||||
expect(source).toContain("FRONTEND_CI_E2E_RUNNER");
|
||||
expect(source).toMatch(
|
||||
/e2e-pr:[\s\S]*?max-parallel: \$\{\{ fromJSON\(vars\.FRONTEND_CI_PR_E2E_MAX_PARALLEL \|\| '2'\) \}\}/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("supports repo-variable runner controls while keeping targeted reruns on Ubuntu", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source).toContain("FRONTEND_CI_STANDARD_RUNNER");
|
||||
expect(source).toContain("FRONTEND_CI_E2E_RUNNER");
|
||||
expect(source).toContain('["self-hosted","Linux","X64","pleno","frontend"]');
|
||||
expect(source).toMatch(/e2e-targeted:[\s\S]*?runs-on: ubuntu-24\.04/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?runs-on: \$\{\{ fromJSON\(vars\.FRONTEND_CI_E2E_RUNNER/u);
|
||||
expect(source).toMatch(/e2e-full:[\s\S]*?frontend","docker"\]/u);
|
||||
});
|
||||
|
||||
it("supports targeted manual Playwright reruns on GitHub-hosted runners", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source).toContain("workflow_dispatch:");
|
||||
expect(source).toContain("targeted-then-full");
|
||||
expect(source).toContain("target_specs:");
|
||||
expect(source).toContain("target_projects:");
|
||||
expect(source).toContain("e2e-targeted:");
|
||||
expect(source).toContain("matrix:");
|
||||
expect(source).toContain("project: ${{ fromJSON(inputs.target_projects || '[\"chromium-desktop\"]') }}");
|
||||
expect(source).toMatch(/e2e-targeted:[\s\S]*?runs-on: ubuntu-24\.04/u);
|
||||
expect(source).toContain('npx playwright test "${args[@]}"');
|
||||
expect(source).toContain('"$spec_path" != tests/e2e/*');
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?needs: \[build-and-unit, e2e-targeted\]/u);
|
||||
});
|
||||
|
||||
it("uses a machine-wide Playwright port lock across self-hosted runner processes", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source.match(/PLAYWRIGHT_PORT_LOCK_ROOT:-\/tmp\/pleno-playwright-port-locks/gu)).toHaveLength(2);
|
||||
expect(source.match(/chmod 1777 "\$lock_root"/gu)).toHaveLength(2);
|
||||
expect(source.match(/PLAYWRIGHT_PORT_LOCK_ROOT:-\/tmp\/pleno-playwright-port-locks/gu)).toHaveLength(3);
|
||||
expect(source.match(/chmod 1777 "\$lock_root"/gu)).toHaveLength(3);
|
||||
expect(source).not.toContain("${RUNNER_TEMP:-/tmp}/pleno-playwright-port-locks");
|
||||
});
|
||||
|
||||
|
||||
@@ -31,21 +31,41 @@ describe("Playwright PR mapping", () => {
|
||||
});
|
||||
|
||||
it("maps department notification table changes to the admin notification E2E coverage", () => {
|
||||
const notificationPaginationSpecs = specsFor(
|
||||
"src/components/displays/pagination/models/DepartmentPos/NotificationsPhonePagination.vue"
|
||||
);
|
||||
|
||||
expect(
|
||||
specsFor("src/views/dashboards/departmentDashboard/modules/notifications/DepartmentNotifications.vue")
|
||||
).toContain("tests/e2e/admin-department-notifications.spec.ts");
|
||||
expect(
|
||||
specsFor("src/components/displays/department/notifications/departmentNotificationsPhoneTable.vue")
|
||||
).toContain("tests/e2e/admin-department-notifications.spec.ts");
|
||||
expect(
|
||||
specsFor("src/components/displays/pagination/models/DepartmentPos/NotificationsPhonePagination.vue")
|
||||
).toContain("tests/e2e/admin-department-notifications.spec.ts");
|
||||
expect(notificationPaginationSpecs).toContain("tests/e2e/admin-department-notifications.spec.ts");
|
||||
expect(notificationPaginationSpecs).not.toContain("tests/e2e/pos-flow.spec.js");
|
||||
expect(notificationPaginationSpecs).not.toContain("tests/e2e/pos-mobile-order-flow.spec.js");
|
||||
expect(notificationPaginationSpecs).not.toContain("tests/e2e/admin-pos-orders.spec.ts");
|
||||
});
|
||||
|
||||
it("maps limited backoffice view changes to limited backoffice E2E coverage", () => {
|
||||
it("maps admin daily-report changes to daily-report E2E coverage", () => {
|
||||
expect(
|
||||
specsFor(
|
||||
"src/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportProductSales.vue"
|
||||
)
|
||||
).toContain("tests/e2e/admin-daily-report.spec.ts");
|
||||
expect(specsFor("src/components/session/token/SessionUser/Objects/DepartmentDailyReports.vue")).toContain(
|
||||
"tests/e2e/admin-daily-report.spec.ts"
|
||||
);
|
||||
});
|
||||
|
||||
it("maps limited backoffice view and service changes to limited backoffice E2E coverage", () => {
|
||||
expect(specsFor("src/views/backoffice/LimitedBackofficePrices.vue")).toContain(
|
||||
"tests/e2e/limited-backoffice.spec.ts"
|
||||
);
|
||||
expect(specsFor("src/views/backoffice/components/LimitedBackofficeLayout.vue")).toContain(
|
||||
"tests/e2e/limited-backoffice.spec.ts"
|
||||
);
|
||||
expect(specsFor("src/services/limitedBackoffice.js")).toContain("tests/e2e/limited-backoffice.spec.ts");
|
||||
});
|
||||
|
||||
it("maps superuser role permission page changes to role permissions E2E coverage", () => {
|
||||
@@ -84,6 +104,7 @@ describe("Playwright PR mapping", () => {
|
||||
it("maps customer product rule changes to POS customer rule E2E coverage", () => {
|
||||
expect(specsFor("src/features/customer/customerProductRules.js")).toContain("tests/e2e/pos-customer-rules.spec.js");
|
||||
expect(specsFor("src/components/displays/boxes/ProductBox.vue")).toContain("tests/e2e/pos-customer-rules.spec.js");
|
||||
expect(specsFor("src/components/shop/POSDepartmentProcess.vue")).toContain("tests/e2e/admin-pos-orders.spec.ts");
|
||||
});
|
||||
|
||||
it("maps limited backoffice changes to the limited backoffice E2E coverage", () => {
|
||||
|
||||
@@ -491,6 +491,10 @@ describe("Periode tab contract", () => {
|
||||
expect(xlvaskUsagePaginationSource).toContain('v-if="shouldShowLocalFilters"');
|
||||
expect(xlvaskUsagePaginationSource).toContain('v-if="!hideSearchField && shouldShowLocalFilters"');
|
||||
expect(xlvaskUsagePaginationSource).toContain('v-if="!props.loadAllAtOnce"');
|
||||
expect(xlvaskUsagePaginationSource).toContain("const buildImportUsageParams = () => {");
|
||||
expect(xlvaskUsagePaginationSource).toContain("dateFrom: props.initialDateFrom");
|
||||
expect(xlvaskUsagePaginationSource).toContain("dateTo: props.initialDateTo");
|
||||
expect(xlvaskUsagePaginationSource).toContain("buildImportUsageParams()");
|
||||
});
|
||||
|
||||
it("loads Selvvask selector counts and progress from the selected period", () => {
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
// @vitest-environment jsdom
|
||||
import { flushPromises } from "@vue/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { mountWithApp } from "./helpers/mountWithApp.js";
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const device = {
|
||||
isMobile: vi.fn(() => false),
|
||||
isTablet: vi.fn(() => false),
|
||||
isDesktop: vi.fn(() => true),
|
||||
isWidescreen: vi.fn(() => false),
|
||||
isUltraWideScreen: vi.fn(() => false),
|
||||
};
|
||||
|
||||
return {
|
||||
authenticatedRequest: vi.fn(),
|
||||
sessionUser: {
|
||||
isSubuser: {
|
||||
value: false,
|
||||
},
|
||||
subuser: {
|
||||
selectedGrantCustomerNumber: {
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
value: ["user"],
|
||||
},
|
||||
hasPermission: vi.fn((permission) => permission === "user"),
|
||||
canAccessCustomerFeature: vi.fn(() => true),
|
||||
canManageSubusers: vi.fn(() => false),
|
||||
getName: vi.fn(() => "E2E User"),
|
||||
functions: {
|
||||
device,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/session/authenticatedRequest.vue", () => ({
|
||||
authenticatedRequest: mocks.authenticatedRequest,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
default: mocks.sessionUser,
|
||||
}));
|
||||
|
||||
import UserDashboard from "@/views/dashboards/UserDashboard.vue";
|
||||
|
||||
const flushDashboard = async () => {
|
||||
await flushPromises();
|
||||
await flushPromises();
|
||||
};
|
||||
|
||||
const RouterLinkStub = {
|
||||
props: ["to"],
|
||||
template: "<a v-bind=\"$attrs\" :href=\"typeof to === 'string' ? to : '#'\"><slot /></a>",
|
||||
};
|
||||
|
||||
const mountDashboard = () =>
|
||||
mountWithApp(UserDashboard, {
|
||||
global: {
|
||||
stubs: {
|
||||
RouterLink: RouterLinkStub,
|
||||
"router-link": RouterLinkStub,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe("UserDashboard invoice shortcut", () => {
|
||||
beforeEach(() => {
|
||||
mocks.authenticatedRequest.mockReset();
|
||||
mocks.sessionUser.isSubuser.value = false;
|
||||
mocks.sessionUser.permissions.value = ["user"];
|
||||
mocks.sessionUser.hasPermission.mockReset();
|
||||
mocks.sessionUser.hasPermission.mockImplementation((permission) => permission === "user");
|
||||
mocks.sessionUser.canAccessCustomerFeature.mockReset();
|
||||
mocks.sessionUser.canAccessCustomerFeature.mockReturnValue(true);
|
||||
mocks.sessionUser.canManageSubusers.mockReset();
|
||||
mocks.sessionUser.canManageSubusers.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("keeps the invoice shortcut disabled while invoice availability is loading", () => {
|
||||
mocks.authenticatedRequest.mockReturnValue(new Promise(() => {}));
|
||||
|
||||
const wrapper = mountDashboard();
|
||||
|
||||
const button = wrapper.get("button#download-invoices-button");
|
||||
expect(button.attributes("disabled")).toBeDefined();
|
||||
expect(button.classes()).toContain("is-loading");
|
||||
expect(wrapper.find("a#download-invoices-button").exists()).toBe(false);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("keeps the invoice shortcut disabled when no invoice exists", async () => {
|
||||
mocks.authenticatedRequest.mockResolvedValue({
|
||||
data: {
|
||||
data: [],
|
||||
meta: {
|
||||
pagination: {
|
||||
total: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountDashboard();
|
||||
await flushDashboard();
|
||||
|
||||
const button = wrapper.get("button#download-invoices-button");
|
||||
expect(button.attributes("disabled")).toBeDefined();
|
||||
expect(button.classes()).not.toContain("is-loading");
|
||||
expect(wrapper.find("a#download-invoices-button").exists()).toBe(false);
|
||||
expect(mocks.authenticatedRequest).toHaveBeenCalledWith("/user/invoices", "GET", {
|
||||
page: 1,
|
||||
limit: 1,
|
||||
order: "created_at:desc",
|
||||
});
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("enables the invoice shortcut when at least one invoice exists", async () => {
|
||||
mocks.authenticatedRequest.mockResolvedValue({
|
||||
data: {
|
||||
data: [{ id: 501 }],
|
||||
meta: {
|
||||
pagination: {
|
||||
total: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountDashboard();
|
||||
await flushDashboard();
|
||||
|
||||
const link = wrapper.get("a#download-invoices-button");
|
||||
expect(link.attributes("href")).toBe("/user/invoices");
|
||||
expect(wrapper.find("button#download-invoices-button").exists()).toBe(false);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("fails closed when invoice availability cannot be loaded", async () => {
|
||||
mocks.authenticatedRequest.mockRejectedValue(new Error("Network unavailable"));
|
||||
|
||||
const wrapper = mountDashboard();
|
||||
await flushDashboard();
|
||||
|
||||
const button = wrapper.get("button#download-invoices-button");
|
||||
expect(button.attributes("disabled")).toBeDefined();
|
||||
expect(wrapper.find("a#download-invoices-button").exists()).toBe(false);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user