Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
398045b519 | ||
|
|
acbbac588f | ||
|
|
bfb1386d49 | ||
|
|
f75ff97042 | ||
|
|
4aa7af9716 | ||
|
|
d2682da3cc | ||
|
|
aa1d2ab623 | ||
|
|
e495a099e4 | ||
|
|
929333d264 | ||
|
|
d2cbf823d8 | ||
|
|
ba580e43e6 | ||
|
|
c4e29a896a | ||
|
|
3c49cec213 | ||
|
|
9f7ebad5fe | ||
|
|
c237fce38d | ||
|
|
8acecc61cd | ||
|
|
63149b7597 | ||
|
|
98d0fbc8c9 | ||
|
|
a331eeadcb | ||
|
|
33e109b131 | ||
|
|
68b4328c72 | ||
|
|
dd8a0a1952 | ||
|
|
fef1fc3c12 | ||
|
|
b58c1eb9ec | ||
|
|
27994d6ac6 | ||
|
|
9d6978e9c7 | ||
|
|
62ae4bc890 | ||
|
|
c2ce6a859b | ||
|
|
aa5e0a5507 | ||
|
|
6c1c1def78 | ||
|
|
1a232183a8 | ||
|
|
cfa7d5be69 | ||
|
|
bb375eb149 | ||
|
|
7e5e6ce680 | ||
|
|
1032f8b4f1 | ||
|
|
744a56c8b2 | ||
|
|
fa1e915b9c | ||
|
|
1a9e7503ef | ||
|
|
c41954993c | ||
|
|
a7c8fc3bff | ||
|
|
6002f97fb6 | ||
|
|
4378dad2b9 | ||
|
|
3f42b77d13 | ||
|
|
f2b66ff19f | ||
|
|
2fd0893773 | ||
|
|
ce844137a0 | ||
|
|
1a1c3db8e5 |
+245
-14
@@ -6,6 +6,39 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
workflow_dispatch:
|
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:
|
schedule:
|
||||||
- cron: "0 2 * * *"
|
- cron: "0 2 * * *"
|
||||||
|
|
||||||
@@ -13,16 +46,22 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
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
|
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:
|
jobs:
|
||||||
format-tests:
|
format-tests:
|
||||||
# CI runs on the repository's self-hosted runner pool.
|
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
|
||||||
runs-on: [self-hosted, Linux, X64, pleno, frontend]
|
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Repair self-hosted workspace permissions
|
- name: Repair self-hosted workspace permissions
|
||||||
|
if: ${{ contains(vars.FRONTEND_CI_STANDARD_RUNNER || 'self-hosted', 'self-hosted') }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||||
@@ -55,10 +94,11 @@ jobs:
|
|||||||
|
|
||||||
build-and-unit:
|
build-and-unit:
|
||||||
needs: format-tests
|
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
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Repair self-hosted workspace permissions
|
- name: Repair self-hosted workspace permissions
|
||||||
|
if: ${{ contains(vars.FRONTEND_CI_STANDARD_RUNNER || 'self-hosted', 'self-hosted') }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||||
@@ -97,15 +137,195 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
VITEST_BATCH_SIZE: 5
|
VITEST_BATCH_SIZE: 5
|
||||||
|
|
||||||
e2e-pr:
|
e2e-targeted:
|
||||||
if: github.event_name != 'schedule'
|
if: >
|
||||||
|
github.event_name == 'workflow_dispatch' &&
|
||||||
|
(inputs.mode == 'targeted' || inputs.mode == 'targeted-then-full')
|
||||||
needs: build-and-unit
|
needs: build-and-unit
|
||||||
name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }}
|
name: E2E-targeted-${{ matrix.project }}
|
||||||
runs-on: [self-hosted, Linux, X64, pleno, frontend, docker]
|
# Use GitHub-hosted runners to avoid self-hosted desktop contention and sleep/power events.
|
||||||
timeout-minutes: 30
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 35
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 4
|
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: ${{ fromJSON(vars.FRONTEND_CI_E2E_RUNNER || '["self-hosted","Linux","X64","pleno","frontend","docker"]') }}
|
||||||
|
timeout-minutes: 45
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: ${{ fromJSON(vars.FRONTEND_CI_PR_E2E_MAX_PARALLEL || '2') }}
|
||||||
matrix:
|
matrix:
|
||||||
suite: [core, changed]
|
suite: [core, changed]
|
||||||
project: [chromium-desktop, chromium-mobile]
|
project: [chromium-desktop, chromium-mobile]
|
||||||
@@ -116,6 +336,7 @@ jobs:
|
|||||||
PLAYWRIGHT_VIDEO_MODE: on-first-retry
|
PLAYWRIGHT_VIDEO_MODE: on-first-retry
|
||||||
steps:
|
steps:
|
||||||
- name: Repair self-hosted workspace permissions
|
- name: Repair self-hosted workspace permissions
|
||||||
|
if: ${{ contains(vars.FRONTEND_CI_E2E_RUNNER || 'self-hosted', 'self-hosted') }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||||
@@ -149,11 +370,14 @@ jobs:
|
|||||||
zero_sha="0000000000000000000000000000000000000000"
|
zero_sha="0000000000000000000000000000000000000000"
|
||||||
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_BASE_SHA" ]]; then
|
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_BASE_SHA" ]]; then
|
||||||
base_ref="$PR_BASE_SHA"
|
base_ref="$PR_BASE_SHA"
|
||||||
|
head_ref="$PR_HEAD_SHA"
|
||||||
elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then
|
elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then
|
||||||
git fetch --no-tags --prune origin "$DEFAULT_BRANCH"
|
git fetch --no-tags --prune origin "$DEFAULT_BRANCH"
|
||||||
base_ref="origin/$DEFAULT_BRANCH"
|
base_ref="origin/$DEFAULT_BRANCH"
|
||||||
|
head_ref="$HEAD_SHA"
|
||||||
else
|
else
|
||||||
base_ref="$PUSH_BEFORE_SHA"
|
base_ref="$PUSH_BEFORE_SHA"
|
||||||
|
head_ref="$HEAD_SHA"
|
||||||
fi
|
fi
|
||||||
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_HEAD_SHA" ]]; then
|
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_HEAD_SHA" ]]; then
|
||||||
head_ref="$PR_HEAD_SHA"
|
head_ref="$PR_HEAD_SHA"
|
||||||
@@ -288,15 +512,21 @@ jobs:
|
|||||||
if: >
|
if: >
|
||||||
always() &&
|
always() &&
|
||||||
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch) &&
|
(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' &&
|
needs.build-and-unit.result == 'success' &&
|
||||||
(github.event_name == 'schedule' || needs.e2e-pr.result == 'success')
|
(github.event_name == 'schedule' || needs.e2e-pr.result == 'success') &&
|
||||||
needs: [build-and-unit, e2e-pr]
|
(
|
||||||
|
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 }}
|
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
|
timeout-minutes: 60
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 2
|
max-parallel: ${{ fromJSON(vars.FRONTEND_CI_FULL_E2E_MAX_PARALLEL || '1') }}
|
||||||
matrix:
|
matrix:
|
||||||
browser: [chromium, webkit, firefox]
|
browser: [chromium, webkit, firefox]
|
||||||
device: [mobile, desktop, tablet]
|
device: [mobile, desktop, tablet]
|
||||||
@@ -318,6 +548,7 @@ jobs:
|
|||||||
PLAYWRIGHT_VIDEO_MODE: off
|
PLAYWRIGHT_VIDEO_MODE: off
|
||||||
steps:
|
steps:
|
||||||
- name: Repair self-hosted workspace permissions
|
- name: Repair self-hosted workspace permissions
|
||||||
|
if: ${{ contains(vars.FRONTEND_CI_E2E_RUNNER || 'self-hosted', 'self-hosted') }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||||
|
|||||||
@@ -2415,6 +2415,12 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
description: Contact person name
|
description: Contact person name
|
||||||
example: "Mikkel"
|
example: "Mikkel"
|
||||||
|
ean:
|
||||||
|
type: string
|
||||||
|
description: Optional EAN used for e-invoicing in e-conomic
|
||||||
|
maxLength: 13
|
||||||
|
pattern: '^[0-9]{1,13}$'
|
||||||
|
example: "5790001234567"
|
||||||
g_recaptcha_response:
|
g_recaptcha_response:
|
||||||
type: string
|
type: string
|
||||||
description: reCAPTCHA verification token
|
description: reCAPTCHA verification token
|
||||||
@@ -8189,6 +8195,12 @@ paths:
|
|||||||
email: {type: string}
|
email: {type: string}
|
||||||
phone: {type: integer}
|
phone: {type: integer}
|
||||||
name: {type: string}
|
name: {type: string}
|
||||||
|
ean:
|
||||||
|
type: string
|
||||||
|
description: Optional EAN used for e-invoicing in e-conomic
|
||||||
|
maxLength: 13
|
||||||
|
pattern: '^[0-9]{1,13}$'
|
||||||
|
example: "5790001234567"
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Success
|
description: Success
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export const sourceMappings = [
|
|||||||
projects: chromiumProjects,
|
projects: chromiumProjects,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "backoffice",
|
name: "limited-backoffice",
|
||||||
patterns: [/^src\/views\/backoffice\//u],
|
patterns: [/^src\/views\/backoffice\//u, /^src\/services\/limitedBackoffice\.js$/u],
|
||||||
specs: ["tests/e2e/limited-backoffice.spec.ts"],
|
specs: ["tests/e2e/limited-backoffice.spec.ts"],
|
||||||
projects: chromiumProjects,
|
projects: chromiumProjects,
|
||||||
},
|
},
|
||||||
@@ -43,6 +43,12 @@ export const sourceMappings = [
|
|||||||
specs: ["tests/e2e/superuser-roles-permissions.spec.ts"],
|
specs: ["tests/e2e/superuser-roles-permissions.spec.ts"],
|
||||||
projects: chromiumProjects,
|
projects: chromiumProjects,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "superuser-users",
|
||||||
|
patterns: [/^src\/views\/dashboards\/superUserDashboard\/user\//u],
|
||||||
|
specs: ["tests/e2e/superuser-users.spec.ts"],
|
||||||
|
projects: chromiumProjects,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "superuser-dashboard",
|
name: "superuser-dashboard",
|
||||||
patterns: [
|
patterns: [
|
||||||
@@ -89,8 +95,20 @@ export const sourceMappings = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "pos",
|
name: "pos",
|
||||||
patterns: [/\/pos[/-]/iu, /POS/iu, /^src\/assets\/pos\.css$/u],
|
patterns: [
|
||||||
specs: ["tests/e2e/pos-flow.spec.js", "tests/e2e/pos-mobile-order-flow.spec.js", "tests/e2e/admin-pos-orders.spec.ts"],
|
/(?:^|[/_.-])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,
|
||||||
|
/^src\/services\/economicCustomerIdentifiers\.js$/u,
|
||||||
|
],
|
||||||
|
specs: [
|
||||||
|
"tests/e2e/pos-flow.spec.js",
|
||||||
|
"tests/e2e/pos-mobile-order-flow.spec.js",
|
||||||
|
"tests/e2e/admin-pos-orders.spec.ts",
|
||||||
|
"tests/e2e/pos-customer-rules.spec.js",
|
||||||
|
],
|
||||||
projects: chromiumProjects,
|
projects: chromiumProjects,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -102,12 +120,22 @@ export const sourceMappings = [
|
|||||||
{
|
{
|
||||||
name: "admin-department-notifications",
|
name: "admin-department-notifications",
|
||||||
patterns: [
|
patterns: [
|
||||||
|
/^src\/views\/dashboards\/departmentDashboard\/modules\/notifications\/DepartmentNotifications\.vue$/u,
|
||||||
/^src\/components\/displays\/department\/notifications\//u,
|
/^src\/components\/displays\/department\/notifications\//u,
|
||||||
/^src\/components\/displays\/pagination\/models\/DepartmentPos\/NotificationsPhonePagination\.vue$/u,
|
/^src\/components\/displays\/pagination\/models\/DepartmentPos\/NotificationsPhonePagination\.vue$/u,
|
||||||
],
|
],
|
||||||
specs: ["tests/e2e/admin-department-notifications.spec.ts"],
|
specs: ["tests/e2e/admin-department-notifications.spec.ts"],
|
||||||
projects: chromiumProjects,
|
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",
|
name: "limited-backoffice",
|
||||||
patterns: [/^src\/views\/backoffice\/LimitedBackoffice/u],
|
patterns: [/^src\/views\/backoffice\/LimitedBackoffice/u],
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Swal from "sweetalert2";
|
|||||||
import CustomerProductDiscountDisplay
|
import CustomerProductDiscountDisplay
|
||||||
from "@/components/displays/department/pos/displays/CustomerProductDiscountDisplay.vue";
|
from "@/components/displays/department/pos/displays/CustomerProductDiscountDisplay.vue";
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { isProductRestrictedForCustomer } from "@/features/customer/customerProductRules.js";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const expandIcon = ref(null);
|
const expandIcon = ref(null);
|
||||||
@@ -83,23 +84,6 @@ const hasRestrictAdditionalServices = () => {
|
|||||||
return customerAttributesSafe.value.some((attribute) => attribute.attribute === 'restrictAdditionalServices');
|
return customerAttributesSafe.value.some((attribute) => attribute.attribute === 'restrictAdditionalServices');
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasRestrictSpotFree = () => {
|
|
||||||
return customerAttributesSafe.value.some((attribute) => attribute.attribute === 'restrictSpotFree');
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasRestrictInteriorCleaning = () => {
|
|
||||||
return customerAttributesSafe.value.some((attribute) => attribute.attribute === 'restrictInteriorCleaning');
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasRestrictTankCleaning = () => {
|
|
||||||
return customerAttributesSafe.value.some((attribute) => attribute.attribute === 'restrictTankCleaning');
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasOnlyTankCleaning = () => {
|
|
||||||
return customerAttributesSafe.value.some((attribute) => attribute.attribute === 'onlyTankCleaning');
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On before add product addon
|
* On before add product addon
|
||||||
* This is used to check if adding a product has any requirements.
|
* This is used to check if adding a product has any requirements.
|
||||||
@@ -162,37 +146,19 @@ const doesAddonHaveNoteRequirement = (addon) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isAddonRestricted = (addon) => {
|
const isAddonRestricted = (addon) => {
|
||||||
const addonName = getAddonName(addon).toLowerCase();
|
const addonProduct = getAddonProduct(addon);
|
||||||
// Check if the addon has "Spot Free" in the name
|
return isProductRestrictedForCustomer({
|
||||||
if (hasRestrictSpotFree() && addonName.includes('spot free')) {
|
...addonProduct,
|
||||||
return true;
|
name: getAddonName(addon) || addonProduct.name,
|
||||||
}
|
category: addonProduct.category ?? addon?.category,
|
||||||
// Check if the addon has "Indvendig vask" in the name
|
}, customerAttributesSafe.value);
|
||||||
if (hasRestrictInteriorCleaning() && addonName.includes('indvendig vask')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isProductRestricted = () => {
|
const isProductRestricted = () => {
|
||||||
const productName = String(props.name || '').toLowerCase();
|
return isProductRestrictedForCustomer({
|
||||||
// Check if the product has "Spot Free" in the name
|
...props.product,
|
||||||
if (hasRestrictSpotFree() && productName.includes('spot free')) {
|
name: props.name || props.product?.name,
|
||||||
return true;
|
}, customerAttributesSafe.value);
|
||||||
}
|
|
||||||
// Check if the product has "Indvendig vask" in the name
|
|
||||||
if (hasRestrictInteriorCleaning() && productName.includes('indvendig vask')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Check if the product category is 5 (Tank Cleaning)
|
|
||||||
if (props.product?.category === 5) {
|
|
||||||
if (hasRestrictTankCleaning()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!hasOnlyTankCleaning()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const orderByOrderPriority = (addons) => {
|
const orderByOrderPriority = (addons) => {
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ const dropdownRoot = ref(null);
|
|||||||
const dropdownContent = ref(null);
|
const dropdownContent = ref(null);
|
||||||
const isDropdownOpen = ref(false);
|
const isDropdownOpen = ref(false);
|
||||||
const dropdownInstanceId = Math.random().toString(36).substring(2, 15);
|
const dropdownInstanceId = Math.random().toString(36).substring(2, 15);
|
||||||
|
const dropdownPlacement = ref("bottom");
|
||||||
|
const isDropdownPlacementLocked = ref(false);
|
||||||
const shouldOpenDropdownUp = ref(false);
|
const shouldOpenDropdownUp = ref(false);
|
||||||
const dropdownMaxHeight = ref(null);
|
const dropdownMaxHeight = ref(null);
|
||||||
const isDesktopFlyoutLayout = ref(false);
|
const isDesktopFlyoutLayout = ref(false);
|
||||||
@@ -140,6 +142,7 @@ const previewRequestsInFlight = new Set();
|
|||||||
const generatedObjectUrls = new Set();
|
const generatedObjectUrls = new Set();
|
||||||
let previewStateGeneration = 0;
|
let previewStateGeneration = 0;
|
||||||
let contentResizeObserver = null;
|
let contentResizeObserver = null;
|
||||||
|
let dropdownLayoutUpdateId = 0;
|
||||||
const desktopFlyoutMinViewportWidth = 1400;
|
const desktopFlyoutMinViewportWidth = 1400;
|
||||||
const desktopFlyoutRootPanelWidthRem = 15;
|
const desktopFlyoutRootPanelWidthRem = 15;
|
||||||
const desktopFlyoutSubmenuWidthRem = 17;
|
const desktopFlyoutSubmenuWidthRem = 17;
|
||||||
@@ -230,6 +233,8 @@ const dropdownContentStyle = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resetDropdownLayout = () => {
|
const resetDropdownLayout = () => {
|
||||||
|
dropdownPlacement.value = "bottom";
|
||||||
|
isDropdownPlacementLocked.value = false;
|
||||||
shouldOpenDropdownUp.value = false;
|
shouldOpenDropdownUp.value = false;
|
||||||
dropdownMaxHeight.value = null;
|
dropdownMaxHeight.value = null;
|
||||||
isFixedPosition.value = false;
|
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 updateDropdownLayout = async () => {
|
||||||
|
const updateId = ++dropdownLayoutUpdateId;
|
||||||
|
|
||||||
if (!isDropdownOpen.value || !dropdownRoot.value || !dropdownContent.value) {
|
if (!isDropdownOpen.value || !dropdownRoot.value || !dropdownContent.value) {
|
||||||
resetDropdownLayout();
|
resetDropdownLayout();
|
||||||
return;
|
return;
|
||||||
@@ -299,35 +332,41 @@ const updateDropdownLayout = async () => {
|
|||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
|
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownRoot.value || !dropdownContent.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const triggerElement = dropdownRoot.value.querySelector(".dropdown-trigger");
|
const triggerElement = dropdownRoot.value.querySelector(".dropdown-trigger");
|
||||||
const triggerRect = (triggerElement ?? dropdownRoot.value).getBoundingClientRect();
|
const triggerRect = (triggerElement ?? dropdownRoot.value).getBoundingClientRect();
|
||||||
syncDesktopFlyoutState(triggerRect);
|
syncDesktopFlyoutState(triggerRect);
|
||||||
syncDesktopFlyoutPosition();
|
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
if (!dropdownContent.value) {
|
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownContent.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewportInsets = getViewportInsets();
|
const viewportInsets = getViewportInsets();
|
||||||
const viewportTop = viewportInsets.top;
|
const viewportTop = viewportInsets.top;
|
||||||
const viewportBottom = window.innerHeight - viewportInsets.bottom;
|
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 menuHeight = Math.ceil(dropdownContent.value.scrollHeight);
|
||||||
const openUpward = menuHeight > spaceBelow && spaceAbove > spaceBelow;
|
lockDropdownPlacement(triggerRect, menuHeight, viewportInsets);
|
||||||
const availableHeight = openUpward ? spaceAbove : spaceBelow;
|
const availableHeight = getDropdownAvailableHeight(triggerRect, viewportInsets);
|
||||||
const nextMaxHeight = availableHeight > 0 && menuHeight > availableHeight ? Math.floor(availableHeight) : null;
|
const nextMaxHeight = availableHeight > 0 && menuHeight > availableHeight ? Math.floor(availableHeight) : null;
|
||||||
|
|
||||||
shouldOpenDropdownUp.value = openUpward;
|
|
||||||
dropdownMaxHeight.value = nextMaxHeight;
|
dropdownMaxHeight.value = nextMaxHeight;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownContent.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
syncDesktopFlyoutPosition();
|
syncDesktopFlyoutPosition();
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
if (!dropdownContent.value) {
|
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownContent.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +381,12 @@ const updateDropdownLayout = async () => {
|
|||||||
const boundedHeight = Math.floor(renderedMenuRect.height - overflowAbove - overflowBelow);
|
const boundedHeight = Math.floor(renderedMenuRect.height - overflowAbove - overflowBelow);
|
||||||
dropdownMaxHeight.value = boundedHeight > 0 ? boundedHeight : 1;
|
dropdownMaxHeight.value = boundedHeight > 0 ? boundedHeight : 1;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
syncDesktopFlyoutPosition();
|
syncDesktopFlyoutPosition();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1465,7 +1510,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
contentResizeObserver = new ResizeObserver(() => {
|
contentResizeObserver = new ResizeObserver(() => {
|
||||||
if (isDropdownOpen.value) {
|
if (isDropdownOpen.value) {
|
||||||
syncDesktopFlyoutPosition();
|
void updateDropdownLayout();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2399,7 +2444,7 @@ watch(
|
|||||||
() => {
|
() => {
|
||||||
if (isDropdownOpen.value) {
|
if (isDropdownOpen.value) {
|
||||||
void nextTick(() => {
|
void nextTick(() => {
|
||||||
syncDesktopFlyoutPosition();
|
void updateDropdownLayout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2507,7 +2552,6 @@ const syncDesktopFlyoutPosition = () => {
|
|||||||
|
|
||||||
const triggerRect = triggerEl.getBoundingClientRect();
|
const triggerRect = triggerEl.getBoundingClientRect();
|
||||||
const contentHeight = dropdownContentEl.scrollHeight;
|
const contentHeight = dropdownContentEl.scrollHeight;
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
const viewportWidth = window.innerWidth;
|
const viewportWidth = window.innerWidth;
|
||||||
const contentRect = dropdownContentEl.getBoundingClientRect();
|
const contentRect = dropdownContentEl.getBoundingClientRect();
|
||||||
|
|
||||||
@@ -2537,12 +2581,13 @@ const syncDesktopFlyoutPosition = () => {
|
|||||||
|
|
||||||
const viewportInsets = getViewportInsets();
|
const viewportInsets = getViewportInsets();
|
||||||
const viewportTop = viewportInsets.top;
|
const viewportTop = viewportInsets.top;
|
||||||
const viewportBottom = viewportHeight - viewportInsets.bottom;
|
const effectiveContentHeight = dropdownMaxHeight.value
|
||||||
let top = triggerRect.bottom;
|
? Math.min(contentHeight, dropdownMaxHeight.value)
|
||||||
|
: contentHeight;
|
||||||
if (top + contentHeight > viewportBottom) {
|
const top =
|
||||||
top = Math.max(viewportTop, triggerRect.top - contentHeight);
|
dropdownPlacement.value === "top"
|
||||||
}
|
? Math.max(viewportTop, triggerRect.top - effectiveContentHeight)
|
||||||
|
: triggerRect.bottom;
|
||||||
|
|
||||||
const nextFixedStyles = {
|
const nextFixedStyles = {
|
||||||
top: `${top}px`,
|
top: `${top}px`,
|
||||||
@@ -2558,18 +2603,8 @@ const syncDesktopFlyoutPosition = () => {
|
|||||||
fixedPositionStyles.value = nextFixedStyles;
|
fixedPositionStyles.value = nextFixedStyles;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const contentRect = dropdownContentEl.getBoundingClientRect();
|
if (dropdownContentEl.style.top !== "") {
|
||||||
const contentBottom = contentRect.top + contentRect.height;
|
dropdownContentEl.style.top = "";
|
||||||
|
|
||||||
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 = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+20
-4
@@ -24,8 +24,24 @@ const onCustomerChange = () => {
|
|||||||
isCustomerSelected.value = false;
|
isCustomerSelected.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseValue = (value) => {
|
const parseFixedPriceValue = (value) => {
|
||||||
let tmp_value = parseFloat(value).toFixed(2);
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number.parseInt(String(value), 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatNumber = (value) => Number.isInteger(value) ? String(value) : value.toFixed(2);
|
||||||
|
|
||||||
|
const parseValue = (discount) => {
|
||||||
|
const fixedPrice = parseFixedPriceValue(discount.fixed_price);
|
||||||
|
if (fixedPrice !== null) {
|
||||||
|
return `${formatNumber(fixedPrice)} Kr.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tmp_value = parseFloat(discount.percentage).toFixed(2);
|
||||||
// Add the percentage sign
|
// Add the percentage sign
|
||||||
return `${tmp_value}%`;
|
return `${tmp_value}%`;
|
||||||
}
|
}
|
||||||
@@ -78,7 +94,7 @@ watch(customer_id, onCustomerChange, { immediate: true });
|
|||||||
<!--<span class="icon is-small mr-1">
|
<!--<span class="icon is-small mr-1">
|
||||||
<i class="fas fa-percent" aria-hidden="true"></i>
|
<i class="fas fa-percent" aria-hidden="true"></i>
|
||||||
</span> -->
|
</span> -->
|
||||||
<span>{{ parseValue(discount.percentage) }}</span>
|
<span>{{ parseValue(discount) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,4 +102,4 @@ watch(customer_id, onCustomerChange, { immediate: true });
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ const discount_product = ref(null);
|
|||||||
const discount_category = ref(null);
|
const discount_category = ref(null);
|
||||||
// The global discount (if any)
|
// The global discount (if any)
|
||||||
const discount_global = ref(null);
|
const discount_global = ref(null);
|
||||||
|
// The fixed product price (if any)
|
||||||
|
const fixed_product_price = ref(null);
|
||||||
|
|
||||||
// Show the discounts dropdown
|
// Show the discounts dropdown
|
||||||
const showDiscountsDropdown = ref(false);
|
const showDiscountsDropdown = ref(false);
|
||||||
@@ -49,6 +51,19 @@ const hasGlobalDiscount = () => {
|
|||||||
return discount_global.value > 0;
|
return discount_global.value > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasFixedProductPrice = () => {
|
||||||
|
return fixed_product_price.value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseFixedPriceValue = (value) => {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number.parseInt(String(value), 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
// Set the product discount
|
// Set the product discount
|
||||||
const setProductDiscount = (percentage) => {
|
const setProductDiscount = (percentage) => {
|
||||||
// Check if the percentage is a number
|
// Check if the percentage is a number
|
||||||
@@ -83,6 +98,16 @@ const setGlobalDiscount = (percentage) => {
|
|||||||
discount_global.value = percentage;
|
discount_global.value = percentage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setFixedProductPrice = (fixedPrice) => {
|
||||||
|
const parsedFixedPrice = parseFixedPriceValue(fixedPrice);
|
||||||
|
if (parsedFixedPrice === null) {
|
||||||
|
fixed_product_price.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed_product_price.value = parsedFixedPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Debugging function
|
// Debugging function
|
||||||
const getDiscountDebug = () => {
|
const getDiscountDebug = () => {
|
||||||
@@ -101,9 +126,15 @@ const getDiscountDebug = () => {
|
|||||||
|
|
||||||
// Parse the customer discounts
|
// Parse the customer discounts
|
||||||
const parseCustomerDiscounts = () => {
|
const parseCustomerDiscounts = () => {
|
||||||
const tmp_discount_product = props.customer_discounts.find((discount) => discount.product_or_category_id === props.product.id && !discount.is_category)
|
discount_product.value = null;
|
||||||
const tmp_discount_category = props.customer_discounts.find((discount) => discount.product_or_category_id === props.product.category && discount.is_category);
|
discount_category.value = null;
|
||||||
const tmp_discount_global = props.customer_discounts.find((discount) => discount.id === 999999 && discount.is_category);
|
discount_global.value = null;
|
||||||
|
fixed_product_price.value = null;
|
||||||
|
|
||||||
|
const customerDiscounts = Array.isArray(props.customer_discounts) ? props.customer_discounts : [];
|
||||||
|
const tmp_discount_product = customerDiscounts.find((discount) => discount.product_or_category_id == props.product.id && Number(discount.is_category) === 0)
|
||||||
|
const tmp_discount_category = customerDiscounts.find((discount) => discount.product_or_category_id == props.product.category && Number(discount.is_category) === 1);
|
||||||
|
const tmp_discount_global = customerDiscounts.find((discount) => discount.id === 999999 && Number(discount.is_category) === 1);
|
||||||
|
|
||||||
//console.log("Product discount: ", tmp_discount_product);
|
//console.log("Product discount: ", tmp_discount_product);
|
||||||
//console.log("Category discount: ", tmp_discount_category);
|
//console.log("Category discount: ", tmp_discount_category);
|
||||||
@@ -112,6 +143,7 @@ const parseCustomerDiscounts = () => {
|
|||||||
// Set the product discount
|
// Set the product discount
|
||||||
if (tmp_discount_product) {
|
if (tmp_discount_product) {
|
||||||
setProductDiscount(tmp_discount_product.percentage);
|
setProductDiscount(tmp_discount_product.percentage);
|
||||||
|
setFixedProductPrice(tmp_discount_product.fixed_price);
|
||||||
}
|
}
|
||||||
// Set the category discount
|
// Set the category discount
|
||||||
if (tmp_discount_category) {
|
if (tmp_discount_category) {
|
||||||
@@ -125,6 +157,10 @@ const parseCustomerDiscounts = () => {
|
|||||||
|
|
||||||
// Get the best discount for the customer
|
// Get the best discount for the customer
|
||||||
const getBestDiscount = () => {
|
const getBestDiscount = () => {
|
||||||
|
if (hasFixedProductPrice()) {
|
||||||
|
highestEligibleDiscount.value = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
// Set the best discount to 0
|
// Set the best discount to 0
|
||||||
let tmp_best_discount = 0;
|
let tmp_best_discount = 0;
|
||||||
// Check if the product discount is higher than the current best discount
|
// Check if the product discount is higher than the current best discount
|
||||||
@@ -182,13 +218,32 @@ watch(() => props.customer_discounts, () => {
|
|||||||
<span
|
<span
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-controls="dropdown-menu"
|
aria-controls="dropdown-menu"
|
||||||
v-if="highestEligibleDiscount > 0"
|
v-if="hasFixedProductPrice()"
|
||||||
|
class="tag is-success is-light is-text text-can-not-select"
|
||||||
|
>{{ fixed_product_price }} Kr.</span>
|
||||||
|
<span
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-controls="dropdown-menu"
|
||||||
|
v-else-if="highestEligibleDiscount > 0"
|
||||||
class="tag is-warning is-light is-text text-can-not-select"
|
class="tag is-warning is-light is-text text-can-not-select"
|
||||||
> -{{ highestEligibleDiscount }}%</span>
|
> -{{ highestEligibleDiscount }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||||
<div class="dropdown-content py-0">
|
<div class="dropdown-content py-0">
|
||||||
<div class="list has-overflow-ellipsis" style="width: 340px">
|
<div class="list has-overflow-ellipsis" style="width: 340px">
|
||||||
|
<template v-if="hasFixedProductPrice()">
|
||||||
|
<a class="list-item">
|
||||||
|
<div class="list-item-content">
|
||||||
|
<div class="list-item-title">Fast pris</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-item-controls list-item-controls-force-visible">
|
||||||
|
<div class="tags has-addons">
|
||||||
|
<span class="tag is-success is-light">{{ fixed_product_price }} Kr.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<!-- Best discount -->
|
<!-- Best discount -->
|
||||||
<a class="list-item">
|
<a class="list-item">
|
||||||
<div class="list-item-content">
|
<div class="list-item-content">
|
||||||
@@ -239,6 +294,7 @@ watch(() => props.customer_discounts, () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,4 +310,4 @@ watch(() => props.customer_discounts, () => {
|
|||||||
.text-can-not-select {
|
.text-can-not-select {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+19
@@ -62,6 +62,7 @@ watch(
|
|||||||
form.invoiceEmail,
|
form.invoiceEmail,
|
||||||
form.contactEmail,
|
form.contactEmail,
|
||||||
form.contactPhone,
|
form.contactPhone,
|
||||||
|
form.ean,
|
||||||
canCreateCustomer.value,
|
canCreateCustomer.value,
|
||||||
searchResult.value,
|
searchResult.value,
|
||||||
errorMessage.value,
|
errorMessage.value,
|
||||||
@@ -124,6 +125,24 @@ watch(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label"
|
||||||
|
><small>{{ $t("admin.pos.ean") }}</small></label
|
||||||
|
>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
v-model="form.ean"
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
maxlength="13"
|
||||||
|
:placeholder="$t('admin.pos.ean')"
|
||||||
|
:disabled="searchResult === null || submitting"
|
||||||
|
data-testid="pos-mobile-add-customer-ean"
|
||||||
|
@input="handleAutofillFieldInput('ean')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
><small>{{ $t("admin.pos.invoice_email") }}</small></label
|
><small>{{ $t("admin.pos.invoice_email") }}</small></label
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export const POS_ADD_CUSTOMER_AUTOFILL_FIELDS = [
|
|||||||
"invoiceEmail",
|
"invoiceEmail",
|
||||||
"contactEmail",
|
"contactEmail",
|
||||||
"contactPhone",
|
"contactPhone",
|
||||||
|
"ean",
|
||||||
];
|
];
|
||||||
|
|
||||||
export function normalizePosAddCustomerValue(value) {
|
export function normalizePosAddCustomerValue(value) {
|
||||||
@@ -20,6 +21,7 @@ export function createPosAddCustomerFields(overrides = {}) {
|
|||||||
invoiceEmail: "",
|
invoiceEmail: "",
|
||||||
contactEmail: "",
|
contactEmail: "",
|
||||||
contactPhone: "",
|
contactPhone: "",
|
||||||
|
ean: "",
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -30,6 +32,7 @@ export function createPosAddCustomerManualOverrides(overrides = {}) {
|
|||||||
invoiceEmail: false,
|
invoiceEmail: false,
|
||||||
contactEmail: false,
|
contactEmail: false,
|
||||||
contactPhone: false,
|
contactPhone: false,
|
||||||
|
ean: false,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -93,6 +96,7 @@ export function buildPosAddCustomerAutofillValues(searchResult) {
|
|||||||
invoiceEmail: normalizePosAddCustomerValue(searchResult?.email),
|
invoiceEmail: normalizePosAddCustomerValue(searchResult?.email),
|
||||||
contactEmail: normalizePosAddCustomerValue(searchResult?.email),
|
contactEmail: normalizePosAddCustomerValue(searchResult?.email),
|
||||||
contactPhone: normalizePosAddCustomerValue(searchResult?.phone),
|
contactPhone: normalizePosAddCustomerValue(searchResult?.phone),
|
||||||
|
ean: normalizePosAddCustomerValue(searchResult?.ean),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const props = defineProps({
|
|||||||
import {computed, ref, watch} from 'vue';
|
import {computed, ref, watch} from 'vue';
|
||||||
import { departments, getDepartments, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
import { departments, getDepartments, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import {orderBy, orderDirection, setOrder, loadList} from "@/components/pagination/paginatedList.vue";
|
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
|
||||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue";
|
import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue";
|
||||||
@@ -38,6 +38,8 @@ import {
|
|||||||
setCachedXlvaskUsageAmount
|
setCachedXlvaskUsageAmount
|
||||||
} from "@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js";
|
} from "@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js";
|
||||||
|
|
||||||
|
const { orderBy, orderDirection, setOrder, loadList } = usePaginatedListInstance();
|
||||||
|
|
||||||
const redirectDepartmentOrderPage = (orderId, departmentId) => {
|
const redirectDepartmentOrderPage = (orderId, departmentId) => {
|
||||||
// Send the user to the order page (In a new tab)
|
// Send the user to the order page (In a new tab)
|
||||||
// `/admin/${departmentId}/modules/pos/orders/${orderId}`
|
// `/admin/${departmentId}/modules/pos/orders/${orderId}`
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const SELFWASH_PERIOD_ALL_LIMIT = 10000;
|
||||||
+2
-1
@@ -38,7 +38,8 @@ if (router.currentRoute.value.params.departmentId) {
|
|||||||
<TableLabeledPagination :label="SessionUser.objects.department_notification_sms.meta.title">
|
<TableLabeledPagination :label="SessionUser.objects.department_notification_sms.meta.title">
|
||||||
<template #buttons="{ loadList }">
|
<template #buttons="{ loadList }">
|
||||||
<button
|
<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)})"
|
@click="SessionUser.objects.department_notification_sms.showCreateObjectForm(() => loadList(), {department: parseInt(router.currentRoute.value.params.departmentId)})"
|
||||||
>
|
>
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
|
|||||||
@@ -21,9 +21,32 @@ const props = defineProps({
|
|||||||
type: Number, default: 0
|
type: Number, default: 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
import {computed, ref, watch } from "vue";
|
import {computed, provide, ref, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import {
|
import {
|
||||||
|
PaginatedListKey,
|
||||||
|
usePaginatedList,
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
|
||||||
|
import PaginationDisplayTemplateDates
|
||||||
|
from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
|
||||||
|
import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { BSwitch } from "buefy";
|
||||||
|
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
|
||||||
|
import { SELFWASH_PERIOD_ALL_LIMIT } from "@/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js";
|
||||||
|
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const router = useRouter();
|
||||||
|
const paginatedList = usePaginatedList();
|
||||||
|
provide(PaginatedListKey, paginatedList);
|
||||||
|
|
||||||
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
list,
|
list,
|
||||||
loadList,
|
loadList,
|
||||||
@@ -38,23 +61,8 @@ import {
|
|||||||
setOrder,
|
setOrder,
|
||||||
hideSearchField,
|
hideSearchField,
|
||||||
setHideSearchField,
|
setHideSearchField,
|
||||||
} from "@/components/pagination/paginatedList.vue";
|
} = paginatedList;
|
||||||
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
|
||||||
|
|
||||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
|
||||||
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
|
||||||
import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
|
|
||||||
import PaginationDisplayTemplateDates
|
|
||||||
from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
|
|
||||||
import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { BSwitch } from "buefy";
|
|
||||||
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
|
|
||||||
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const router = useRouter();
|
|
||||||
const SELFWASH_PERIOD_ALL_LIMIT = 10000;
|
|
||||||
setEndpoint("/modules/xlvask/services/usage/orders", false);
|
setEndpoint("/modules/xlvask/services/usage/orders", false);
|
||||||
if (props.loadAllAtOnce) {
|
if (props.loadAllAtOnce) {
|
||||||
setMetaItemsPerPage(SELFWASH_PERIOD_ALL_LIMIT, false);
|
setMetaItemsPerPage(SELFWASH_PERIOD_ALL_LIMIT, false);
|
||||||
|
|||||||
@@ -10,11 +10,21 @@ const props = defineProps(["setCustomerFilter", "hideSearch", "autoLoad"]);
|
|||||||
|
|
||||||
const paginatedList = usePaginatedList();
|
const paginatedList = usePaginatedList();
|
||||||
provide(PaginatedListKey, paginatedList);
|
provide(PaginatedListKey, paginatedList);
|
||||||
const { list, loadList, setEndpoint, setFilter, setOrder, hideSearchField, setHideSearchField } = paginatedList;
|
const {
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
setAdditionalQueryParameters,
|
||||||
|
setEndpoint,
|
||||||
|
setFilter,
|
||||||
|
setOrder,
|
||||||
|
hideSearchField,
|
||||||
|
setHideSearchField,
|
||||||
|
} = paginatedList;
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
setEndpoint("/users", false);
|
setEndpoint("/users", false);
|
||||||
setFilter("customer_number", 0, false);
|
setFilter("customer_number", 0, false);
|
||||||
|
setAdditionalQueryParameters({ include_limited_backoffice_employees: "true" });
|
||||||
setOrder("created_at", "desc");
|
setOrder("created_at", "desc");
|
||||||
|
|
||||||
// Hide the search field, if the hideSearch prop is set
|
// Hide the search field, if the hideSearch prop is set
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const editUser = (user) => {
|
const editUser = (user) => {
|
||||||
showEditUserForm(user.id, user.customer_number, user.display_name, user.group_id);
|
showEditUserForm(user.id, user.customer_number, user.display_name, user.group_id, {
|
||||||
|
limitedBackofficeManaged: Boolean(user.limited_backoffice_managed),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import axios from "axios";
|
|||||||
import { API_URL } from "@/config.js";
|
import { API_URL } from "@/config.js";
|
||||||
import { getError, parseError, removeError } from "@/components/request/HandleGlobalError.vue";
|
import { getError, parseError, removeError } from "@/components/request/HandleGlobalError.vue";
|
||||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import { economicEanPayloadValue, normalizeEconomicEan } from "@/services/economicCustomerIdentifiers.js";
|
||||||
|
|
||||||
const customerFormData = reactive({
|
const customerFormData = reactive({
|
||||||
cvr: null,
|
cvr: null,
|
||||||
contactEmail: null,
|
contactEmail: null,
|
||||||
contactPhone: null
|
contactPhone: null,
|
||||||
|
ean: null
|
||||||
});
|
});
|
||||||
|
|
||||||
const deliverSuccess = ref(false);
|
const deliverSuccess = ref(false);
|
||||||
@@ -17,17 +19,24 @@ let deliverTimeoutId;
|
|||||||
const submitCustomer = async () => {
|
const submitCustomer = async () => {
|
||||||
removeError("authRegisterCvr");
|
removeError("authRegisterCvr");
|
||||||
try {
|
try {
|
||||||
await axios.post(API_URL + "/auth/register/cvr", {
|
const payload = {
|
||||||
cvr: customerFormData.cvr,
|
cvr: customerFormData.cvr,
|
||||||
invoiceEmail: customerFormData.contactEmail,
|
invoiceEmail: customerFormData.contactEmail,
|
||||||
contactEmail: customerFormData.contactEmail,
|
contactEmail: customerFormData.contactEmail,
|
||||||
contactPhone: customerFormData.contactPhone,
|
contactPhone: customerFormData.contactPhone,
|
||||||
companyPhone: customerFormData.contactPhone
|
companyPhone: customerFormData.contactPhone
|
||||||
});
|
};
|
||||||
|
const ean = economicEanPayloadValue(customerFormData.ean);
|
||||||
|
if (ean !== undefined) {
|
||||||
|
payload.ean = ean;
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.post(API_URL + "/auth/register/cvr", payload);
|
||||||
|
|
||||||
customerFormData.contactPhone = null;
|
customerFormData.contactPhone = null;
|
||||||
customerFormData.contactEmail = null;
|
customerFormData.contactEmail = null;
|
||||||
customerFormData.cvr = null;
|
customerFormData.cvr = null;
|
||||||
|
customerFormData.ean = null;
|
||||||
|
|
||||||
deliverSuccess.value = true;
|
deliverSuccess.value = true;
|
||||||
deliverTimeoutId = setTimeout(() => {
|
deliverTimeoutId = setTimeout(() => {
|
||||||
@@ -59,6 +68,20 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="customer_ean">{{ $t("customer_creation.customer.ean_label") }}</label>
|
||||||
|
<input
|
||||||
|
id="customer_ean"
|
||||||
|
:value="customerFormData.ean"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
maxlength="13"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="5790001234567"
|
||||||
|
@input="customerFormData.ean = normalizeEconomicEan($event.target.value)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="email">{{ $t("customer_creation.customer.email_label") }}</label>
|
<label for="email">{{ $t("customer_creation.customer.email_label") }}</label>
|
||||||
<input id="email" v-model="customerFormData.contactEmail" type="email" name="email" autocomplete="email">
|
<input id="email" v-model="customerFormData.contactEmail" type="email" name="email" autocomplete="email">
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ import {
|
|||||||
getProductsCategory,
|
getProductsCategory,
|
||||||
loadOrderItems,
|
loadOrderItems,
|
||||||
productCategoryAllowed,
|
productCategoryAllowed,
|
||||||
|
isProductRestricted,
|
||||||
setProductsCategory,
|
setProductsCategory,
|
||||||
getUserProductDiscount,
|
getUserProductPrice,
|
||||||
showFakeCreateOrderItem,
|
showFakeCreateOrderItem,
|
||||||
department_id,
|
department_id,
|
||||||
user_discounts,
|
user_discounts,
|
||||||
getUserGlobalDiscount,
|
getUserGlobalDiscount,
|
||||||
getUserDiscounts,
|
getUserDiscounts,
|
||||||
customer_id,
|
customer_id,
|
||||||
customer_attributes
|
customer_attributes,
|
||||||
|
loadCustomerAttributes
|
||||||
} from "@/components/shop/POSDepartmentProcess.vue";
|
} from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
import {getProductCategory, getProducts} from "@/components/shop/Products.vue";
|
import {getProductCategory, getProducts} from "@/components/shop/Products.vue";
|
||||||
import {createOrderItem} from "@/components/shop/OrdersItems.vue";
|
import {createOrderItem} from "@/components/shop/OrdersItems.vue";
|
||||||
@@ -151,6 +153,10 @@ const fetchProductsForCategory = async ({
|
|||||||
products.value = [];
|
products.value = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (customerNumber) {
|
||||||
|
await loadCustomerAttributes(customerNumber);
|
||||||
|
}
|
||||||
|
|
||||||
const response = normalizedCategory === null
|
const response = normalizedCategory === null
|
||||||
? await getProducts(departmentId, props.finalPrice, customerNumber)
|
? await getProducts(departmentId, props.finalPrice, customerNumber)
|
||||||
: await getProductCategory(normalizedCategory, departmentId, props.finalPrice, customerNumber);
|
: await getProductCategory(normalizedCategory, departmentId, props.finalPrice, customerNumber);
|
||||||
@@ -549,10 +555,9 @@ const addPreviousOrderToCurrent = async (previousOrder) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousOrderProduct = getPreviousOrderProduct(orderItem);
|
const previousOrderProduct = getPreviousOrderProduct(orderItem) || { id: productId, price: getPreviousOrderProductPrice(orderItem) };
|
||||||
const discount = getUserProductDiscount(previousOrderProduct || {});
|
|
||||||
const basePrice = getPreviousOrderProductPrice(orderItem);
|
const basePrice = getPreviousOrderProductPrice(orderItem);
|
||||||
const discountedPrice = (basePrice - (basePrice * (discount / 100))).toFixed(0);
|
const discountedPrice = String(getUserProductPrice({ ...previousOrderProduct, price: basePrice }));
|
||||||
|
|
||||||
showFakeCreateOrderItem(productId, quantity, discountedPrice);
|
showFakeCreateOrderItem(productId, quantity, discountedPrice);
|
||||||
await createOrderItem(orderId, productId, quantity);
|
await createOrderItem(orderId, productId, quantity);
|
||||||
@@ -586,14 +591,12 @@ const getRecommendedProductPrice = (productId) => {
|
|||||||
return '0';
|
return '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
const discount = getUserProductDiscount(product);
|
return String(getUserProductPrice(product));
|
||||||
const price = Number(product.price ?? 0);
|
|
||||||
return (price - (price * (discount / 100))).toFixed(0);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRecommendedProductCategoryAllowed = (productId) => {
|
const isRecommendedProductCategoryAllowed = (productId) => {
|
||||||
const product = getProductById(productId);
|
const product = getProductById(productId);
|
||||||
return !!product && productCategoryAllowed(product.category);
|
return !!product && !isProductRestricted(product);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addRecommendedProductToOrder = async (productId) => {
|
const addRecommendedProductToOrder = async (productId) => {
|
||||||
|
|||||||
@@ -405,6 +405,24 @@ watch(activeQuickAction, (nextMode, previousMode) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label"
|
||||||
|
><small>{{ $t("admin.pos.ean") }}</small></label
|
||||||
|
>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
v-model="addCustomerForm.ean"
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
maxlength="13"
|
||||||
|
:placeholder="$t('admin.pos.ean')"
|
||||||
|
:disabled="addCustomerSearchResult === null || isAddCustomerSubmitting"
|
||||||
|
data-testid="pos-desktop-add-customer-ean"
|
||||||
|
@input="handleAddCustomerFieldInput('ean')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
><small>{{ $t("admin.pos.invoice_email") }}</small></label
|
><small>{{ $t("admin.pos.invoice_email") }}</small></label
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import Swal from "sweetalert2";
|
|||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
export const showEditUserForm = async (id, customer_number, display_name, role) => {
|
export const showEditUserForm = async (id, customer_number, display_name, role, options = {}) => {
|
||||||
|
const lockedRoleFields = options.limitedBackofficeManaged ? 'disabled' : '';
|
||||||
|
|
||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
title: 'Edit User',
|
title: 'Edit User',
|
||||||
html: `
|
html: `
|
||||||
@@ -13,7 +15,7 @@ export const showEditUserForm = async (id, customer_number, display_name, role)
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Customer Number</label>
|
<label class="label">Customer Number</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" id="customer_number" value="${customer_number}" />
|
<input class="input" type="text" id="customer_number" value="${customer_number}" ${lockedRoleFields} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -25,7 +27,7 @@ export const showEditUserForm = async (id, customer_number, display_name, role)
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Role</label>
|
<label class="label">Role</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" id="role" value="${role}" />
|
<input class="input" type="text" id="role" value="${role}" ${lockedRoleFields} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -50,4 +52,4 @@ export const showEditUserForm = async (id, customer_number, display_name, role)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
const props = defineProps(['invoice_id'])
|
|
||||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
import { handleEconomicError } from "@/components/request/HandleEconomicError.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 () => {
|
const getOrderPDF = async () => {
|
||||||
await authenticatedRequest('/invoices/pdf?id=' + props.invoice_id,
|
if (!hasInvoice.value) {
|
||||||
'GET',{
|
return;
|
||||||
}) .then((response) => {
|
}
|
||||||
|
|
||||||
|
await authenticatedRequest("/invoices/pdf", "GET", {
|
||||||
|
id: props.invoice_id,
|
||||||
|
}).then((response) => {
|
||||||
// Open the PDF in a new tab
|
// Open the PDF in a new tab
|
||||||
window.open(response.data.data.url, '_blank');
|
window.open(response.data.data.url, "_blank");
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
handleEconomicError(error);
|
handleEconomicError(error);
|
||||||
});
|
});
|
||||||
@@ -19,9 +27,17 @@ const getOrderPDF = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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 ({
|
createComplaint: async ({
|
||||||
department_id,
|
department_id,
|
||||||
customer_number = null,
|
customer_number = null,
|
||||||
|
|||||||
@@ -32,6 +32,23 @@ const normalizeDateTimeLocalValue = (value) => {
|
|||||||
return `${datePart}T${timePart.slice(0, 5)}`;
|
return `${datePart}T${timePart.slice(0, 5)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sanitizeQueryParams = (params = {}) => {
|
||||||
|
if (!params || typeof params !== 'object') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(params).filter(([, value]) => value !== null && value !== undefined)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildQueryString = (params = {}) => {
|
||||||
|
const queryParams = sanitizeQueryParams(params);
|
||||||
|
const queryString = new URLSearchParams(queryParams).toString();
|
||||||
|
|
||||||
|
return queryString ? `?${queryString}` : '';
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Global Objects object, which contains global functions for objects.
|
* The Global Objects object, which contains global functions for objects.
|
||||||
*/
|
*/
|
||||||
@@ -431,20 +448,15 @@ export const ObjectsGlobal = {
|
|||||||
* @returns {Promise} The promise
|
* @returns {Promise} The promise
|
||||||
*/
|
*/
|
||||||
object: async (endpoint, id, optionsObject = {}) => {
|
object: async (endpoint, id, optionsObject = {}) => {
|
||||||
const cacheKey = JSON.stringify({endpoint, id, optionsObject});
|
const requestParams = typeof id === 'object' && id !== null
|
||||||
|
? sanitizeQueryParams(id)
|
||||||
|
: {id};
|
||||||
|
const cacheKey = JSON.stringify({endpoint, id: requestParams, optionsObject});
|
||||||
if (ObjectsGlobal.cache.has(cacheKey)) {
|
if (ObjectsGlobal.cache.has(cacheKey)) {
|
||||||
return ObjectsGlobal.cache.get(cacheKey);
|
return ObjectsGlobal.cache.get(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
let requestEndpoint = endpoint;
|
const requestEndpoint = `${endpoint}${buildQueryString(requestParams)}`;
|
||||||
if (typeof id === 'object') {
|
|
||||||
// If the id is an object, we need to convert it to a query string
|
|
||||||
const params = new URLSearchParams(id).toString();
|
|
||||||
requestEndpoint = `${endpoint}?${params}`;
|
|
||||||
} else {
|
|
||||||
// If the id is a number, we can just append it to the endpoint
|
|
||||||
requestEndpoint = `${endpoint}?id=${id}`;
|
|
||||||
}
|
|
||||||
let options = ObjectsGlobal.requestOptions.applyOptions(optionsObject);
|
let options = ObjectsGlobal.requestOptions.applyOptions(optionsObject);
|
||||||
let promise;
|
let promise;
|
||||||
if (options.authenticated) {
|
if (options.authenticated) {
|
||||||
@@ -499,23 +511,24 @@ export const ObjectsGlobal = {
|
|||||||
* @returns {Promise} The promise
|
* @returns {Promise} The promise
|
||||||
*/
|
*/
|
||||||
objects: async (endpoint, data = {}) => {
|
objects: async (endpoint, data = {}) => {
|
||||||
const cacheKey = JSON.stringify({endpoint, data});
|
const requestData = sanitizeQueryParams(data);
|
||||||
if (ObjectsGlobal.cache.has(cacheKey)) {
|
if (requestData.filters && typeof requestData.filters === 'object') {
|
||||||
return ObjectsGlobal.cache.get(cacheKey);
|
requestData.filters = Object.entries(requestData.filters).flatMap(([key, value]) => {
|
||||||
}
|
|
||||||
|
|
||||||
if (data.filters && typeof data.filters === 'object') {
|
|
||||||
data.filters = Object.entries(data.filters).flatMap(([key, value]) => {
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value.map(v => `${key}:${v}`);
|
return value.map(v => `${key}:${v}`);
|
||||||
}
|
}
|
||||||
return `${key}:${value}`;
|
return `${key}:${value}`;
|
||||||
}).join(',');
|
}).join(',');
|
||||||
}
|
}
|
||||||
|
const cacheKey = JSON.stringify({endpoint, data: requestData});
|
||||||
|
if (ObjectsGlobal.cache.has(cacheKey)) {
|
||||||
|
return ObjectsGlobal.cache.get(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
const promise = authenticatedRequest(
|
const promise = authenticatedRequest(
|
||||||
endpoint,
|
endpoint,
|
||||||
"GET",
|
"GET",
|
||||||
data
|
requestData
|
||||||
).then((response) => {
|
).then((response) => {
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ import i18n from '@/i18n';
|
|||||||
|
|
||||||
const t = (key) => i18n.global.t(key);
|
const t = (key) => i18n.global.t(key);
|
||||||
|
|
||||||
|
const normalizeProductQueryOptions = (options = {}) => {
|
||||||
|
const queryOptions = { ...options };
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(queryOptions, "final_price")) {
|
||||||
|
queryOptions.final_price = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(queryOptions).filter(([, value]) => value !== null && value !== undefined)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Local products cache
|
* Local products cache
|
||||||
* @type {ref<null>}
|
* @type {ref<null>}
|
||||||
@@ -269,19 +280,19 @@ export const Products = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
get: {
|
get: {
|
||||||
all: async (options = {department_id: null, customer_id: null, category_id: null, final_price: false}) => {
|
all: async (options = {}) => {
|
||||||
return ObjectsGlobal.get.objects(Products.meta.endpoint, {...options});
|
return ObjectsGlobal.get.objects(Products.meta.endpoint, normalizeProductQueryOptions(options));
|
||||||
},
|
},
|
||||||
single: async (id, options = {department_id: null, customer_id: null, category_id: null, final_price: false}) => {
|
single: async (id, options = {}) => {
|
||||||
return ObjectsGlobal.get.object(Products.meta.endpoint, {id: id, ...options});
|
return ObjectsGlobal.get.object(Products.meta.endpoint, {id: id, ...normalizeProductQueryOptions(options)});
|
||||||
},
|
},
|
||||||
category: async (category_id, department_id, customer_id = null, final_price = false) => {
|
category: async (category_id, department_id, customer_id = null, final_price = false) => {
|
||||||
return ObjectsGlobal.get.object(Products.meta.endpoint, {
|
return ObjectsGlobal.get.object(Products.meta.endpoint, normalizeProductQueryOptions({
|
||||||
category: category_id,
|
category: category_id,
|
||||||
department_id: department_id,
|
department_id: department_id,
|
||||||
...(customer_id !== null ? {customer_id: customer_id} : {}),
|
...(customer_id !== null ? {customer_id: customer_id} : {}),
|
||||||
...{final_price: final_price}
|
...{final_price: final_price}
|
||||||
});
|
}));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
delete: async (id) => {
|
delete: async (id) => {
|
||||||
@@ -324,4 +335,4 @@ export const Products = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import { getAttributes } from "@/components/shop/CustomerAttributes.vue";
|
|||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||||
import { doesOrderContainWashCertificateProduct } from "@/components/displays/department/pos/utils/washCertificate.js";
|
import { doesOrderContainWashCertificateProduct } from "@/components/displays/department/pos/utils/washCertificate.js";
|
||||||
|
import {
|
||||||
|
isProductCategoryRestrictedForCustomer,
|
||||||
|
isProductRestrictedForCustomer,
|
||||||
|
} from "@/features/customer/customerProductRules.js";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,6 +157,8 @@ export const nextStep = async (options = { isMobile: false, orderCreation: true
|
|||||||
clearErrors();
|
clearErrors();
|
||||||
// If the current step is 1, create the order
|
// If the current step is 1, create the order
|
||||||
if (step.value === 1) {
|
if (step.value === 1) {
|
||||||
|
await loadCustomerAttributes(customer_id.value);
|
||||||
|
|
||||||
if (normalizedOptions.orderCreation === false) {
|
if (normalizedOptions.orderCreation === false) {
|
||||||
step.value++;
|
step.value++;
|
||||||
return;
|
return;
|
||||||
@@ -1613,24 +1619,7 @@ export const customerUsesPONumbers = () => {
|
|||||||
|
|
||||||
/** Check if the product category is allowed */
|
/** Check if the product category is allowed */
|
||||||
export const productCategoryAllowed = (category) => {
|
export const productCategoryAllowed = (category) => {
|
||||||
// Check if the product category is the addons category
|
return !isProductCategoryRestrictedForCustomer(category, customer_attributes.value);
|
||||||
if (category === "addons") {
|
|
||||||
// Check if the customer has the attribute restrictAdditionalServices
|
|
||||||
return !hasAttribute("restrictAdditionalServices");
|
|
||||||
}
|
|
||||||
if (category === "spot_free") {
|
|
||||||
// Check if the customer has the attribute restrictSpotFree
|
|
||||||
return !hasAttribute("restrictSpotFree");
|
|
||||||
}
|
|
||||||
if (category === "interior_cleaning") {
|
|
||||||
// Check if the customer has the attribute restrictInteriorCleaning
|
|
||||||
return !hasAttribute("restrictInteriorCleaning");
|
|
||||||
}
|
|
||||||
if (category === "tank_cleaning") {
|
|
||||||
// Check if the customer has the attribute restrictTankCleaning
|
|
||||||
return !hasAttribute("restrictTankCleaning");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Check if the customer should be billed per order, or per billing period */
|
/** Check if the customer should be billed per order, or per billing period */
|
||||||
@@ -1676,43 +1665,12 @@ export const hasOnlyTankCleaning = () => {
|
|||||||
|
|
||||||
/** Check if a product is restricted based on customer attributes */
|
/** Check if a product is restricted based on customer attributes */
|
||||||
export const isProductRestricted = (product) => {
|
export const isProductRestricted = (product) => {
|
||||||
if (!product) return false;
|
return isProductRestrictedForCustomer(product, customer_attributes.value);
|
||||||
const productName = (product.name || "").toLowerCase();
|
|
||||||
|
|
||||||
// Check if the product has "Spot Free" in the name
|
|
||||||
if (!canBuySpotFree() && productName.includes("spot free")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Check if the product has "Indvendig vask" in the name
|
|
||||||
if (!canBuyInteriorCleaning() && productName.includes("indvendig vask")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Check if the product category is 5 (Tank Cleaning)
|
|
||||||
if (product.category === 5) {
|
|
||||||
if (!canBuyTankCleaning()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!hasOnlyTankCleaning()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Check if an addon is restricted based on customer attributes */
|
/** Check if an addon is restricted based on customer attributes */
|
||||||
export const isAddonRestricted = (addon) => {
|
export const isAddonRestricted = (addon) => {
|
||||||
if (!addon) return false;
|
return isProductRestrictedForCustomer(addon, customer_attributes.value);
|
||||||
const addonName = (addon.name || "").toLowerCase();
|
|
||||||
|
|
||||||
// Check if the addon has "Spot Free" in the name
|
|
||||||
if (!canBuySpotFree() && addonName.includes("spot free")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Check if the addon has "Indvendig vask" in the name
|
|
||||||
if (!canBuyInteriorCleaning() && addonName.includes("indvendig vask")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function loadOrderDetails(onAfterSuccess = null, options = {}) {
|
export function loadOrderDetails(onAfterSuccess = null, options = {}) {
|
||||||
@@ -1844,13 +1802,30 @@ export const getUserDiscounts = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Get user product discount */
|
const ensureUserDiscountsLoaded = () => {
|
||||||
export const getUserProductDiscount = (product, allowCategory = null, onlyCategory = null) => {
|
|
||||||
// Check if the user discounts are loaded
|
|
||||||
if (!has_user_discounts_loaded.value) {
|
if (!has_user_discounts_loaded.value) {
|
||||||
has_user_discounts_loaded.value = true;
|
has_user_discounts_loaded.value = true;
|
||||||
getUserDiscounts().then(() => {});
|
getUserDiscounts().then(() => {});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDirectCustomerProductDiscount = (discount, productId) => {
|
||||||
|
return discount?.product_or_category_id == productId && Number(discount?.is_category) === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseFixedPriceValue = (value) => {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number.parseInt(String(value), 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Get user product discount */
|
||||||
|
export const getUserProductDiscount = (product, allowCategory = null, onlyCategory = null) => {
|
||||||
|
// Check if the user discounts are loaded
|
||||||
|
ensureUserDiscountsLoaded();
|
||||||
|
|
||||||
// Get the product's id
|
// Get the product's id
|
||||||
const productId = product.id;
|
const productId = product.id;
|
||||||
@@ -1948,6 +1923,36 @@ export const getUserProductDiscount = (product, allowCategory = null, onlyCatego
|
|||||||
return appliedDiscount;
|
return appliedDiscount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUserProductFixedPrice = (product) => {
|
||||||
|
ensureUserDiscountsLoaded();
|
||||||
|
|
||||||
|
const productId = product?.id;
|
||||||
|
if (productId === null || productId === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixedPriceDiscount = (Array.isArray(user_discounts.value) ? user_discounts.value : []).find((discount) => (
|
||||||
|
isDirectCustomerProductDiscount(discount, productId) && parseFixedPriceValue(discount.fixed_price) !== null
|
||||||
|
));
|
||||||
|
|
||||||
|
return fixedPriceDiscount ? parseFixedPriceValue(fixedPriceDiscount.fixed_price) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserProductPrice = (product) => {
|
||||||
|
const fixedPrice = getUserProductFixedPrice(product);
|
||||||
|
if (fixedPrice !== null) {
|
||||||
|
return fixedPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = Number(product?.price ?? 0);
|
||||||
|
const discount = Number(getUserProductDiscount(product) || 0);
|
||||||
|
if (!Number.isFinite(price)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number((price - (price * (discount / 100))).toFixed(0));
|
||||||
|
};
|
||||||
|
|
||||||
export const showFakeCreateOrderItem = (product, quantity, price) => {
|
export const showFakeCreateOrderItem = (product, quantity, price) => {
|
||||||
order_items.value.push({
|
order_items.value.push({
|
||||||
id: Math.floor(Math.random() * 1000),
|
id: Math.floor(Math.random() * 1000),
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ const descriptionKeyOverrides: Partial<Record<EntityType, string[]>> = {
|
|||||||
xlvask_customers: ['email', 'city', 'address', 'vatnumber'],
|
xlvask_customers: ['email', 'city', 'address', 'vatnumber'],
|
||||||
motorapi_lookups: ['endpoint', 'license_plate', 'result'],
|
motorapi_lookups: ['endpoint', 'license_plate', 'result'],
|
||||||
fxratesapi_conversion_rates: ['base', 'target', 'endpoint', 'result'],
|
fxratesapi_conversion_rates: ['base', 'target', 'endpoint', 'result'],
|
||||||
customer_discounts: ['customer_number', 'percentage'],
|
customer_discounts: ['customer_number', 'fixed_price', 'percentage'],
|
||||||
module_config: ['description', 'value'],
|
module_config: ['description', 'value'],
|
||||||
department_goals: ['criteria.type', 'criteria.progress_alert_frequency', 'criteria.progress_alert_destination']
|
department_goals: ['criteria.type', 'criteria.progress_alert_frequency', 'criteria.progress_alert_destination']
|
||||||
};
|
};
|
||||||
@@ -251,6 +251,7 @@ const keyFieldOverrides: Partial<Record<EntityType, FieldSelector[]>> = {
|
|||||||
),
|
),
|
||||||
customer_discounts: fieldList(
|
customer_discounts: fieldList(
|
||||||
field('Discount', 'discount', 'percentage'),
|
field('Discount', 'discount', 'percentage'),
|
||||||
|
field('Fixed price', 'fixed_price'),
|
||||||
field('Target', 'product_or_category_id', 'object_id'),
|
field('Target', 'product_or_category_id', 'object_id'),
|
||||||
field('Category', 'is_category'),
|
field('Category', 'is_category'),
|
||||||
field('Customer #', 'customer_number'),
|
field('Customer #', 'customer_number'),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { computed, onBeforeUnmount, reactive, ref, watch } from "vue";
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { searchAndSelectCustomer } from "@/components/shop/POSDepartmentProcess.vue";
|
import { searchAndSelectCustomer } from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||||
|
import { economicEanPayloadValue } from "@/services/economicCustomerIdentifiers.js";
|
||||||
import {
|
import {
|
||||||
applyPosAddCustomerSearchResult,
|
applyPosAddCustomerSearchResult,
|
||||||
createPosAddCustomerFields,
|
createPosAddCustomerFields,
|
||||||
@@ -152,14 +153,20 @@ export function usePosAddCustomerForm(options = {}) {
|
|||||||
errorMessage.value = null;
|
errorMessage.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await SessionUser.request("/auth/register/cvr", "POST", {
|
const payload = {
|
||||||
cvr: form.cvr,
|
cvr: form.cvr,
|
||||||
companyPhone: Number.parseInt(form.companyPhone, 10),
|
companyPhone: Number.parseInt(form.companyPhone, 10),
|
||||||
invoiceEmail: form.invoiceEmail,
|
invoiceEmail: form.invoiceEmail,
|
||||||
contactEmail: form.contactEmail,
|
contactEmail: form.contactEmail,
|
||||||
contactPhone: Number.parseInt(form.contactPhone, 10),
|
contactPhone: Number.parseInt(form.contactPhone, 10),
|
||||||
searchResult: searchResult.value,
|
searchResult: searchResult.value,
|
||||||
});
|
};
|
||||||
|
const ean = economicEanPayloadValue(form.ean);
|
||||||
|
if (ean !== undefined) {
|
||||||
|
payload.ean = ean;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SessionUser.request("/auth/register/cvr", "POST", payload);
|
||||||
|
|
||||||
const selectedCustomer = await searchAndSelectCustomer(form.companyPhone);
|
const selectedCustomer = await searchAndSelectCustomer(form.companyPhone);
|
||||||
const createdCustomerNumber = Number.parseInt(form.companyPhone, 10);
|
const createdCustomerNumber = Number.parseInt(form.companyPhone, 10);
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
const TANK_CLEANING_TERMS = ["tank cleaning", "tankcleaning", "tankrens"];
|
||||||
|
|
||||||
|
const normalizeAttributeName = (attribute) => {
|
||||||
|
if (typeof attribute === "string") {
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(attribute?.attribute ?? "");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasCustomerAttribute = (attributes, attributeName) => {
|
||||||
|
if (!Array.isArray(attributes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes.some((attribute) => normalizeAttributeName(attribute) === attributeName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizedText = (value) => String(value ?? "").trim().toLowerCase();
|
||||||
|
|
||||||
|
const textContainsAny = (value, terms) => {
|
||||||
|
const text = normalizedText(value);
|
||||||
|
return terms.some((term) => term !== "" && text.includes(term));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isTankCleaningCategory = (category) => {
|
||||||
|
if (Number(category) === 5) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return textContainsAny(category, ["tank_cleaning", ...TANK_CLEANING_TERMS]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isTankCleaningProduct = (product) => {
|
||||||
|
if (!product) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTankCleaningCategory(product.category ?? product.product_category)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return textContainsAny(
|
||||||
|
[
|
||||||
|
product.name,
|
||||||
|
product.product_name,
|
||||||
|
product.category_name,
|
||||||
|
product.categoryName,
|
||||||
|
].join(" "),
|
||||||
|
TANK_CLEANING_TERMS
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isProductCategoryRestrictedForCustomer = (category, attributes = []) => {
|
||||||
|
if (category === "addons" && hasCustomerAttribute(attributes, "restrictAdditionalServices")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (category === "spot_free" && hasCustomerAttribute(attributes, "restrictSpotFree")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (category === "interior_cleaning" && hasCustomerAttribute(attributes, "restrictInteriorCleaning")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isTankCleaningCategory(category) && hasCustomerAttribute(attributes, "restrictTankCleaning")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasCustomerAttribute(attributes, "onlyTankCleaning") && !isTankCleaningCategory(category)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isProductRestrictedForCustomer = (product, attributes = []) => {
|
||||||
|
if (!product) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const productName = normalizedText(product.name ?? product.product_name);
|
||||||
|
|
||||||
|
if (hasCustomerAttribute(attributes, "restrictSpotFree") && productName.includes("spot free")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasCustomerAttribute(attributes, "restrictInteriorCleaning") && productName.includes("indvendig vask")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isTankCleaningProduct(product) && hasCustomerAttribute(attributes, "restrictTankCleaning")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasCustomerAttribute(attributes, "onlyTankCleaning") && !isTankCleaningProduct(product)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
@@ -1220,7 +1220,13 @@
|
|||||||
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
|
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
|
||||||
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
|
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
|
||||||
"edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}",
|
"edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}",
|
||||||
"deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}"
|
"deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}",
|
||||||
|
"login_link": "@:{'templates.generated.compat.limited_backoffice.employees.login_link'}",
|
||||||
|
"login_link_title": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_title'}",
|
||||||
|
"login_link_qr_alt": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_qr_alt'}",
|
||||||
|
"login_link_url": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_url'}",
|
||||||
|
"copy_login_link": "@:{'templates.generated.compat.limited_backoffice.employees.copy_login_link'}",
|
||||||
|
"login_link_copied": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_copied'}"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -1250,7 +1256,8 @@
|
|||||||
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
|
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
|
||||||
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
|
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
|
||||||
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
|
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
|
||||||
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}"
|
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}",
|
||||||
|
"generate_employee_login_link": "@:{'templates.generated.compat.limited_backoffice.errors.generate_employee_login_link'}"
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
|
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
|
||||||
@@ -1510,6 +1517,14 @@
|
|||||||
"subtitle_suffix": "@:{'words.generated.salg'} @:{'words.generated.af'} produktet",
|
"subtitle_suffix": "@:{'words.generated.salg'} @:{'words.generated.af'} produktet",
|
||||||
"title": "Produktsalg"
|
"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": {
|
"reports": {
|
||||||
"bookings": {
|
"bookings": {
|
||||||
"completed_count": "@.capitalize:{'words.generated.antal'} @:{'words.generated.fuldførte'}",
|
"completed_count": "@.capitalize:{'words.generated.antal'} @:{'words.generated.fuldførte'}",
|
||||||
@@ -1704,6 +1719,7 @@
|
|||||||
"company_phone": "Virksomhedstelefon",
|
"company_phone": "Virksomhedstelefon",
|
||||||
"contact_email": "@.capitalize:{'words.generated.kontakt'}-@:{'words.generated.e'}-@:{'words.generated.mail'}",
|
"contact_email": "@.capitalize:{'words.generated.kontakt'}-@:{'words.generated.e'}-@:{'words.generated.mail'}",
|
||||||
"contact_phone": "Kontakttelefon",
|
"contact_phone": "Kontakttelefon",
|
||||||
|
"ean": "EAN",
|
||||||
"created_by": "@.capitalize:{'words.generated.oprettet'} @:{'words.generated.af'}",
|
"created_by": "@.capitalize:{'words.generated.oprettet'} @:{'words.generated.af'}",
|
||||||
"customer_conflict": {
|
"customer_conflict": {
|
||||||
"help_text": "@.capitalize:{'words.replication.article.host_mention'} @:{'words.generated.indtastede'} @:{'words.generated.nummerplade'} @:{'words.generated.tilhører'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.anden'} @:{'words.generated.gemt'} @:{'words.generated.kunde'}. @.capitalize:{'words.generated.vælg'} @:{'words.generated.om'} @:{'words.replication.article.host_mention'} @:{'words.generated.valgte'} @:{'words.generated.kunde'} @:{'words.generated.skal'} @:{'words.generated.beholdes'}, @:{'words.generated.eller'} @:{'words.generated.om'} @:{'words.generated.der'} @:{'words.generated.skal'} @:{'words.generated.skiftes'} @:{'words.generated.til'} @:{'words.generated.køretøjets'} @:{'words.generated.kunde'}.",
|
"help_text": "@.capitalize:{'words.replication.article.host_mention'} @:{'words.generated.indtastede'} @:{'words.generated.nummerplade'} @:{'words.generated.tilhører'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.anden'} @:{'words.generated.gemt'} @:{'words.generated.kunde'}. @.capitalize:{'words.generated.vælg'} @:{'words.generated.om'} @:{'words.replication.article.host_mention'} @:{'words.generated.valgte'} @:{'words.generated.kunde'} @:{'words.generated.skal'} @:{'words.generated.beholdes'}, @:{'words.generated.eller'} @:{'words.generated.om'} @:{'words.generated.der'} @:{'words.generated.skal'} @:{'words.generated.skiftes'} @:{'words.generated.til'} @:{'words.generated.køretøjets'} @:{'words.generated.kunde'}.",
|
||||||
@@ -3252,6 +3268,7 @@
|
|||||||
"benefit_portal": "@:{'words.generated.kundeportal'}",
|
"benefit_portal": "@:{'words.generated.kundeportal'}",
|
||||||
"benefit_portal_desc": "@:{'words.generated.benyt'} kundeportalen @:{'words.generated.til'} @:{'words.generated.at'} @:{'words.generated.tilføje'} @:{'words.generated.køretøjer'}, @:{'words.generated.booke'} @:{'words.generated.vaske'}, downloade @:{'words.generated.fakturaer'}, @:{'words.generated.vaskecertifikater'} @:{'words.generated.og'} @:{'words.generated.information'} @:{'words.generated.for'} @:{'words.generated.historiske'} @:{'words.generated.vaske'}",
|
"benefit_portal_desc": "@:{'words.generated.benyt'} kundeportalen @:{'words.generated.til'} @:{'words.generated.at'} @:{'words.generated.tilføje'} @:{'words.generated.køretøjer'}, @:{'words.generated.booke'} @:{'words.generated.vaske'}, downloade @:{'words.generated.fakturaer'}, @:{'words.generated.vaskecertifikater'} @:{'words.generated.og'} @:{'words.generated.information'} @:{'words.generated.for'} @:{'words.generated.historiske'} @:{'words.generated.vaske'}",
|
||||||
"benefit_wash_desc": "@:{'words.generated.benyt'} @:{'words.generated.vores'} @:{'words.generated.nationale'} @:{'words.generated.netværk'} @:{'words.generated.af'} @:{'words.generated.vaskehaller'} @:{'words.generated.til'} @:{'words.generated.konkurrencedygtige'} @:{'words.generated.priser'}",
|
"benefit_wash_desc": "@:{'words.generated.benyt'} @:{'words.generated.vores'} @:{'words.generated.nationale'} @:{'words.generated.netværk'} @:{'words.generated.af'} @:{'words.generated.vaskehaller'} @:{'words.generated.til'} @:{'words.generated.konkurrencedygtige'} @:{'words.generated.priser'}",
|
||||||
|
"ean_label": "@:{'templates.generated.compat.admin.pos.ean'}",
|
||||||
"email_label": "@:{'words.generated.firma'} @:{'words.generated.email'}",
|
"email_label": "@:{'words.generated.firma'} @:{'words.generated.email'}",
|
||||||
"intro": "@.capitalize:{'words.generated.som'} @:{'words.generated.kunde'} @:{'words.generated.hos'} @:{'words.generated.truck'} @:{'words.generated.wash'} @:{'words.generated.far'} @:{'words.generated.du'} @:{'words.generated.adgang'} @:{'words.generated.til'}:",
|
"intro": "@.capitalize:{'words.generated.som'} @:{'words.generated.kunde'} @:{'words.generated.hos'} @:{'words.generated.truck'} @:{'words.generated.wash'} @:{'words.generated.far'} @:{'words.generated.du'} @:{'words.generated.adgang'} @:{'words.generated.til'}:",
|
||||||
"phone_label": "@:{'words.generated.firma'} @:{'words.generated.telefon'}",
|
"phone_label": "@:{'words.generated.firma'} @:{'words.generated.telefon'}",
|
||||||
@@ -5755,6 +5772,31 @@
|
|||||||
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
|
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
|
||||||
},
|
},
|
||||||
"other": "@:{'templates.generated.compat.global.other'}",
|
"other": "@:{'templates.generated.compat.global.other'}",
|
||||||
|
"overview": {
|
||||||
|
"active_vehicles": "Aktive køretøjer",
|
||||||
|
"attributes": "Attributter",
|
||||||
|
"connected": "Forbundet",
|
||||||
|
"customer_management": "Kundestyring",
|
||||||
|
"customer_number_required": "Et kundenummer er påkrævet før kundeindstillinger kan administreres.",
|
||||||
|
"customer_rules": "Kunderegler",
|
||||||
|
"effective_access": "Effektiv adgang",
|
||||||
|
"loaded": "Indlæst",
|
||||||
|
"load_failed": "Brugerdata kunne ikke indlæses",
|
||||||
|
"loading_user": "Indlæser bruger",
|
||||||
|
"management_hub": "Administrationshub",
|
||||||
|
"missing": "Mangler",
|
||||||
|
"more_permissions": "+{count} flere tilladelser",
|
||||||
|
"no_subscription_transactions": "Der er ikke registreret vaskeabonnementstransaktioner for kunden.",
|
||||||
|
"not_invoiced": "Ikke faktureret",
|
||||||
|
"open_draft": "Åben kladde",
|
||||||
|
"open_vehicles": "Åbn køretøjer",
|
||||||
|
"price_overrides": "Prisoverstyringer",
|
||||||
|
"pricing": "Priser",
|
||||||
|
"subscription_invoicing": "Abonnementsfakturering",
|
||||||
|
"subtitle": "Overblik over kundedata, adgang, priser, køretøjer og abonnementsfakturering.",
|
||||||
|
"vehicles_load_failed": "Køretøjer kunne ikke indlæses.",
|
||||||
|
"workspace_shortcuts": "Arbejdsgange"
|
||||||
|
},
|
||||||
"permissions": "@.capitalize:{'words.generated.tilladelser'}",
|
"permissions": "@.capitalize:{'words.generated.tilladelser'}",
|
||||||
"user_data": "@.capitalize:{'words.generated.brugerdata'}",
|
"user_data": "@.capitalize:{'words.generated.brugerdata'}",
|
||||||
"user_id": "@.capitalize:{'words.generated.bruger'} @.upper:{'words.generated.id'}",
|
"user_id": "@.capitalize:{'words.generated.bruger'} @.upper:{'words.generated.id'}",
|
||||||
@@ -6083,7 +6125,13 @@
|
|||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
"inactive": "Inaktiv",
|
"inactive": "Inaktiv",
|
||||||
"edit": "@:{'words.generated.rediger'}",
|
"edit": "@:{'words.generated.rediger'}",
|
||||||
"deactivate": "Deaktiver"
|
"deactivate": "Deaktiver",
|
||||||
|
"login_link": "QR-login",
|
||||||
|
"login_link_title": "Medarbejderloginlink",
|
||||||
|
"login_link_qr_alt": "QR-kode-loginlink",
|
||||||
|
"login_link_url": "Loginlink",
|
||||||
|
"copy_login_link": "Kopiér link",
|
||||||
|
"login_link_copied": "Linket er kopieret."
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -6113,7 +6161,8 @@
|
|||||||
"save_prices": "@.capitalize:{'words.generated.afdelingspriser'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.gemmes'}.",
|
"save_prices": "@.capitalize:{'words.generated.afdelingspriser'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.gemmes'}.",
|
||||||
"load_employees": "@.capitalize:{'words.generated.medarbejdere'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.indlæses'}.",
|
"load_employees": "@.capitalize:{'words.generated.medarbejdere'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.indlæses'}.",
|
||||||
"save_employee": "@.capitalize:{'words.generated.medarbejderen'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.gemmes'}.",
|
"save_employee": "@.capitalize:{'words.generated.medarbejderen'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.gemmes'}.",
|
||||||
"deactivate_employee": "@.capitalize:{'words.generated.medarbejderen'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} deaktiveres."
|
"deactivate_employee": "@.capitalize:{'words.generated.medarbejderen'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} deaktiveres.",
|
||||||
|
"generate_employee_login_link": "Medarbejderens loginlink kunne ikke oprettes."
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "Rolletilladelser",
|
"title": "Rolletilladelser",
|
||||||
|
|||||||
@@ -1331,7 +1331,13 @@
|
|||||||
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
|
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
|
||||||
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
|
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
|
||||||
"edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}",
|
"edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}",
|
||||||
"deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}"
|
"deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}",
|
||||||
|
"login_link": "@:{'templates.generated.compat.limited_backoffice.employees.login_link'}",
|
||||||
|
"login_link_title": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_title'}",
|
||||||
|
"login_link_qr_alt": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_qr_alt'}",
|
||||||
|
"login_link_url": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_url'}",
|
||||||
|
"copy_login_link": "@:{'templates.generated.compat.limited_backoffice.employees.copy_login_link'}",
|
||||||
|
"login_link_copied": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_copied'}"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -1361,7 +1367,8 @@
|
|||||||
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
|
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
|
||||||
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
|
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
|
||||||
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
|
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
|
||||||
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}"
|
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}",
|
||||||
|
"generate_employee_login_link": "@:{'templates.generated.compat.limited_backoffice.errors.generate_employee_login_link'}"
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
|
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
|
||||||
@@ -1621,6 +1628,14 @@
|
|||||||
"subtitle_suffix": "Produktverk?@:{'words.generated.ufe'}",
|
"subtitle_suffix": "Produktverk?@:{'words.generated.ufe'}",
|
||||||
"title": "@:{'templates.generated.compat.admin.daily_report.product_sales.subtitle_suffix'}"
|
"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": {
|
"reports": {
|
||||||
"bookings": {
|
"bookings": {
|
||||||
"completed_count": "@:{'words.generated.abgeschlossene'} @:{'words.generated.anzahl'}",
|
"completed_count": "@:{'words.generated.abgeschlossene'} @:{'words.generated.anzahl'}",
|
||||||
@@ -1815,6 +1830,7 @@
|
|||||||
"company_phone": "Firmentelefon",
|
"company_phone": "Firmentelefon",
|
||||||
"contact_email": "@:{'words.generated.kontakt'}-@.upper:{'words.generated.e'}-@:{'words.generated.mail'}",
|
"contact_email": "@:{'words.generated.kontakt'}-@.upper:{'words.generated.e'}-@:{'words.generated.mail'}",
|
||||||
"contact_phone": "Kontakttelefonnummer",
|
"contact_phone": "Kontakttelefonnummer",
|
||||||
|
"ean": "EAN",
|
||||||
"created_by": "@.capitalize:{'words.generated.erstellt'} @:{'words.generated.von'}",
|
"created_by": "@.capitalize:{'words.generated.erstellt'} @:{'words.generated.von'}",
|
||||||
"customer_conflict": {
|
"customer_conflict": {
|
||||||
"help_text": "@.capitalize:{'words.generated.dieses'} @:{'words.generated.kennzeichen'} @:{'words.generated.gehoert'} @:{'words.generated.zu'} @:{'words.generated.einem'} @:{'words.generated.anderen'} @:{'words.generated.gespeicherten'} @:{'words.generated.kunden'}. @:{'words.generated.waehlen'} @.capitalize:{'words.generated.sie'}, @:{'words.generated.ob'} @:{'words.generated.der'} @:{'words.generated.ausgewaehlte'} @.capitalize:{'words.generated.kunde'} @:{'words.generated.beibehalten'} @:{'words.generated.oder'} @:{'words.generated.zum'} @:{'words.generated.fahrzeugkunden'} @:{'words.generated.gewechselt'} @:{'words.generated.werden'} @:{'words.generated.soll'}.",
|
"help_text": "@.capitalize:{'words.generated.dieses'} @:{'words.generated.kennzeichen'} @:{'words.generated.gehoert'} @:{'words.generated.zu'} @:{'words.generated.einem'} @:{'words.generated.anderen'} @:{'words.generated.gespeicherten'} @:{'words.generated.kunden'}. @:{'words.generated.waehlen'} @.capitalize:{'words.generated.sie'}, @:{'words.generated.ob'} @:{'words.generated.der'} @:{'words.generated.ausgewaehlte'} @.capitalize:{'words.generated.kunde'} @:{'words.generated.beibehalten'} @:{'words.generated.oder'} @:{'words.generated.zum'} @:{'words.generated.fahrzeugkunden'} @:{'words.generated.gewechselt'} @:{'words.generated.werden'} @:{'words.generated.soll'}.",
|
||||||
@@ -3363,6 +3379,7 @@
|
|||||||
"benefit_portal": "@:{'words.generated.kundenportal'}",
|
"benefit_portal": "@:{'words.generated.kundenportal'}",
|
||||||
"benefit_portal_desc": "@.capitalize:{'words.generated.nutzen'} @.capitalize:{'words.generated.sie'} @:{'words.generated.das'} @:{'words.generated.kundenportal'}, @:{'words.generated.um'} @:{'words.generated.fahrzeuge'} hinzuzuf?@:{'words.generated.gen'}, @.upper:{'words.generated.w'}?@:{'words.generated.schen'} @:{'words.generated.zu'} @:{'words.generated.buchen'}, @:{'words.generated.rechnungen'} @:{'words.generated.und'} @:{'words.generated.waschzertifikate'} herunterzuladen @:{'words.generated.sowie'} @:{'words.generated.informationen'} @:{'words.generated.zu'} @:{'words.generated.fr'}?@:{'words.generated.heren'} @.upper:{'words.generated.w'}?@:{'words.generated.schen'} einzusehen",
|
"benefit_portal_desc": "@.capitalize:{'words.generated.nutzen'} @.capitalize:{'words.generated.sie'} @:{'words.generated.das'} @:{'words.generated.kundenportal'}, @:{'words.generated.um'} @:{'words.generated.fahrzeuge'} hinzuzuf?@:{'words.generated.gen'}, @.upper:{'words.generated.w'}?@:{'words.generated.schen'} @:{'words.generated.zu'} @:{'words.generated.buchen'}, @:{'words.generated.rechnungen'} @:{'words.generated.und'} @:{'words.generated.waschzertifikate'} herunterzuladen @:{'words.generated.sowie'} @:{'words.generated.informationen'} @:{'words.generated.zu'} @:{'words.generated.fr'}?@:{'words.generated.heren'} @.upper:{'words.generated.w'}?@:{'words.generated.schen'} einzusehen",
|
||||||
"benefit_wash_desc": "@.capitalize:{'words.generated.nutzen'} @.capitalize:{'words.generated.sie'} @:{'words.generated.unser'} @:{'words.generated.nationales'} @:{'words.generated.netz'} @:{'words.generated.von'} @:{'words.generated.waschhallen'} @:{'words.generated.zu'} @:{'words.generated.wettbewerbsf'}?@:{'words.generated.higen'} @:{'words.generated.preisen'}",
|
"benefit_wash_desc": "@.capitalize:{'words.generated.nutzen'} @.capitalize:{'words.generated.sie'} @:{'words.generated.unser'} @:{'words.generated.nationales'} @:{'words.generated.netz'} @:{'words.generated.von'} @:{'words.generated.waschhallen'} @:{'words.generated.zu'} @:{'words.generated.wettbewerbsf'}?@:{'words.generated.higen'} @:{'words.generated.preisen'}",
|
||||||
|
"ean_label": "@:{'templates.generated.compat.admin.pos.ean'}",
|
||||||
"email_label": "Firmen-@.upper:{'words.generated.e'}-@:{'words.generated.mail'}",
|
"email_label": "Firmen-@.upper:{'words.generated.e'}-@:{'words.generated.mail'}",
|
||||||
"intro": "@.capitalize:{'words.generated.als'} @.capitalize:{'words.generated.kunde'} @:{'words.generated.bei'} @:{'words.generated.truck'} @.capitalize:{'words.generated.wash'} @:{'words.generated.erhalten'} @.capitalize:{'words.generated.sie'} @:{'words.generated.zugang'} @:{'words.generated.zu'}:",
|
"intro": "@.capitalize:{'words.generated.als'} @.capitalize:{'words.generated.kunde'} @:{'words.generated.bei'} @:{'words.generated.truck'} @.capitalize:{'words.generated.wash'} @:{'words.generated.erhalten'} @.capitalize:{'words.generated.sie'} @:{'words.generated.zugang'} @:{'words.generated.zu'}:",
|
||||||
"phone_label": "@:{'templates.generated.compat.admin.pos.company_phone'}",
|
"phone_label": "@:{'templates.generated.compat.admin.pos.company_phone'}",
|
||||||
@@ -3578,6 +3595,13 @@
|
|||||||
"opening_hours": {
|
"opening_hours": {
|
||||||
"closed": "@:{'templates.generated.compat.global.closed'}"
|
"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'}",
|
"search_placeholder": "@.capitalize:{'words.generated.nach'} @:{'words.generated.abteilungsname'} @:{'words.generated.suchen'}",
|
||||||
"select_department": "@:{'templates.generated.compat.nav.select_department'}",
|
"select_department": "@:{'templates.generated.compat.nav.select_department'}",
|
||||||
"subtitle": "@:{'words.generated.abteilungen'} @:{'words.generated.verwalten'}",
|
"subtitle": "@:{'words.generated.abteilungen'} @:{'words.generated.verwalten'}",
|
||||||
@@ -4417,6 +4441,7 @@
|
|||||||
"departments": {
|
"departments": {
|
||||||
"columns": {
|
"columns": {
|
||||||
"archived": "Archiviert",
|
"archived": "Archiviert",
|
||||||
|
"custom_pricing_only": "@:{'templates.generated.compat.departments.pricing.custom_pricing_only'}",
|
||||||
"dimension": "Abmessung",
|
"dimension": "Abmessung",
|
||||||
"economic_department": "@:{'templates.generated.compat.objects.economic_departments.single'}",
|
"economic_department": "@:{'templates.generated.compat.objects.economic_departments.single'}",
|
||||||
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
|
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
|
||||||
@@ -5858,6 +5883,31 @@
|
|||||||
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
|
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
|
||||||
},
|
},
|
||||||
"other": "@.capitalize:{'words.generated.andere'}",
|
"other": "@.capitalize:{'words.generated.andere'}",
|
||||||
|
"overview": {
|
||||||
|
"active_vehicles": "Aktive Fahrzeuge",
|
||||||
|
"attributes": "Attribute",
|
||||||
|
"connected": "Verbunden",
|
||||||
|
"customer_management": "Kundenverwaltung",
|
||||||
|
"customer_number_required": "Eine Kundennummer ist erforderlich, bevor Kundeneinstellungen verwaltet werden können.",
|
||||||
|
"customer_rules": "Kundenregeln",
|
||||||
|
"effective_access": "Effektiver Zugriff",
|
||||||
|
"loaded": "Geladen",
|
||||||
|
"load_failed": "Benutzerdaten konnten nicht geladen werden",
|
||||||
|
"loading_user": "Benutzer wird geladen",
|
||||||
|
"management_hub": "Verwaltung",
|
||||||
|
"missing": "Fehlt",
|
||||||
|
"more_permissions": "+{count} weitere Berechtigungen",
|
||||||
|
"no_subscription_transactions": "Für diesen Kunden sind keine Waschabonnement-Transaktionen registriert.",
|
||||||
|
"not_invoiced": "Nicht fakturiert",
|
||||||
|
"open_draft": "Offener Entwurf",
|
||||||
|
"open_vehicles": "Fahrzeuge öffnen",
|
||||||
|
"price_overrides": "Preisüberschreibungen",
|
||||||
|
"pricing": "Preise",
|
||||||
|
"subscription_invoicing": "Abonnementabrechnung",
|
||||||
|
"subtitle": "Überblick über Kundendaten, Zugriff, Preise, Fahrzeuge und Abonnementabrechnung.",
|
||||||
|
"vehicles_load_failed": "Fahrzeuge konnten nicht geladen werden.",
|
||||||
|
"workspace_shortcuts": "Arbeitsbereich"
|
||||||
|
},
|
||||||
"permissions": "Tilladelser",
|
"permissions": "Tilladelser",
|
||||||
"user_data": "Brugerdata",
|
"user_data": "Brugerdata",
|
||||||
"user_id": "@:{'words.generated.benutzer'}-@.upper:{'words.generated.id'}",
|
"user_id": "@:{'words.generated.benutzer'}-@.upper:{'words.generated.id'}",
|
||||||
@@ -6064,6 +6114,20 @@
|
|||||||
},
|
},
|
||||||
"vehicles": {
|
"vehicles": {
|
||||||
"add": "@:{'words.generated.fahrzeug'} @:{'words.generated.hinzuf'}?@:{'words.generated.gen'}",
|
"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'}",
|
"brand": "@:{'templates.generated.compat.admin.pos.make'}",
|
||||||
"color": "Farbe",
|
"color": "Farbe",
|
||||||
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
|
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
|
||||||
@@ -6172,7 +6236,13 @@
|
|||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
"inactive": "Inaktiv",
|
"inactive": "Inaktiv",
|
||||||
"edit": "@.capitalize:{'words.generated.bearbeiten'}",
|
"edit": "@.capitalize:{'words.generated.bearbeiten'}",
|
||||||
"deactivate": "Deaktivieren"
|
"deactivate": "Deaktivieren",
|
||||||
|
"login_link": "QR-Login",
|
||||||
|
"login_link_title": "Mitarbeiter-Loginlink",
|
||||||
|
"login_link_qr_alt": "QR-Code-Loginlink",
|
||||||
|
"login_link_url": "Loginlink",
|
||||||
|
"copy_login_link": "Link kopieren",
|
||||||
|
"login_link_copied": "Link kopiert."
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -6202,7 +6272,8 @@
|
|||||||
"save_prices": "@.capitalize:{'words.generated.abteilungspreise'} @:{'words.generated.konnten'} @:{'words.generated.nicht'} @:{'words.generated.gespeichert'} @:{'words.generated.werden'}.",
|
"save_prices": "@.capitalize:{'words.generated.abteilungspreise'} @:{'words.generated.konnten'} @:{'words.generated.nicht'} @:{'words.generated.gespeichert'} @:{'words.generated.werden'}.",
|
||||||
"load_employees": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnten'} @:{'words.generated.nicht'} @:{'words.generated.geladen'} @:{'words.generated.werden'}.",
|
"load_employees": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnten'} @:{'words.generated.nicht'} @:{'words.generated.geladen'} @:{'words.generated.werden'}.",
|
||||||
"save_employee": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnte'} @:{'words.generated.nicht'} @:{'words.generated.gespeichert'} @:{'words.generated.werden'}.",
|
"save_employee": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnte'} @:{'words.generated.nicht'} @:{'words.generated.gespeichert'} @:{'words.generated.werden'}.",
|
||||||
"deactivate_employee": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnte'} @:{'words.generated.nicht'} @:{'words.generated.deaktiviert'} @:{'words.generated.werden'}."
|
"deactivate_employee": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnte'} @:{'words.generated.nicht'} @:{'words.generated.deaktiviert'} @:{'words.generated.werden'}.",
|
||||||
|
"generate_employee_login_link": "Mitarbeiter-Loginlink konnte nicht erstellt werden."
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "Rollenberechtigungen",
|
"title": "Rollenberechtigungen",
|
||||||
|
|||||||
@@ -1052,7 +1052,13 @@
|
|||||||
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
|
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
|
||||||
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
|
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
|
||||||
"edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}",
|
"edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}",
|
||||||
"deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}"
|
"deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}",
|
||||||
|
"login_link": "@:{'templates.generated.compat.limited_backoffice.employees.login_link'}",
|
||||||
|
"login_link_title": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_title'}",
|
||||||
|
"login_link_qr_alt": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_qr_alt'}",
|
||||||
|
"login_link_url": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_url'}",
|
||||||
|
"copy_login_link": "@:{'templates.generated.compat.limited_backoffice.employees.copy_login_link'}",
|
||||||
|
"login_link_copied": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_copied'}"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -1082,7 +1088,8 @@
|
|||||||
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
|
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
|
||||||
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
|
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
|
||||||
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
|
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
|
||||||
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}"
|
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}",
|
||||||
|
"generate_employee_login_link": "@:{'templates.generated.compat.limited_backoffice.errors.generate_employee_login_link'}"
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
|
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
|
||||||
@@ -1342,6 +1349,14 @@
|
|||||||
"subtitle_suffix": "@:{'words.generated.product'} @:{'words.generated.sales'}",
|
"subtitle_suffix": "@:{'words.generated.product'} @:{'words.generated.sales'}",
|
||||||
"title": "@.capitalize:{'words.generated.product'} @.capitalize:{'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": {
|
"reports": {
|
||||||
"bookings": {
|
"bookings": {
|
||||||
"completed_count": "@.capitalize:{'words.generated.completed'} @:{'words.generated.count'}",
|
"completed_count": "@.capitalize:{'words.generated.completed'} @:{'words.generated.count'}",
|
||||||
@@ -1536,6 +1551,7 @@
|
|||||||
"company_phone": "@.capitalize:{'words.generated.company'} @:{'words.generated.phone'}",
|
"company_phone": "@.capitalize:{'words.generated.company'} @:{'words.generated.phone'}",
|
||||||
"contact_email": "@.capitalize:{'words.generated.contact'} @:{'words.generated.email'}",
|
"contact_email": "@.capitalize:{'words.generated.contact'} @:{'words.generated.email'}",
|
||||||
"contact_phone": "@.capitalize:{'words.generated.contact'} @:{'words.generated.phone'}",
|
"contact_phone": "@.capitalize:{'words.generated.contact'} @:{'words.generated.phone'}",
|
||||||
|
"ean": "EAN",
|
||||||
"created_by": "@.capitalize:{'words.generated.created'} @:{'words.generated.by'}",
|
"created_by": "@.capitalize:{'words.generated.created'} @:{'words.generated.by'}",
|
||||||
"customer_conflict": {
|
"customer_conflict": {
|
||||||
"help_text": "@.capitalize:{'words.generated.this'} @:{'words.generated.typed'} @:{'words.generated.plate'} @:{'words.generated.belongs'} @:{'words.generated.to'} @:{'words.generated.another'} @:{'words.generated.saved'} @:{'words.generated.customer'}. @.capitalize:{'words.generated.choose'} @:{'words.generated.whether'} @:{'words.generated.to'} @:{'words.generated.keep'} @:{'words.replication.article.host_mention'} @:{'words.generated.selected'} @:{'words.generated.customer'} @:{'words.generated.or'} @:{'words.generated.switch'} @:{'words.generated.to'} @:{'words.replication.article.host_mention'} @:{'words.generated.vehicle'} @:{'words.generated.customer'}.",
|
"help_text": "@.capitalize:{'words.generated.this'} @:{'words.generated.typed'} @:{'words.generated.plate'} @:{'words.generated.belongs'} @:{'words.generated.to'} @:{'words.generated.another'} @:{'words.generated.saved'} @:{'words.generated.customer'}. @.capitalize:{'words.generated.choose'} @:{'words.generated.whether'} @:{'words.generated.to'} @:{'words.generated.keep'} @:{'words.replication.article.host_mention'} @:{'words.generated.selected'} @:{'words.generated.customer'} @:{'words.generated.or'} @:{'words.generated.switch'} @:{'words.generated.to'} @:{'words.replication.article.host_mention'} @:{'words.generated.vehicle'} @:{'words.generated.customer'}.",
|
||||||
@@ -3084,6 +3100,7 @@
|
|||||||
"benefit_portal": "@.capitalize:{'words.generated.customer'} @.capitalize:{'words.generated.portal'}",
|
"benefit_portal": "@.capitalize:{'words.generated.customer'} @.capitalize:{'words.generated.portal'}",
|
||||||
"benefit_portal_desc": "@.capitalize:{'words.generated.use'} @:{'words.replication.article.host_mention'} @:{'words.generated.customer'} @:{'words.generated.portal'} @:{'words.generated.to'} @:{'words.generated.add'} @:{'words.generated.vehicles'}, @:{'words.generated.book'} @:{'words.generated.washes'}, @:{'words.generated.download'} @:{'words.generated.invoices'}, @:{'words.generated.wash'} @:{'words.generated.certificates'} @:{'words.generated.and'} @:{'words.generated.information'} @:{'words.generated.for'} @:{'words.generated.historical'} @:{'words.generated.washes'}",
|
"benefit_portal_desc": "@.capitalize:{'words.generated.use'} @:{'words.replication.article.host_mention'} @:{'words.generated.customer'} @:{'words.generated.portal'} @:{'words.generated.to'} @:{'words.generated.add'} @:{'words.generated.vehicles'}, @:{'words.generated.book'} @:{'words.generated.washes'}, @:{'words.generated.download'} @:{'words.generated.invoices'}, @:{'words.generated.wash'} @:{'words.generated.certificates'} @:{'words.generated.and'} @:{'words.generated.information'} @:{'words.generated.for'} @:{'words.generated.historical'} @:{'words.generated.washes'}",
|
||||||
"benefit_wash_desc": "@.capitalize:{'words.generated.use'} @:{'words.generated.our'} @:{'words.generated.national'} @:{'words.generated.network'} @:{'words.generated.of'} @:{'words.generated.wash'} @:{'words.generated.halls'} @:{'words.generated.at'} @:{'words.generated.competitive'} @:{'words.generated.prices'}",
|
"benefit_wash_desc": "@.capitalize:{'words.generated.use'} @:{'words.generated.our'} @:{'words.generated.national'} @:{'words.generated.network'} @:{'words.generated.of'} @:{'words.generated.wash'} @:{'words.generated.halls'} @:{'words.generated.at'} @:{'words.generated.competitive'} @:{'words.generated.prices'}",
|
||||||
|
"ean_label": "@:{'templates.generated.compat.admin.pos.ean'}",
|
||||||
"email_label": "@.capitalize:{'words.generated.company'} @:{'words.generated.email'}",
|
"email_label": "@.capitalize:{'words.generated.company'} @:{'words.generated.email'}",
|
||||||
"intro": "@.capitalize:{'words.generated.as'} @:{'words.generated.a'} @:{'words.generated.customer'} @:{'words.generated.at'} @.capitalize:{'words.generated.truck'} @.capitalize:{'words.generated.wash'} @:{'words.generated.you'} @:{'words.generated.get'} @:{'words.generated.access'} @:{'words.generated.to'}:",
|
"intro": "@.capitalize:{'words.generated.as'} @:{'words.generated.a'} @:{'words.generated.customer'} @:{'words.generated.at'} @.capitalize:{'words.generated.truck'} @.capitalize:{'words.generated.wash'} @:{'words.generated.you'} @:{'words.generated.get'} @:{'words.generated.access'} @:{'words.generated.to'}:",
|
||||||
"phone_label": "@:{'templates.generated.compat.admin.pos.company_phone'}",
|
"phone_label": "@:{'templates.generated.compat.admin.pos.company_phone'}",
|
||||||
@@ -5587,6 +5604,31 @@
|
|||||||
"transaction_history": "@.capitalize:{'words.generated.transaction'} @:{'words.generated.history'}"
|
"transaction_history": "@.capitalize:{'words.generated.transaction'} @:{'words.generated.history'}"
|
||||||
},
|
},
|
||||||
"other": "@:{'templates.generated.compat.global.other'}",
|
"other": "@:{'templates.generated.compat.global.other'}",
|
||||||
|
"overview": {
|
||||||
|
"active_vehicles": "Active vehicles",
|
||||||
|
"attributes": "Attributes",
|
||||||
|
"connected": "Connected",
|
||||||
|
"customer_management": "Customer management",
|
||||||
|
"customer_number_required": "A customer number is required before customer settings can be managed.",
|
||||||
|
"customer_rules": "Customer rules",
|
||||||
|
"effective_access": "Effective access",
|
||||||
|
"loaded": "Loaded",
|
||||||
|
"load_failed": "User data could not be loaded",
|
||||||
|
"loading_user": "Loading user",
|
||||||
|
"management_hub": "Management hub",
|
||||||
|
"missing": "Missing",
|
||||||
|
"more_permissions": "+{count} more permissions",
|
||||||
|
"no_subscription_transactions": "No wash subscription transactions are registered for this customer.",
|
||||||
|
"not_invoiced": "Not invoiced",
|
||||||
|
"open_draft": "Open draft",
|
||||||
|
"open_vehicles": "Open vehicles",
|
||||||
|
"price_overrides": "Price overrides",
|
||||||
|
"pricing": "Pricing",
|
||||||
|
"subscription_invoicing": "Subscription invoicing",
|
||||||
|
"subtitle": "Overview of customer data, access, pricing, vehicles, and subscription invoicing.",
|
||||||
|
"vehicles_load_failed": "Vehicles could not be loaded.",
|
||||||
|
"workspace_shortcuts": "Workspace shortcuts"
|
||||||
|
},
|
||||||
"permissions": "@.capitalize:{'words.generated.permissions'}",
|
"permissions": "@.capitalize:{'words.generated.permissions'}",
|
||||||
"user_data": "@.capitalize:{'words.generated.user'} @:{'words.generated.data'}",
|
"user_data": "@.capitalize:{'words.generated.user'} @:{'words.generated.data'}",
|
||||||
"user_id": "@.capitalize:{'words.generated.user'} @.upper:{'words.generated.id'}",
|
"user_id": "@.capitalize:{'words.generated.user'} @.upper:{'words.generated.id'}",
|
||||||
@@ -5915,7 +5957,13 @@
|
|||||||
"active": "Active",
|
"active": "Active",
|
||||||
"inactive": "@.capitalize:{'words.generated.inactive'}",
|
"inactive": "@.capitalize:{'words.generated.inactive'}",
|
||||||
"edit": "@.capitalize:{'words.generated.edit'}",
|
"edit": "@.capitalize:{'words.generated.edit'}",
|
||||||
"deactivate": "@.capitalize:{'words.generated.deactivate'}"
|
"deactivate": "@.capitalize:{'words.generated.deactivate'}",
|
||||||
|
"login_link": "QR login",
|
||||||
|
"login_link_title": "Employee login link",
|
||||||
|
"login_link_qr_alt": "QR code login link",
|
||||||
|
"login_link_url": "Login link",
|
||||||
|
"copy_login_link": "Copy link",
|
||||||
|
"login_link_copied": "Link copied."
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -5945,7 +5993,8 @@
|
|||||||
"save_prices": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.save'} @:{'words.generated.department'} @:{'words.generated.prices'}.",
|
"save_prices": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.save'} @:{'words.generated.department'} @:{'words.generated.prices'}.",
|
||||||
"load_employees": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.load'} @:{'words.generated.employees'}.",
|
"load_employees": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.load'} @:{'words.generated.employees'}.",
|
||||||
"save_employee": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.save'} @:{'words.generated.employee'}.",
|
"save_employee": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.save'} @:{'words.generated.employee'}.",
|
||||||
"deactivate_employee": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.deactivate'} @:{'words.generated.employee'}."
|
"deactivate_employee": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.deactivate'} @:{'words.generated.employee'}.",
|
||||||
|
"generate_employee_login_link": "Could not create employee login link."
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "Role permissions",
|
"title": "Role permissions",
|
||||||
|
|||||||
@@ -126,6 +126,14 @@
|
|||||||
"subtitle_suffix": "@:{'templates.generated.compat.admin.daily_report.product_sales.subtitle_suffix'}",
|
"subtitle_suffix": "@:{'templates.generated.compat.admin.daily_report.product_sales.subtitle_suffix'}",
|
||||||
"title": "@:{'templates.generated.compat.admin.daily_report.product_sales.title'}"
|
"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": {
|
"reports": {
|
||||||
"avg_revenue": {
|
"avg_revenue": {
|
||||||
"label": "@:{'templates.generated.compat.department_reports.avg_revenue_per_wash'}",
|
"label": "@:{'templates.generated.compat.department_reports.avg_revenue_per_wash'}",
|
||||||
@@ -385,6 +393,7 @@
|
|||||||
"delete": "@:{'templates.generated.compat.global.delete'}",
|
"delete": "@:{'templates.generated.compat.global.delete'}",
|
||||||
"details": "@:{'templates.generated.compat.admin.pos.details'}",
|
"details": "@:{'templates.generated.compat.admin.pos.details'}",
|
||||||
"download": "@:{'templates.generated.compat.guest.pwa.title'}",
|
"download": "@:{'templates.generated.compat.guest.pwa.title'}",
|
||||||
|
"ean": "@:{'templates.generated.compat.admin.pos.ean'}",
|
||||||
"drafts_assignment": {
|
"drafts_assignment": {
|
||||||
"button": "@:{'templates.generated.compat.admin.pos.drafts_assignment.button'}",
|
"button": "@:{'templates.generated.compat.admin.pos.drafts_assignment.button'}",
|
||||||
"create_invoice_collection": "@:{'templates.generated.compat.admin.pos.drafts_assignment.create_invoice_collection'}",
|
"create_invoice_collection": "@:{'templates.generated.compat.admin.pos.drafts_assignment.create_invoice_collection'}",
|
||||||
@@ -2061,6 +2070,7 @@
|
|||||||
"benefit_wash": "@:{'templates.generated.compat.tables.bookings.wash'}",
|
"benefit_wash": "@:{'templates.generated.compat.tables.bookings.wash'}",
|
||||||
"benefit_wash_desc": "@:{'templates.generated.compat.tables.bookings.wash_desc'}",
|
"benefit_wash_desc": "@:{'templates.generated.compat.tables.bookings.wash_desc'}",
|
||||||
"cvr_label": "@:{'templates.generated.compat.customers.cvr'}",
|
"cvr_label": "@:{'templates.generated.compat.customers.cvr'}",
|
||||||
|
"ean_label": "@:{'templates.generated.compat.customer_creation.customer.ean_label'}",
|
||||||
"email_label": "@:{'templates.generated.compat.customer_creation.customer.email_label'}",
|
"email_label": "@:{'templates.generated.compat.customer_creation.customer.email_label'}",
|
||||||
"intro": "@:{'templates.generated.compat.customer_creation.customer.intro'}",
|
"intro": "@:{'templates.generated.compat.customer_creation.customer.intro'}",
|
||||||
"phone_label": "@:{'templates.generated.compat.customer_creation.customer.phone_label'}",
|
"phone_label": "@:{'templates.generated.compat.customer_creation.customer.phone_label'}",
|
||||||
@@ -4580,8 +4590,8 @@
|
|||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
"modules": "Modules",
|
"modules": "Modules",
|
||||||
"branding": "Profile & Branding",
|
"branding": "Profile & Branding",
|
||||||
"gateways": "Gateways",
|
"gateways": "@.capitalize:{'words.generated.gateways'}",
|
||||||
"stripe": "Stripe",
|
"stripe": "@:{'words.generated.stripe'}",
|
||||||
"pricing": "Pricing",
|
"pricing": "Pricing",
|
||||||
"categories": "Categories"
|
"categories": "Categories"
|
||||||
},
|
},
|
||||||
@@ -4624,7 +4634,7 @@
|
|||||||
"department_id": "Department ID",
|
"department_id": "Department ID",
|
||||||
"economic_department_id": "Economic department",
|
"economic_department_id": "Economic department",
|
||||||
"branding": "Branding",
|
"branding": "Branding",
|
||||||
"created_at": "Created",
|
"created_at": "@:common.created",
|
||||||
"updated_at": "Updated"
|
"updated_at": "Updated"
|
||||||
},
|
},
|
||||||
"hardware": {
|
"hardware": {
|
||||||
@@ -4640,12 +4650,12 @@
|
|||||||
"quick_links": {
|
"quick_links": {
|
||||||
"title": "Department tools",
|
"title": "Department tools",
|
||||||
"subtitle": "Open the focused setup areas for this department",
|
"subtitle": "Open the focused setup areas for this department",
|
||||||
"modules": "Modules",
|
"modules": "@:superuser_dashboard.department_navigation.modules",
|
||||||
"branding": "Branding",
|
"branding": "@:superuser_dashboard.department_overview.profile.branding",
|
||||||
"gateways": "Gateways",
|
"gateways": "@:superuser_dashboard.department_navigation.gateways",
|
||||||
"stripe": "Stripe",
|
"stripe": "@:superuser_dashboard.department_navigation.stripe",
|
||||||
"pricing": "Pricing",
|
"pricing": "@:superuser_dashboard.department_navigation.pricing",
|
||||||
"categories": "Categories"
|
"categories": "@:superuser_dashboard.department_navigation.categories"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"invalid_department": "A valid department is required",
|
"invalid_department": "A valid department is required",
|
||||||
@@ -5630,6 +5640,31 @@
|
|||||||
"transaction_history": "@:{'templates.generated.compat.user_admin.orders.transaction_history'}"
|
"transaction_history": "@:{'templates.generated.compat.user_admin.orders.transaction_history'}"
|
||||||
},
|
},
|
||||||
"other": "@:{'templates.generated.compat.user_admin.other'}",
|
"other": "@:{'templates.generated.compat.user_admin.other'}",
|
||||||
|
"overview": {
|
||||||
|
"active_vehicles": "@:{'templates.generated.compat.user_admin.overview.active_vehicles'}",
|
||||||
|
"attributes": "@:{'templates.generated.compat.user_admin.overview.attributes'}",
|
||||||
|
"connected": "@:{'templates.generated.compat.user_admin.overview.connected'}",
|
||||||
|
"customer_management": "@:{'templates.generated.compat.user_admin.overview.customer_management'}",
|
||||||
|
"customer_number_required": "@:{'templates.generated.compat.user_admin.overview.customer_number_required'}",
|
||||||
|
"customer_rules": "@:{'templates.generated.compat.user_admin.overview.customer_rules'}",
|
||||||
|
"effective_access": "@:{'templates.generated.compat.user_admin.overview.effective_access'}",
|
||||||
|
"loaded": "@:{'templates.generated.compat.user_admin.overview.loaded'}",
|
||||||
|
"load_failed": "@:{'templates.generated.compat.user_admin.overview.load_failed'}",
|
||||||
|
"loading_user": "@:{'templates.generated.compat.user_admin.overview.loading_user'}",
|
||||||
|
"management_hub": "@:{'templates.generated.compat.user_admin.overview.management_hub'}",
|
||||||
|
"missing": "@:{'templates.generated.compat.user_admin.overview.missing'}",
|
||||||
|
"more_permissions": "@:{'templates.generated.compat.user_admin.overview.more_permissions'}",
|
||||||
|
"no_subscription_transactions": "@:{'templates.generated.compat.user_admin.overview.no_subscription_transactions'}",
|
||||||
|
"not_invoiced": "@:{'templates.generated.compat.user_admin.overview.not_invoiced'}",
|
||||||
|
"open_draft": "@:{'templates.generated.compat.user_admin.overview.open_draft'}",
|
||||||
|
"open_vehicles": "@:{'templates.generated.compat.user_admin.overview.open_vehicles'}",
|
||||||
|
"price_overrides": "@:{'templates.generated.compat.user_admin.overview.price_overrides'}",
|
||||||
|
"pricing": "@:{'templates.generated.compat.user_admin.overview.pricing'}",
|
||||||
|
"subscription_invoicing": "@:{'templates.generated.compat.user_admin.overview.subscription_invoicing'}",
|
||||||
|
"subtitle": "@:{'templates.generated.compat.user_admin.overview.subtitle'}",
|
||||||
|
"vehicles_load_failed": "@:{'templates.generated.compat.user_admin.overview.vehicles_load_failed'}",
|
||||||
|
"workspace_shortcuts": "@:{'templates.generated.compat.user_admin.overview.workspace_shortcuts'}"
|
||||||
|
},
|
||||||
"permissions": "@:{'templates.generated.compat.user_admin.permissions'}",
|
"permissions": "@:{'templates.generated.compat.user_admin.permissions'}",
|
||||||
"subtitle": "@:user_admin.user_data",
|
"subtitle": "@:user_admin.user_data",
|
||||||
"title": "@:{'templates.generated.compat.common.user'}",
|
"title": "@:{'templates.generated.compat.common.user'}",
|
||||||
|
|||||||
@@ -1334,7 +1334,13 @@
|
|||||||
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
|
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
|
||||||
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
|
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
|
||||||
"edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}",
|
"edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}",
|
||||||
"deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}"
|
"deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}",
|
||||||
|
"login_link": "@:{'templates.generated.compat.limited_backoffice.employees.login_link'}",
|
||||||
|
"login_link_title": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_title'}",
|
||||||
|
"login_link_qr_alt": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_qr_alt'}",
|
||||||
|
"login_link_url": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_url'}",
|
||||||
|
"copy_login_link": "@:{'templates.generated.compat.limited_backoffice.employees.copy_login_link'}",
|
||||||
|
"login_link_copied": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_copied'}"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -1364,7 +1370,8 @@
|
|||||||
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
|
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
|
||||||
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
|
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
|
||||||
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
|
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
|
||||||
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}"
|
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}",
|
||||||
|
"generate_employee_login_link": "@:{'templates.generated.compat.limited_backoffice.errors.generate_employee_login_link'}"
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
|
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
|
||||||
@@ -1624,6 +1631,14 @@
|
|||||||
"subtitle_suffix": "@:{'words.generated.produktsalg'}",
|
"subtitle_suffix": "@:{'words.generated.produktsalg'}",
|
||||||
"title": "@.capitalize:{'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": {
|
"reports": {
|
||||||
"bookings": {
|
"bookings": {
|
||||||
"completed_count": "@.capitalize:{'words.generated.fullført'} @:{'words.generated.telling'}",
|
"completed_count": "@.capitalize:{'words.generated.fullført'} @:{'words.generated.telling'}",
|
||||||
@@ -1818,6 +1833,7 @@
|
|||||||
"company_phone": "Firmatelefon",
|
"company_phone": "Firmatelefon",
|
||||||
"contact_email": "@.capitalize:{'words.generated.kontakt'} @:{'words.generated.e'}-@:{'words.generated.post'}",
|
"contact_email": "@.capitalize:{'words.generated.kontakt'} @:{'words.generated.e'}-@:{'words.generated.post'}",
|
||||||
"contact_phone": "@.capitalize:{'words.generated.kontakt'} @:{'words.generated.telefon'}",
|
"contact_phone": "@.capitalize:{'words.generated.kontakt'} @:{'words.generated.telefon'}",
|
||||||
|
"ean": "EAN",
|
||||||
"created_by": "Laget @:{'words.generated.av'}",
|
"created_by": "Laget @:{'words.generated.av'}",
|
||||||
"customer_conflict": {
|
"customer_conflict": {
|
||||||
"help_text": "@.capitalize:{'words.generated.det'} @:{'words.generated.innskrevne'} @:{'words.generated.skiltet'} @:{'words.generated.tilhoerer'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.annen'} @:{'words.generated.lagret'} @:{'words.generated.kunde'}. @.capitalize:{'words.generated.velg'} @:{'words.generated.om'} @:{'words.replication.article.host_mention'} @:{'words.generated.valgte'} @:{'words.generated.kunden'} @:{'words.generated.skal'} @:{'words.generated.beholdes'} @:{'words.generated.eller'} @:{'words.generated.om'} @:{'words.generated.det'} @:{'words.generated.skal'} @:{'words.generated.byttes'} @:{'words.generated.til'} @:{'words.generated.kjoretoykunden'}.",
|
"help_text": "@.capitalize:{'words.generated.det'} @:{'words.generated.innskrevne'} @:{'words.generated.skiltet'} @:{'words.generated.tilhoerer'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.annen'} @:{'words.generated.lagret'} @:{'words.generated.kunde'}. @.capitalize:{'words.generated.velg'} @:{'words.generated.om'} @:{'words.replication.article.host_mention'} @:{'words.generated.valgte'} @:{'words.generated.kunden'} @:{'words.generated.skal'} @:{'words.generated.beholdes'} @:{'words.generated.eller'} @:{'words.generated.om'} @:{'words.generated.det'} @:{'words.generated.skal'} @:{'words.generated.byttes'} @:{'words.generated.til'} @:{'words.generated.kjoretoykunden'}.",
|
||||||
@@ -3366,6 +3382,7 @@
|
|||||||
"benefit_portal": "@:{'words.generated.kundeportal'}",
|
"benefit_portal": "@:{'words.generated.kundeportal'}",
|
||||||
"benefit_portal_desc": "@.capitalize:{'words.generated.bruk'} kundeportalen @:{'words.generated.til'} @:{'words.generated.a'} @:{'words.generated.legge'} @:{'words.generated.til'} @:{'words.generated.kjøretøy'}, bokvaske, @:{'words.generated.laste'} @:{'words.generated.ned'} @:{'words.generated.fakturaer'}, @:{'words.generated.vaskesertifikater'} @:{'words.generated.og'} @:{'words.generated.informasjon'} @:{'words.generated.for'} @:{'words.generated.historiske'} @:{'words.generated.vask'}.",
|
"benefit_portal_desc": "@.capitalize:{'words.generated.bruk'} kundeportalen @:{'words.generated.til'} @:{'words.generated.a'} @:{'words.generated.legge'} @:{'words.generated.til'} @:{'words.generated.kjøretøy'}, bokvaske, @:{'words.generated.laste'} @:{'words.generated.ned'} @:{'words.generated.fakturaer'}, @:{'words.generated.vaskesertifikater'} @:{'words.generated.og'} @:{'words.generated.informasjon'} @:{'words.generated.for'} @:{'words.generated.historiske'} @:{'words.generated.vask'}.",
|
||||||
"benefit_wash_desc": "@.capitalize:{'words.generated.bruk'} @:{'words.generated.vart'} @:{'words.generated.nasjonale'} @:{'words.generated.nettverk'} @:{'words.generated.av'} @:{'words.generated.vaskehaller'} @:{'words.generated.til'} @:{'words.generated.konkurransedyktige'} @:{'words.generated.priser'}",
|
"benefit_wash_desc": "@.capitalize:{'words.generated.bruk'} @:{'words.generated.vart'} @:{'words.generated.nasjonale'} @:{'words.generated.nettverk'} @:{'words.generated.av'} @:{'words.generated.vaskehaller'} @:{'words.generated.til'} @:{'words.generated.konkurransedyktige'} @:{'words.generated.priser'}",
|
||||||
|
"ean_label": "@:{'templates.generated.compat.admin.pos.ean'}",
|
||||||
"email_label": "Firmaets @:{'words.generated.e'}-@:{'words.generated.post'}",
|
"email_label": "Firmaets @:{'words.generated.e'}-@:{'words.generated.post'}",
|
||||||
"intro": "@.capitalize:{'words.generated.som'} @:{'words.generated.kunde'} @:{'words.generated.hos'} @:{'words.generated.truck'} @.capitalize:{'words.generated.wash'} @:{'words.generated.far'} @:{'words.generated.du'} @:{'words.generated.tilgang'} @:{'words.generated.til'}:",
|
"intro": "@.capitalize:{'words.generated.som'} @:{'words.generated.kunde'} @:{'words.generated.hos'} @:{'words.generated.truck'} @.capitalize:{'words.generated.wash'} @:{'words.generated.far'} @:{'words.generated.du'} @:{'words.generated.tilgang'} @:{'words.generated.til'}:",
|
||||||
"phone_label": "@:{'templates.generated.compat.admin.pos.company_phone'}",
|
"phone_label": "@:{'templates.generated.compat.admin.pos.company_phone'}",
|
||||||
@@ -3581,6 +3598,13 @@
|
|||||||
"opening_hours": {
|
"opening_hours": {
|
||||||
"closed": "Stengt"
|
"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'}",
|
"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'}",
|
"select_department": "@.capitalize:{'words.generated.velg'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.avdeling'}",
|
||||||
"subtitle": "@.capitalize:{'words.generated.administrer'} @:{'words.generated.avdelinger'}",
|
"subtitle": "@.capitalize:{'words.generated.administrer'} @:{'words.generated.avdelinger'}",
|
||||||
@@ -4420,6 +4444,7 @@
|
|||||||
"departments": {
|
"departments": {
|
||||||
"columns": {
|
"columns": {
|
||||||
"archived": "Arkivert",
|
"archived": "Arkivert",
|
||||||
|
"custom_pricing_only": "@:{'templates.generated.compat.departments.pricing.custom_pricing_only'}",
|
||||||
"dimension": "Dimensjon",
|
"dimension": "Dimensjon",
|
||||||
"economic_department": "@.upper:{'words.generated.e'}-@:{'words.generated.økonomisk'} @:{'words.generated.avdeling'}",
|
"economic_department": "@.upper:{'words.generated.e'}-@:{'words.generated.økonomisk'} @:{'words.generated.avdeling'}",
|
||||||
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
|
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
|
||||||
@@ -5861,6 +5886,31 @@
|
|||||||
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
|
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
|
||||||
},
|
},
|
||||||
"other": "@:{'templates.generated.compat.global.other'}",
|
"other": "@:{'templates.generated.compat.global.other'}",
|
||||||
|
"overview": {
|
||||||
|
"active_vehicles": "Aktive kjøretøy",
|
||||||
|
"attributes": "Attributter",
|
||||||
|
"connected": "Tilkoblet",
|
||||||
|
"customer_management": "Kundestyring",
|
||||||
|
"customer_number_required": "Et kundenummer kreves før kundeinnstillinger kan administreres.",
|
||||||
|
"customer_rules": "Kunderegler",
|
||||||
|
"effective_access": "Effektiv tilgang",
|
||||||
|
"loaded": "Lastet",
|
||||||
|
"load_failed": "Brukerdata kunne ikke lastes",
|
||||||
|
"loading_user": "Laster bruker",
|
||||||
|
"management_hub": "Administrasjon",
|
||||||
|
"missing": "Mangler",
|
||||||
|
"more_permissions": "+{count} flere tillatelser",
|
||||||
|
"no_subscription_transactions": "Ingen vaskeabonnementstransaksjoner er registrert for kunden.",
|
||||||
|
"not_invoiced": "Ikke fakturert",
|
||||||
|
"open_draft": "Åpent utkast",
|
||||||
|
"open_vehicles": "Åpne kjøretøy",
|
||||||
|
"price_overrides": "Prisoverstyringer",
|
||||||
|
"pricing": "Priser",
|
||||||
|
"subscription_invoicing": "Abonnementsfakturering",
|
||||||
|
"subtitle": "Oversikt over kundedata, tilgang, priser, kjøretøy og abonnementsfakturering.",
|
||||||
|
"vehicles_load_failed": "Kjøretøy kunne ikke lastes.",
|
||||||
|
"workspace_shortcuts": "Arbeidsområde"
|
||||||
|
},
|
||||||
"permissions": "@.capitalize:{'words.generated.tillatelser'}",
|
"permissions": "@.capitalize:{'words.generated.tillatelser'}",
|
||||||
"user_data": "@.capitalize:{'words.generated.brukerdata'}",
|
"user_data": "@.capitalize:{'words.generated.brukerdata'}",
|
||||||
"user_id": "@.capitalize:{'words.generated.bruker'}-@.upper:{'words.generated.id'}",
|
"user_id": "@.capitalize:{'words.generated.bruker'}-@.upper:{'words.generated.id'}",
|
||||||
@@ -6067,6 +6117,20 @@
|
|||||||
},
|
},
|
||||||
"vehicles": {
|
"vehicles": {
|
||||||
"add": "@:{'words.generated.lagg'} @:{'words.generated.till'} @:{'words.generated.fordon'}",
|
"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",
|
"brand": "Märke",
|
||||||
"color": "Färg",
|
"color": "Färg",
|
||||||
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
|
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
|
||||||
@@ -6175,7 +6239,13 @@
|
|||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
"inactive": "Inaktiv",
|
"inactive": "Inaktiv",
|
||||||
"edit": "@:{'words.generated.rediger'}",
|
"edit": "@:{'words.generated.rediger'}",
|
||||||
"deactivate": "Deaktiver"
|
"deactivate": "Deaktiver",
|
||||||
|
"login_link": "QR-innlogging",
|
||||||
|
"login_link_title": "Innloggingslenke for medarbeider",
|
||||||
|
"login_link_qr_alt": "QR-kode for innloggingslenke",
|
||||||
|
"login_link_url": "Innloggingslenke",
|
||||||
|
"copy_login_link": "Kopier lenke",
|
||||||
|
"login_link_copied": "Lenken er kopiert."
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -6205,7 +6275,8 @@
|
|||||||
"save_prices": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.lagre'} @:{'words.generated.avdelingspriser'}.",
|
"save_prices": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.lagre'} @:{'words.generated.avdelingspriser'}.",
|
||||||
"load_employees": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.laste'} @:{'words.generated.medarbeidere'}.",
|
"load_employees": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.laste'} @:{'words.generated.medarbeidere'}.",
|
||||||
"save_employee": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.lagre'} @:{'words.generated.medarbeideren'}.",
|
"save_employee": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.lagre'} @:{'words.generated.medarbeideren'}.",
|
||||||
"deactivate_employee": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} deaktivere @:{'words.generated.medarbeideren'}."
|
"deactivate_employee": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} deaktivere @:{'words.generated.medarbeideren'}.",
|
||||||
|
"generate_employee_login_link": "Kunne ikke opprette innloggingslenke for medarbeideren."
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "Rolletillatelser",
|
"title": "Rolletillatelser",
|
||||||
|
|||||||
@@ -1384,7 +1384,13 @@
|
|||||||
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
|
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
|
||||||
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
|
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
|
||||||
"edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}",
|
"edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}",
|
||||||
"deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}"
|
"deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}",
|
||||||
|
"login_link": "@:{'templates.generated.compat.limited_backoffice.employees.login_link'}",
|
||||||
|
"login_link_title": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_title'}",
|
||||||
|
"login_link_qr_alt": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_qr_alt'}",
|
||||||
|
"login_link_url": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_url'}",
|
||||||
|
"copy_login_link": "@:{'templates.generated.compat.limited_backoffice.employees.copy_login_link'}",
|
||||||
|
"login_link_copied": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_copied'}"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -1414,7 +1420,8 @@
|
|||||||
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
|
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
|
||||||
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
|
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
|
||||||
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
|
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
|
||||||
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}"
|
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}",
|
||||||
|
"generate_employee_login_link": "@:{'templates.generated.compat.limited_backoffice.errors.generate_employee_login_link'}"
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
|
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
|
||||||
@@ -1674,6 +1681,14 @@
|
|||||||
"subtitle_suffix": "@:{'words.generated.produktforsaljning'}",
|
"subtitle_suffix": "@:{'words.generated.produktforsaljning'}",
|
||||||
"title": "@.capitalize:{'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": {
|
"reports": {
|
||||||
"bookings": {
|
"bookings": {
|
||||||
"completed_count": "@.capitalize:{'words.generated.antal'} @:{'words.generated.slutforda'}",
|
"completed_count": "@.capitalize:{'words.generated.antal'} @:{'words.generated.slutforda'}",
|
||||||
@@ -1868,6 +1883,7 @@
|
|||||||
"company_phone": "Färetagstelefon",
|
"company_phone": "Färetagstelefon",
|
||||||
"contact_email": "@.capitalize:{'words.generated.kontakt'}-@:{'words.generated.e'}-@:{'words.generated.post'}",
|
"contact_email": "@.capitalize:{'words.generated.kontakt'}-@:{'words.generated.e'}-@:{'words.generated.post'}",
|
||||||
"contact_phone": "Kontakttelefon",
|
"contact_phone": "Kontakttelefon",
|
||||||
|
"ean": "EAN",
|
||||||
"created_by": "@.capitalize:{'words.generated.skapad'} @:{'words.generated.av'}",
|
"created_by": "@.capitalize:{'words.generated.skapad'} @:{'words.generated.av'}",
|
||||||
"customer_conflict": {
|
"customer_conflict": {
|
||||||
"help_text": "@.capitalize:{'words.generated.det'} @:{'words.generated.angivna'} @:{'words.generated.registreringsnumret'} @:{'words.generated.tillhor'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.annan'} @:{'words.generated.sparad'} @:{'words.generated.kund'}. @:{'words.generated.valj_2'} @:{'words.generated.om'} @:{'words.replication.article.host_mention'} @:{'words.generated.valda'} @:{'words.generated.kunden'} @:{'words.generated.ska'} @:{'words.generated.behallas'} @:{'words.generated.eller'} @:{'words.generated.om'} @:{'words.generated.bytet'} @:{'words.generated.ska'} @:{'words.generated.goras'} @:{'words.generated.till'} @:{'words.generated.fordonskunden'}.",
|
"help_text": "@.capitalize:{'words.generated.det'} @:{'words.generated.angivna'} @:{'words.generated.registreringsnumret'} @:{'words.generated.tillhor'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.annan'} @:{'words.generated.sparad'} @:{'words.generated.kund'}. @:{'words.generated.valj_2'} @:{'words.generated.om'} @:{'words.replication.article.host_mention'} @:{'words.generated.valda'} @:{'words.generated.kunden'} @:{'words.generated.ska'} @:{'words.generated.behallas'} @:{'words.generated.eller'} @:{'words.generated.om'} @:{'words.generated.bytet'} @:{'words.generated.ska'} @:{'words.generated.goras'} @:{'words.generated.till'} @:{'words.generated.fordonskunden'}.",
|
||||||
@@ -3416,6 +3432,7 @@
|
|||||||
"benefit_portal": "@:{'words.generated.kundportal'}",
|
"benefit_portal": "@:{'words.generated.kundportal'}",
|
||||||
"benefit_portal_desc": "@.capitalize:{'words.generated.anvand'} kundportalen @:{'words.generated.for'} @:{'words.generated.att'} @:{'words.generated.lagga'} @:{'words.generated.till'} @:{'words.generated.fordon'}, @:{'words.generated.boka'} @:{'words.generated.tvattar'}, @:{'words.generated.ladda'} @:{'words.generated.ner'} @:{'words.generated.fakturor'}, @:{'words.generated.tvattcertifikat'} @:{'words.generated.och'} @:{'words.generated.information'} @:{'words.generated.om'} @:{'words.generated.historiska'} @:{'words.generated.tvattar'}",
|
"benefit_portal_desc": "@.capitalize:{'words.generated.anvand'} kundportalen @:{'words.generated.for'} @:{'words.generated.att'} @:{'words.generated.lagga'} @:{'words.generated.till'} @:{'words.generated.fordon'}, @:{'words.generated.boka'} @:{'words.generated.tvattar'}, @:{'words.generated.ladda'} @:{'words.generated.ner'} @:{'words.generated.fakturor'}, @:{'words.generated.tvattcertifikat'} @:{'words.generated.och'} @:{'words.generated.information'} @:{'words.generated.om'} @:{'words.generated.historiska'} @:{'words.generated.tvattar'}",
|
||||||
"benefit_wash_desc": "@.capitalize:{'words.generated.anvand'} @:{'words.generated.vart'} @:{'words.generated.nationella'} @:{'words.generated.natverk'} @:{'words.generated.av'} @:{'words.generated.tvatthallar'} @:{'words.generated.till'} @:{'words.generated.konkurrenskraftiga'} @:{'words.generated.priser'}",
|
"benefit_wash_desc": "@.capitalize:{'words.generated.anvand'} @:{'words.generated.vart'} @:{'words.generated.nationella'} @:{'words.generated.natverk'} @:{'words.generated.av'} @:{'words.generated.tvatthallar'} @:{'words.generated.till'} @:{'words.generated.konkurrenskraftiga'} @:{'words.generated.priser'}",
|
||||||
|
"ean_label": "@:{'templates.generated.compat.admin.pos.ean'}",
|
||||||
"email_label": "Företagets @:{'words.generated.e'}-@:{'words.generated.post'}",
|
"email_label": "Företagets @:{'words.generated.e'}-@:{'words.generated.post'}",
|
||||||
"intro": "@.capitalize:{'words.generated.som'} @:{'words.generated.kund'} @:{'words.generated.hos'} @:{'words.generated.truck'} @.capitalize:{'words.generated.wash'} @:{'words.generated.far'} @:{'words.generated.du'} @:{'words.generated.tillgang'} @:{'words.generated.till'}:",
|
"intro": "@.capitalize:{'words.generated.som'} @:{'words.generated.kund'} @:{'words.generated.hos'} @:{'words.generated.truck'} @.capitalize:{'words.generated.wash'} @:{'words.generated.far'} @:{'words.generated.du'} @:{'words.generated.tillgang'} @:{'words.generated.till'}:",
|
||||||
"phone_label": "@:{'templates.generated.compat.admin.pos.company_phone'}",
|
"phone_label": "@:{'templates.generated.compat.admin.pos.company_phone'}",
|
||||||
@@ -3631,6 +3648,13 @@
|
|||||||
"opening_hours": {
|
"opening_hours": {
|
||||||
"closed": "Stengt"
|
"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'}",
|
"search_placeholder": "@:{'words.generated.sok'} @:{'words.generated.efter'} @:{'words.generated.avdelningsnamn'}",
|
||||||
"select_department": "@:{'words.generated.velg'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.avdeling'}",
|
"select_department": "@:{'words.generated.velg'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.avdeling'}",
|
||||||
"subtitle": "@:{'words.generated.administrer'} @:{'words.generated.avdelinger'}",
|
"subtitle": "@:{'words.generated.administrer'} @:{'words.generated.avdelinger'}",
|
||||||
@@ -4470,6 +4494,7 @@
|
|||||||
"departments": {
|
"departments": {
|
||||||
"columns": {
|
"columns": {
|
||||||
"archived": "Arkiverad",
|
"archived": "Arkiverad",
|
||||||
|
"custom_pricing_only": "@:{'templates.generated.compat.departments.pricing.custom_pricing_only'}",
|
||||||
"dimension": "Dimension",
|
"dimension": "Dimension",
|
||||||
"economic_department": "@:{'templates.generated.compat.objects.economic_departments.single'}",
|
"economic_department": "@:{'templates.generated.compat.objects.economic_departments.single'}",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
@@ -5911,6 +5936,31 @@
|
|||||||
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
|
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
|
||||||
},
|
},
|
||||||
"other": "Andet",
|
"other": "Andet",
|
||||||
|
"overview": {
|
||||||
|
"active_vehicles": "Aktiva fordon",
|
||||||
|
"attributes": "Attribut",
|
||||||
|
"connected": "Ansluten",
|
||||||
|
"customer_management": "Kundhantering",
|
||||||
|
"customer_number_required": "Ett kundnummer krävs innan kundinställningar kan hanteras.",
|
||||||
|
"customer_rules": "Kundregler",
|
||||||
|
"effective_access": "Effektiv åtkomst",
|
||||||
|
"loaded": "Inläst",
|
||||||
|
"load_failed": "Användardata kunde inte läsas in",
|
||||||
|
"loading_user": "Läser in användare",
|
||||||
|
"management_hub": "Administration",
|
||||||
|
"missing": "Saknas",
|
||||||
|
"more_permissions": "+{count} fler behörigheter",
|
||||||
|
"no_subscription_transactions": "Inga tvättabonnemangstransaktioner är registrerade för kunden.",
|
||||||
|
"not_invoiced": "Ej fakturerat",
|
||||||
|
"open_draft": "Öppet utkast",
|
||||||
|
"open_vehicles": "Öppna fordon",
|
||||||
|
"price_overrides": "Prisöverskrivningar",
|
||||||
|
"pricing": "Priser",
|
||||||
|
"subscription_invoicing": "Abonnemangsfakturering",
|
||||||
|
"subtitle": "Översikt över kunddata, åtkomst, priser, fordon och abonnemangsfakturering.",
|
||||||
|
"vehicles_load_failed": "Fordon kunde inte läsas in.",
|
||||||
|
"workspace_shortcuts": "Arbetsyta"
|
||||||
|
},
|
||||||
"permissions": "Tilladelser",
|
"permissions": "Tilladelser",
|
||||||
"user_data": "Brugerdata",
|
"user_data": "Brugerdata",
|
||||||
"user_id": "Användar-@.upper:{'words.generated.id'}",
|
"user_id": "Användar-@.upper:{'words.generated.id'}",
|
||||||
@@ -6117,6 +6167,20 @@
|
|||||||
},
|
},
|
||||||
"vehicles": {
|
"vehicles": {
|
||||||
"add": "@:{'words.generated.lagg'} @:{'words.generated.till'} @:{'words.generated.fordon'}",
|
"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",
|
"brand": "Märke",
|
||||||
"color": "Färg",
|
"color": "Färg",
|
||||||
"delete_vehicle": "@.capitalize:{'words.generated.ta'} @:{'words.generated.bort'} @:{'words.generated.fordon'}",
|
"delete_vehicle": "@.capitalize:{'words.generated.ta'} @:{'words.generated.bort'} @:{'words.generated.fordon'}",
|
||||||
@@ -6225,7 +6289,13 @@
|
|||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
"inactive": "Inaktiv",
|
"inactive": "Inaktiv",
|
||||||
"edit": "@:{'words.generated.redigera'}",
|
"edit": "@:{'words.generated.redigera'}",
|
||||||
"deactivate": "@.capitalize:{'words.generated.inaktivera'}"
|
"deactivate": "@.capitalize:{'words.generated.inaktivera'}",
|
||||||
|
"login_link": "QR-inloggning",
|
||||||
|
"login_link_title": "Inloggningslänk för medarbetare",
|
||||||
|
"login_link_qr_alt": "QR-kod för inloggningslänk",
|
||||||
|
"login_link_url": "Inloggningslänk",
|
||||||
|
"copy_login_link": "Kopiera länk",
|
||||||
|
"login_link_copied": "Länken har kopierats."
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -6255,7 +6325,8 @@
|
|||||||
"save_prices": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.spara'} @:{'words.generated.avdelningspriser'}.",
|
"save_prices": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.spara'} @:{'words.generated.avdelningspriser'}.",
|
||||||
"load_employees": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.ladda'} @:{'words.generated.medarbetare'}.",
|
"load_employees": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.ladda'} @:{'words.generated.medarbetare'}.",
|
||||||
"save_employee": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.spara'} @:{'words.generated.medarbetaren'}.",
|
"save_employee": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.spara'} @:{'words.generated.medarbetaren'}.",
|
||||||
"deactivate_employee": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.inaktivera'} @:{'words.generated.medarbetaren'}."
|
"deactivate_employee": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.inaktivera'} @:{'words.generated.medarbetaren'}.",
|
||||||
|
"generate_employee_login_link": "Kunde inte skapa inloggningslänk för medarbetaren."
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "Rollbehörigheter",
|
"title": "Rollbehörigheter",
|
||||||
|
|||||||
@@ -27,6 +27,14 @@
|
|||||||
"subtitle_suffix": "@:{'terms.glossary.salg'} @:{'terms.glossary.af'} produktet",
|
"subtitle_suffix": "@:{'terms.glossary.salg'} @:{'terms.glossary.af'} produktet",
|
||||||
"title": "Produktsalg"
|
"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": {
|
"reports": {
|
||||||
"bookings": {
|
"bookings": {
|
||||||
"completed_count": "@.capitalize:{'terms.glossary.antal'} @:{'terms.glossary.fuldførte'}",
|
"completed_count": "@.capitalize:{'terms.glossary.antal'} @:{'terms.glossary.fuldførte'}",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"company_phone": "Virksomhedstelefon",
|
"company_phone": "Virksomhedstelefon",
|
||||||
"contact_email": "@.capitalize:{'terms.glossary.kontakt'}-@:{'terms.glossary.e'}-@:{'terms.glossary.mail'}",
|
"contact_email": "@.capitalize:{'terms.glossary.kontakt'}-@:{'terms.glossary.e'}-@:{'terms.glossary.mail'}",
|
||||||
"contact_phone": "Kontakttelefon",
|
"contact_phone": "Kontakttelefon",
|
||||||
|
"ean": "EAN",
|
||||||
"created_by": "@.capitalize:{'terms.glossary.oprettet'} @:{'terms.glossary.af'}",
|
"created_by": "@.capitalize:{'terms.glossary.oprettet'} @:{'terms.glossary.af'}",
|
||||||
"customer_conflict": {
|
"customer_conflict": {
|
||||||
"help_text": "@.capitalize:{'terms.replication.article.host_mention'} @:{'terms.glossary.indtastede'} @:{'terms.glossary.nummerplade'} @:{'terms.glossary.tilhører'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.anden'} @:{'terms.glossary.gemt'} @:{'terms.glossary.kunde'}. @.capitalize:{'terms.glossary.vælg'} @:{'terms.glossary.om'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.valgte'} @:{'terms.glossary.kunde'} @:{'terms.glossary.skal'} @:{'terms.glossary.beholdes'}, @:{'terms.glossary.eller'} @:{'terms.glossary.om'} @:{'terms.glossary.der'} @:{'terms.glossary.skal'} @:{'terms.glossary.skiftes'} @:{'terms.glossary.til'} @:{'terms.glossary.køretøjets'} @:{'terms.glossary.kunde'}.",
|
"help_text": "@.capitalize:{'terms.replication.article.host_mention'} @:{'terms.glossary.indtastede'} @:{'terms.glossary.nummerplade'} @:{'terms.glossary.tilhører'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.anden'} @:{'terms.glossary.gemt'} @:{'terms.glossary.kunde'}. @.capitalize:{'terms.glossary.vælg'} @:{'terms.glossary.om'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.valgte'} @:{'terms.glossary.kunde'} @:{'terms.glossary.skal'} @:{'terms.glossary.beholdes'}, @:{'terms.glossary.eller'} @:{'terms.glossary.om'} @:{'terms.glossary.der'} @:{'terms.glossary.skal'} @:{'terms.glossary.skiftes'} @:{'terms.glossary.til'} @:{'terms.glossary.køretøjets'} @:{'terms.glossary.kunde'}.",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"benefit_portal": "@:{'terms.glossary.kundeportal'}",
|
"benefit_portal": "@:{'terms.glossary.kundeportal'}",
|
||||||
"benefit_portal_desc": "@:{'terms.glossary.benyt'} kundeportalen @:{'terms.glossary.til'} @:{'terms.glossary.at'} @:{'terms.glossary.tilføje'} @:{'terms.glossary.køretøjer'}, @:{'terms.glossary.booke'} @:{'terms.glossary.vaske'}, downloade @:{'terms.glossary.fakturaer'}, @:{'terms.glossary.vaskecertifikater'} @:{'terms.glossary.og'} @:{'terms.glossary.information'} @:{'terms.glossary.for'} @:{'terms.glossary.historiske'} @:{'terms.glossary.vaske'}",
|
"benefit_portal_desc": "@:{'terms.glossary.benyt'} kundeportalen @:{'terms.glossary.til'} @:{'terms.glossary.at'} @:{'terms.glossary.tilføje'} @:{'terms.glossary.køretøjer'}, @:{'terms.glossary.booke'} @:{'terms.glossary.vaske'}, downloade @:{'terms.glossary.fakturaer'}, @:{'terms.glossary.vaskecertifikater'} @:{'terms.glossary.og'} @:{'terms.glossary.information'} @:{'terms.glossary.for'} @:{'terms.glossary.historiske'} @:{'terms.glossary.vaske'}",
|
||||||
"benefit_wash_desc": "@:{'terms.glossary.benyt'} @:{'terms.glossary.vores'} @:{'terms.glossary.nationale'} @:{'terms.glossary.netværk'} @:{'terms.glossary.af'} @:{'terms.glossary.vaskehaller'} @:{'terms.glossary.til'} @:{'terms.glossary.konkurrencedygtige'} @:{'terms.glossary.priser'}",
|
"benefit_wash_desc": "@:{'terms.glossary.benyt'} @:{'terms.glossary.vores'} @:{'terms.glossary.nationale'} @:{'terms.glossary.netværk'} @:{'terms.glossary.af'} @:{'terms.glossary.vaskehaller'} @:{'terms.glossary.til'} @:{'terms.glossary.konkurrencedygtige'} @:{'terms.glossary.priser'}",
|
||||||
|
"ean_label": "@:{'phrases.compat.admin.pos.ean'}",
|
||||||
"email_label": "@:{'terms.glossary.firma'} @:{'terms.glossary.email'}",
|
"email_label": "@:{'terms.glossary.firma'} @:{'terms.glossary.email'}",
|
||||||
"intro": "@.capitalize:{'terms.glossary.som'} @:{'terms.glossary.kunde'} @:{'terms.glossary.hos'} @:{'terms.glossary.truck'} @:{'terms.glossary.wash'} @:{'terms.glossary.far'} @:{'terms.glossary.du'} @:{'terms.glossary.adgang'} @:{'terms.glossary.til'}:",
|
"intro": "@.capitalize:{'terms.glossary.som'} @:{'terms.glossary.kunde'} @:{'terms.glossary.hos'} @:{'terms.glossary.truck'} @:{'terms.glossary.wash'} @:{'terms.glossary.far'} @:{'terms.glossary.du'} @:{'terms.glossary.adgang'} @:{'terms.glossary.til'}:",
|
||||||
"phone_label": "@:{'terms.glossary.firma'} @:{'terms.glossary.telefon'}",
|
"phone_label": "@:{'terms.glossary.firma'} @:{'terms.glossary.telefon'}",
|
||||||
|
|||||||
@@ -14,6 +14,31 @@
|
|||||||
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
|
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
|
||||||
},
|
},
|
||||||
"other": "@:{'phrases.compat.global.other'}",
|
"other": "@:{'phrases.compat.global.other'}",
|
||||||
|
"overview": {
|
||||||
|
"active_vehicles": "Aktive køretøjer",
|
||||||
|
"attributes": "Attributter",
|
||||||
|
"connected": "Forbundet",
|
||||||
|
"customer_management": "Kundestyring",
|
||||||
|
"customer_number_required": "Et kundenummer er påkrævet før kundeindstillinger kan administreres.",
|
||||||
|
"customer_rules": "Kunderegler",
|
||||||
|
"effective_access": "Effektiv adgang",
|
||||||
|
"loaded": "Indlæst",
|
||||||
|
"load_failed": "Brugerdata kunne ikke indlæses",
|
||||||
|
"loading_user": "Indlæser bruger",
|
||||||
|
"management_hub": "Administrationshub",
|
||||||
|
"missing": "Mangler",
|
||||||
|
"more_permissions": "+{count} flere tilladelser",
|
||||||
|
"no_subscription_transactions": "Der er ikke registreret vaskeabonnementstransaktioner for kunden.",
|
||||||
|
"not_invoiced": "Ikke faktureret",
|
||||||
|
"open_draft": "Åben kladde",
|
||||||
|
"open_vehicles": "Åbn køretøjer",
|
||||||
|
"price_overrides": "Prisoverstyringer",
|
||||||
|
"pricing": "Priser",
|
||||||
|
"subscription_invoicing": "Abonnementsfakturering",
|
||||||
|
"subtitle": "Overblik over kundedata, adgang, priser, køretøjer og abonnementsfakturering.",
|
||||||
|
"vehicles_load_failed": "Køretøjer kunne ikke indlæses.",
|
||||||
|
"workspace_shortcuts": "Arbejdsgange"
|
||||||
|
},
|
||||||
"permissions": "@.capitalize:{'terms.glossary.tilladelser'}",
|
"permissions": "@.capitalize:{'terms.glossary.tilladelser'}",
|
||||||
"user_data": "@.capitalize:{'terms.glossary.brugerdata'}",
|
"user_data": "@.capitalize:{'terms.glossary.brugerdata'}",
|
||||||
"user_id": "@.capitalize:{'terms.glossary.bruger'} @.upper:{'terms.glossary.id'}",
|
"user_id": "@.capitalize:{'terms.glossary.bruger'} @.upper:{'terms.glossary.id'}",
|
||||||
|
|||||||
@@ -76,7 +76,13 @@
|
|||||||
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
|
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
|
||||||
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
|
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
|
||||||
"edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}",
|
"edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}",
|
||||||
"deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}"
|
"deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}",
|
||||||
|
"login_link": "@:{'phrases.compat.limited_backoffice.employees.login_link'}",
|
||||||
|
"login_link_title": "@:{'phrases.compat.limited_backoffice.employees.login_link_title'}",
|
||||||
|
"login_link_qr_alt": "@:{'phrases.compat.limited_backoffice.employees.login_link_qr_alt'}",
|
||||||
|
"login_link_url": "@:{'phrases.compat.limited_backoffice.employees.login_link_url'}",
|
||||||
|
"copy_login_link": "@:{'phrases.compat.limited_backoffice.employees.copy_login_link'}",
|
||||||
|
"login_link_copied": "@:{'phrases.compat.limited_backoffice.employees.login_link_copied'}"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -106,7 +112,8 @@
|
|||||||
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
|
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
|
||||||
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
|
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
|
||||||
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
|
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
|
||||||
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}"
|
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}",
|
||||||
|
"generate_employee_login_link": "@:{'phrases.compat.limited_backoffice.errors.generate_employee_login_link'}"
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
|
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
|
||||||
@@ -321,7 +328,13 @@
|
|||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
"inactive": "Inaktiv",
|
"inactive": "Inaktiv",
|
||||||
"edit": "@:{'terms.glossary.rediger'}",
|
"edit": "@:{'terms.glossary.rediger'}",
|
||||||
"deactivate": "Deaktiver"
|
"deactivate": "Deaktiver",
|
||||||
|
"login_link": "QR-login",
|
||||||
|
"login_link_title": "Medarbejderloginlink",
|
||||||
|
"login_link_qr_alt": "QR-kode-loginlink",
|
||||||
|
"login_link_url": "Loginlink",
|
||||||
|
"copy_login_link": "Kopiér link",
|
||||||
|
"login_link_copied": "Linket er kopieret."
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -351,7 +364,8 @@
|
|||||||
"save_prices": "@.capitalize:{'terms.glossary.afdelingspriser'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.gemmes'}.",
|
"save_prices": "@.capitalize:{'terms.glossary.afdelingspriser'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.gemmes'}.",
|
||||||
"load_employees": "@.capitalize:{'terms.glossary.medarbejdere'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.indlæses'}.",
|
"load_employees": "@.capitalize:{'terms.glossary.medarbejdere'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.indlæses'}.",
|
||||||
"save_employee": "@.capitalize:{'terms.glossary.medarbejderen'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.gemmes'}.",
|
"save_employee": "@.capitalize:{'terms.glossary.medarbejderen'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.gemmes'}.",
|
||||||
"deactivate_employee": "@.capitalize:{'terms.glossary.medarbejderen'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} deaktiveres."
|
"deactivate_employee": "@.capitalize:{'terms.glossary.medarbejderen'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} deaktiveres.",
|
||||||
|
"generate_employee_login_link": "Medarbejderens loginlink kunne ikke oprettes."
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "Rolletilladelser",
|
"title": "Rolletilladelser",
|
||||||
|
|||||||
@@ -27,6 +27,14 @@
|
|||||||
"subtitle_suffix": "Produktverk?@:{'terms.glossary.ufe'}",
|
"subtitle_suffix": "Produktverk?@:{'terms.glossary.ufe'}",
|
||||||
"title": "@:{'phrases.compat.admin.daily_report.product_sales.subtitle_suffix'}"
|
"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": {
|
"reports": {
|
||||||
"bookings": {
|
"bookings": {
|
||||||
"completed_count": "@:{'terms.glossary.abgeschlossene'} @:{'terms.glossary.anzahl'}",
|
"completed_count": "@:{'terms.glossary.abgeschlossene'} @:{'terms.glossary.anzahl'}",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"company_phone": "Firmentelefon",
|
"company_phone": "Firmentelefon",
|
||||||
"contact_email": "@:{'terms.glossary.kontakt'}-@.upper:{'terms.glossary.e'}-@:{'terms.glossary.mail'}",
|
"contact_email": "@:{'terms.glossary.kontakt'}-@.upper:{'terms.glossary.e'}-@:{'terms.glossary.mail'}",
|
||||||
"contact_phone": "Kontakttelefonnummer",
|
"contact_phone": "Kontakttelefonnummer",
|
||||||
|
"ean": "EAN",
|
||||||
"created_by": "@.capitalize:{'terms.glossary.erstellt'} @:{'terms.glossary.von'}",
|
"created_by": "@.capitalize:{'terms.glossary.erstellt'} @:{'terms.glossary.von'}",
|
||||||
"customer_conflict": {
|
"customer_conflict": {
|
||||||
"help_text": "@.capitalize:{'terms.glossary.dieses'} @:{'terms.glossary.kennzeichen'} @:{'terms.glossary.gehoert'} @:{'terms.glossary.zu'} @:{'terms.glossary.einem'} @:{'terms.glossary.anderen'} @:{'terms.glossary.gespeicherten'} @:{'terms.glossary.kunden'}. @:{'terms.glossary.waehlen'} @.capitalize:{'terms.glossary.sie'}, @:{'terms.glossary.ob'} @:{'terms.glossary.der'} @:{'terms.glossary.ausgewaehlte'} @.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.beibehalten'} @:{'terms.glossary.oder'} @:{'terms.glossary.zum'} @:{'terms.glossary.fahrzeugkunden'} @:{'terms.glossary.gewechselt'} @:{'terms.glossary.werden'} @:{'terms.glossary.soll'}.",
|
"help_text": "@.capitalize:{'terms.glossary.dieses'} @:{'terms.glossary.kennzeichen'} @:{'terms.glossary.gehoert'} @:{'terms.glossary.zu'} @:{'terms.glossary.einem'} @:{'terms.glossary.anderen'} @:{'terms.glossary.gespeicherten'} @:{'terms.glossary.kunden'}. @:{'terms.glossary.waehlen'} @.capitalize:{'terms.glossary.sie'}, @:{'terms.glossary.ob'} @:{'terms.glossary.der'} @:{'terms.glossary.ausgewaehlte'} @.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.beibehalten'} @:{'terms.glossary.oder'} @:{'terms.glossary.zum'} @:{'terms.glossary.fahrzeugkunden'} @:{'terms.glossary.gewechselt'} @:{'terms.glossary.werden'} @:{'terms.glossary.soll'}.",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"benefit_portal": "@:{'terms.glossary.kundenportal'}",
|
"benefit_portal": "@:{'terms.glossary.kundenportal'}",
|
||||||
"benefit_portal_desc": "@.capitalize:{'terms.glossary.nutzen'} @.capitalize:{'terms.glossary.sie'} @:{'terms.glossary.das'} @:{'terms.glossary.kundenportal'}, @:{'terms.glossary.um'} @:{'terms.glossary.fahrzeuge'} hinzuzuf?@:{'terms.glossary.gen'}, @.upper:{'terms.glossary.w'}?@:{'terms.glossary.schen'} @:{'terms.glossary.zu'} @:{'terms.glossary.buchen'}, @:{'terms.glossary.rechnungen'} @:{'terms.glossary.und'} @:{'terms.glossary.waschzertifikate'} herunterzuladen @:{'terms.glossary.sowie'} @:{'terms.glossary.informationen'} @:{'terms.glossary.zu'} @:{'terms.glossary.fr'}?@:{'terms.glossary.heren'} @.upper:{'terms.glossary.w'}?@:{'terms.glossary.schen'} einzusehen",
|
"benefit_portal_desc": "@.capitalize:{'terms.glossary.nutzen'} @.capitalize:{'terms.glossary.sie'} @:{'terms.glossary.das'} @:{'terms.glossary.kundenportal'}, @:{'terms.glossary.um'} @:{'terms.glossary.fahrzeuge'} hinzuzuf?@:{'terms.glossary.gen'}, @.upper:{'terms.glossary.w'}?@:{'terms.glossary.schen'} @:{'terms.glossary.zu'} @:{'terms.glossary.buchen'}, @:{'terms.glossary.rechnungen'} @:{'terms.glossary.und'} @:{'terms.glossary.waschzertifikate'} herunterzuladen @:{'terms.glossary.sowie'} @:{'terms.glossary.informationen'} @:{'terms.glossary.zu'} @:{'terms.glossary.fr'}?@:{'terms.glossary.heren'} @.upper:{'terms.glossary.w'}?@:{'terms.glossary.schen'} einzusehen",
|
||||||
"benefit_wash_desc": "@.capitalize:{'terms.glossary.nutzen'} @.capitalize:{'terms.glossary.sie'} @:{'terms.glossary.unser'} @:{'terms.glossary.nationales'} @:{'terms.glossary.netz'} @:{'terms.glossary.von'} @:{'terms.glossary.waschhallen'} @:{'terms.glossary.zu'} @:{'terms.glossary.wettbewerbsf'}?@:{'terms.glossary.higen'} @:{'terms.glossary.preisen'}",
|
"benefit_wash_desc": "@.capitalize:{'terms.glossary.nutzen'} @.capitalize:{'terms.glossary.sie'} @:{'terms.glossary.unser'} @:{'terms.glossary.nationales'} @:{'terms.glossary.netz'} @:{'terms.glossary.von'} @:{'terms.glossary.waschhallen'} @:{'terms.glossary.zu'} @:{'terms.glossary.wettbewerbsf'}?@:{'terms.glossary.higen'} @:{'terms.glossary.preisen'}",
|
||||||
|
"ean_label": "@:{'phrases.compat.admin.pos.ean'}",
|
||||||
"email_label": "Firmen-@.upper:{'terms.glossary.e'}-@:{'terms.glossary.mail'}",
|
"email_label": "Firmen-@.upper:{'terms.glossary.e'}-@:{'terms.glossary.mail'}",
|
||||||
"intro": "@.capitalize:{'terms.glossary.als'} @.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.bei'} @:{'terms.glossary.truck'} @.capitalize:{'terms.glossary.wash'} @:{'terms.glossary.erhalten'} @.capitalize:{'terms.glossary.sie'} @:{'terms.glossary.zugang'} @:{'terms.glossary.zu'}:",
|
"intro": "@.capitalize:{'terms.glossary.als'} @.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.bei'} @:{'terms.glossary.truck'} @.capitalize:{'terms.glossary.wash'} @:{'terms.glossary.erhalten'} @.capitalize:{'terms.glossary.sie'} @:{'terms.glossary.zugang'} @:{'terms.glossary.zu'}:",
|
||||||
"phone_label": "@:{'phrases.compat.admin.pos.company_phone'}",
|
"phone_label": "@:{'phrases.compat.admin.pos.company_phone'}",
|
||||||
|
|||||||
@@ -21,6 +21,13 @@
|
|||||||
"opening_hours": {
|
"opening_hours": {
|
||||||
"closed": "@:{'phrases.compat.global.closed'}"
|
"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'}",
|
"search_placeholder": "@.capitalize:{'terms.glossary.nach'} @:{'terms.glossary.abteilungsname'} @:{'terms.glossary.suchen'}",
|
||||||
"select_department": "@:{'phrases.compat.nav.select_department'}",
|
"select_department": "@:{'phrases.compat.nav.select_department'}",
|
||||||
"subtitle": "@:{'terms.glossary.abteilungen'} @:{'terms.glossary.verwalten'}",
|
"subtitle": "@:{'terms.glossary.abteilungen'} @:{'terms.glossary.verwalten'}",
|
||||||
|
|||||||
@@ -140,6 +140,7 @@
|
|||||||
"departments": {
|
"departments": {
|
||||||
"columns": {
|
"columns": {
|
||||||
"archived": "Archiviert",
|
"archived": "Archiviert",
|
||||||
|
"custom_pricing_only": "@:{'phrases.compat.departments.pricing.custom_pricing_only'}",
|
||||||
"dimension": "Abmessung",
|
"dimension": "Abmessung",
|
||||||
"economic_department": "@:{'phrases.compat.objects.economic_departments.single'}",
|
"economic_department": "@:{'phrases.compat.objects.economic_departments.single'}",
|
||||||
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
|
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
|
||||||
|
|||||||
@@ -14,6 +14,31 @@
|
|||||||
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
|
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
|
||||||
},
|
},
|
||||||
"other": "@.capitalize:{'terms.glossary.andere'}",
|
"other": "@.capitalize:{'terms.glossary.andere'}",
|
||||||
|
"overview": {
|
||||||
|
"active_vehicles": "Aktive Fahrzeuge",
|
||||||
|
"attributes": "Attribute",
|
||||||
|
"connected": "Verbunden",
|
||||||
|
"customer_management": "Kundenverwaltung",
|
||||||
|
"customer_number_required": "Eine Kundennummer ist erforderlich, bevor Kundeneinstellungen verwaltet werden können.",
|
||||||
|
"customer_rules": "Kundenregeln",
|
||||||
|
"effective_access": "Effektiver Zugriff",
|
||||||
|
"loaded": "Geladen",
|
||||||
|
"load_failed": "Benutzerdaten konnten nicht geladen werden",
|
||||||
|
"loading_user": "Benutzer wird geladen",
|
||||||
|
"management_hub": "Verwaltung",
|
||||||
|
"missing": "Fehlt",
|
||||||
|
"more_permissions": "+{count} weitere Berechtigungen",
|
||||||
|
"no_subscription_transactions": "Für diesen Kunden sind keine Waschabonnement-Transaktionen registriert.",
|
||||||
|
"not_invoiced": "Nicht fakturiert",
|
||||||
|
"open_draft": "Offener Entwurf",
|
||||||
|
"open_vehicles": "Fahrzeuge öffnen",
|
||||||
|
"price_overrides": "Preisüberschreibungen",
|
||||||
|
"pricing": "Preise",
|
||||||
|
"subscription_invoicing": "Abonnementabrechnung",
|
||||||
|
"subtitle": "Überblick über Kundendaten, Zugriff, Preise, Fahrzeuge und Abonnementabrechnung.",
|
||||||
|
"vehicles_load_failed": "Fahrzeuge konnten nicht geladen werden.",
|
||||||
|
"workspace_shortcuts": "Arbeitsbereich"
|
||||||
|
},
|
||||||
"permissions": "Tilladelser",
|
"permissions": "Tilladelser",
|
||||||
"user_data": "Brugerdata",
|
"user_data": "Brugerdata",
|
||||||
"user_id": "@:{'terms.glossary.benutzer'}-@.upper:{'terms.glossary.id'}",
|
"user_id": "@:{'terms.glossary.benutzer'}-@.upper:{'terms.glossary.id'}",
|
||||||
|
|||||||
@@ -2,6 +2,20 @@
|
|||||||
"compat": {
|
"compat": {
|
||||||
"vehicles": {
|
"vehicles": {
|
||||||
"add": "@:{'terms.glossary.fahrzeug'} @:{'terms.glossary.hinzuf'}?@:{'terms.glossary.gen'}",
|
"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'}",
|
"brand": "@:{'phrases.compat.admin.pos.make'}",
|
||||||
"color": "Farbe",
|
"color": "Farbe",
|
||||||
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
|
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
|
||||||
|
|||||||
@@ -76,7 +76,13 @@
|
|||||||
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
|
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
|
||||||
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
|
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
|
||||||
"edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}",
|
"edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}",
|
||||||
"deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}"
|
"deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}",
|
||||||
|
"login_link": "@:{'phrases.compat.limited_backoffice.employees.login_link'}",
|
||||||
|
"login_link_title": "@:{'phrases.compat.limited_backoffice.employees.login_link_title'}",
|
||||||
|
"login_link_qr_alt": "@:{'phrases.compat.limited_backoffice.employees.login_link_qr_alt'}",
|
||||||
|
"login_link_url": "@:{'phrases.compat.limited_backoffice.employees.login_link_url'}",
|
||||||
|
"copy_login_link": "@:{'phrases.compat.limited_backoffice.employees.copy_login_link'}",
|
||||||
|
"login_link_copied": "@:{'phrases.compat.limited_backoffice.employees.login_link_copied'}"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -106,7 +112,8 @@
|
|||||||
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
|
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
|
||||||
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
|
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
|
||||||
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
|
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
|
||||||
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}"
|
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}",
|
||||||
|
"generate_employee_login_link": "@:{'phrases.compat.limited_backoffice.errors.generate_employee_login_link'}"
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
|
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
|
||||||
@@ -321,7 +328,13 @@
|
|||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
"inactive": "Inaktiv",
|
"inactive": "Inaktiv",
|
||||||
"edit": "@.capitalize:{'terms.glossary.bearbeiten'}",
|
"edit": "@.capitalize:{'terms.glossary.bearbeiten'}",
|
||||||
"deactivate": "Deaktivieren"
|
"deactivate": "Deaktivieren",
|
||||||
|
"login_link": "QR-Login",
|
||||||
|
"login_link_title": "Mitarbeiter-Loginlink",
|
||||||
|
"login_link_qr_alt": "QR-Code-Loginlink",
|
||||||
|
"login_link_url": "Loginlink",
|
||||||
|
"copy_login_link": "Link kopieren",
|
||||||
|
"login_link_copied": "Link kopiert."
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -351,7 +364,8 @@
|
|||||||
"save_prices": "@.capitalize:{'terms.glossary.abteilungspreise'} @:{'terms.glossary.konnten'} @:{'terms.glossary.nicht'} @:{'terms.glossary.gespeichert'} @:{'terms.glossary.werden'}.",
|
"save_prices": "@.capitalize:{'terms.glossary.abteilungspreise'} @:{'terms.glossary.konnten'} @:{'terms.glossary.nicht'} @:{'terms.glossary.gespeichert'} @:{'terms.glossary.werden'}.",
|
||||||
"load_employees": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnten'} @:{'terms.glossary.nicht'} @:{'terms.glossary.geladen'} @:{'terms.glossary.werden'}.",
|
"load_employees": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnten'} @:{'terms.glossary.nicht'} @:{'terms.glossary.geladen'} @:{'terms.glossary.werden'}.",
|
||||||
"save_employee": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnte'} @:{'terms.glossary.nicht'} @:{'terms.glossary.gespeichert'} @:{'terms.glossary.werden'}.",
|
"save_employee": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnte'} @:{'terms.glossary.nicht'} @:{'terms.glossary.gespeichert'} @:{'terms.glossary.werden'}.",
|
||||||
"deactivate_employee": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnte'} @:{'terms.glossary.nicht'} @:{'terms.glossary.deaktiviert'} @:{'terms.glossary.werden'}."
|
"deactivate_employee": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnte'} @:{'terms.glossary.nicht'} @:{'terms.glossary.deaktiviert'} @:{'terms.glossary.werden'}.",
|
||||||
|
"generate_employee_login_link": "Mitarbeiter-Loginlink konnte nicht erstellt werden."
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "Rollenberechtigungen",
|
"title": "Rollenberechtigungen",
|
||||||
|
|||||||
@@ -27,6 +27,14 @@
|
|||||||
"subtitle_suffix": "@:{'terms.glossary.product'} @:{'terms.glossary.sales'}",
|
"subtitle_suffix": "@:{'terms.glossary.product'} @:{'terms.glossary.sales'}",
|
||||||
"title": "@.capitalize:{'terms.glossary.product'} @.capitalize:{'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": {
|
"reports": {
|
||||||
"bookings": {
|
"bookings": {
|
||||||
"completed_count": "@.capitalize:{'terms.glossary.completed'} @:{'terms.glossary.count'}",
|
"completed_count": "@.capitalize:{'terms.glossary.completed'} @:{'terms.glossary.count'}",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"company_phone": "@.capitalize:{'terms.glossary.company'} @:{'terms.glossary.phone'}",
|
"company_phone": "@.capitalize:{'terms.glossary.company'} @:{'terms.glossary.phone'}",
|
||||||
"contact_email": "@.capitalize:{'terms.glossary.contact'} @:{'terms.glossary.email'}",
|
"contact_email": "@.capitalize:{'terms.glossary.contact'} @:{'terms.glossary.email'}",
|
||||||
"contact_phone": "@.capitalize:{'terms.glossary.contact'} @:{'terms.glossary.phone'}",
|
"contact_phone": "@.capitalize:{'terms.glossary.contact'} @:{'terms.glossary.phone'}",
|
||||||
|
"ean": "EAN",
|
||||||
"created_by": "@.capitalize:{'terms.glossary.created'} @:{'terms.glossary.by'}",
|
"created_by": "@.capitalize:{'terms.glossary.created'} @:{'terms.glossary.by'}",
|
||||||
"customer_conflict": {
|
"customer_conflict": {
|
||||||
"help_text": "@.capitalize:{'terms.glossary.this'} @:{'terms.glossary.typed'} @:{'terms.glossary.plate'} @:{'terms.glossary.belongs'} @:{'terms.glossary.to'} @:{'terms.glossary.another'} @:{'terms.glossary.saved'} @:{'terms.glossary.customer'}. @.capitalize:{'terms.glossary.choose'} @:{'terms.glossary.whether'} @:{'terms.glossary.to'} @:{'terms.glossary.keep'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.selected'} @:{'terms.glossary.customer'} @:{'terms.glossary.or'} @:{'terms.glossary.switch'} @:{'terms.glossary.to'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.vehicle'} @:{'terms.glossary.customer'}.",
|
"help_text": "@.capitalize:{'terms.glossary.this'} @:{'terms.glossary.typed'} @:{'terms.glossary.plate'} @:{'terms.glossary.belongs'} @:{'terms.glossary.to'} @:{'terms.glossary.another'} @:{'terms.glossary.saved'} @:{'terms.glossary.customer'}. @.capitalize:{'terms.glossary.choose'} @:{'terms.glossary.whether'} @:{'terms.glossary.to'} @:{'terms.glossary.keep'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.selected'} @:{'terms.glossary.customer'} @:{'terms.glossary.or'} @:{'terms.glossary.switch'} @:{'terms.glossary.to'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.vehicle'} @:{'terms.glossary.customer'}.",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"benefit_portal": "@.capitalize:{'terms.glossary.customer'} @.capitalize:{'terms.glossary.portal'}",
|
"benefit_portal": "@.capitalize:{'terms.glossary.customer'} @.capitalize:{'terms.glossary.portal'}",
|
||||||
"benefit_portal_desc": "@.capitalize:{'terms.glossary.use'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.customer'} @:{'terms.glossary.portal'} @:{'terms.glossary.to'} @:{'terms.glossary.add'} @:{'terms.glossary.vehicles'}, @:{'terms.glossary.book'} @:{'terms.glossary.washes'}, @:{'terms.glossary.download'} @:{'terms.glossary.invoices'}, @:{'terms.glossary.wash'} @:{'terms.glossary.certificates'} @:{'terms.glossary.and'} @:{'terms.glossary.information'} @:{'terms.glossary.for'} @:{'terms.glossary.historical'} @:{'terms.glossary.washes'}",
|
"benefit_portal_desc": "@.capitalize:{'terms.glossary.use'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.customer'} @:{'terms.glossary.portal'} @:{'terms.glossary.to'} @:{'terms.glossary.add'} @:{'terms.glossary.vehicles'}, @:{'terms.glossary.book'} @:{'terms.glossary.washes'}, @:{'terms.glossary.download'} @:{'terms.glossary.invoices'}, @:{'terms.glossary.wash'} @:{'terms.glossary.certificates'} @:{'terms.glossary.and'} @:{'terms.glossary.information'} @:{'terms.glossary.for'} @:{'terms.glossary.historical'} @:{'terms.glossary.washes'}",
|
||||||
"benefit_wash_desc": "@.capitalize:{'terms.glossary.use'} @:{'terms.glossary.our'} @:{'terms.glossary.national'} @:{'terms.glossary.network'} @:{'terms.glossary.of'} @:{'terms.glossary.wash'} @:{'terms.glossary.halls'} @:{'terms.glossary.at'} @:{'terms.glossary.competitive'} @:{'terms.glossary.prices'}",
|
"benefit_wash_desc": "@.capitalize:{'terms.glossary.use'} @:{'terms.glossary.our'} @:{'terms.glossary.national'} @:{'terms.glossary.network'} @:{'terms.glossary.of'} @:{'terms.glossary.wash'} @:{'terms.glossary.halls'} @:{'terms.glossary.at'} @:{'terms.glossary.competitive'} @:{'terms.glossary.prices'}",
|
||||||
|
"ean_label": "@:{'phrases.compat.admin.pos.ean'}",
|
||||||
"email_label": "@.capitalize:{'terms.glossary.company'} @:{'terms.glossary.email'}",
|
"email_label": "@.capitalize:{'terms.glossary.company'} @:{'terms.glossary.email'}",
|
||||||
"intro": "@.capitalize:{'terms.glossary.as'} @:{'terms.glossary.a'} @:{'terms.glossary.customer'} @:{'terms.glossary.at'} @.capitalize:{'terms.glossary.truck'} @.capitalize:{'terms.glossary.wash'} @:{'terms.glossary.you'} @:{'terms.glossary.get'} @:{'terms.glossary.access'} @:{'terms.glossary.to'}:",
|
"intro": "@.capitalize:{'terms.glossary.as'} @:{'terms.glossary.a'} @:{'terms.glossary.customer'} @:{'terms.glossary.at'} @.capitalize:{'terms.glossary.truck'} @.capitalize:{'terms.glossary.wash'} @:{'terms.glossary.you'} @:{'terms.glossary.get'} @:{'terms.glossary.access'} @:{'terms.glossary.to'}:",
|
||||||
"phone_label": "@:{'phrases.compat.admin.pos.company_phone'}",
|
"phone_label": "@:{'phrases.compat.admin.pos.company_phone'}",
|
||||||
|
|||||||
@@ -14,6 +14,31 @@
|
|||||||
"transaction_history": "@.capitalize:{'terms.glossary.transaction'} @:{'terms.glossary.history'}"
|
"transaction_history": "@.capitalize:{'terms.glossary.transaction'} @:{'terms.glossary.history'}"
|
||||||
},
|
},
|
||||||
"other": "@:{'phrases.compat.global.other'}",
|
"other": "@:{'phrases.compat.global.other'}",
|
||||||
|
"overview": {
|
||||||
|
"active_vehicles": "Active vehicles",
|
||||||
|
"attributes": "Attributes",
|
||||||
|
"connected": "Connected",
|
||||||
|
"customer_management": "Customer management",
|
||||||
|
"customer_number_required": "A customer number is required before customer settings can be managed.",
|
||||||
|
"customer_rules": "Customer rules",
|
||||||
|
"effective_access": "Effective access",
|
||||||
|
"loaded": "Loaded",
|
||||||
|
"load_failed": "User data could not be loaded",
|
||||||
|
"loading_user": "Loading user",
|
||||||
|
"management_hub": "Management hub",
|
||||||
|
"missing": "Missing",
|
||||||
|
"more_permissions": "+{count} more permissions",
|
||||||
|
"no_subscription_transactions": "No wash subscription transactions are registered for this customer.",
|
||||||
|
"not_invoiced": "Not invoiced",
|
||||||
|
"open_draft": "Open draft",
|
||||||
|
"open_vehicles": "Open vehicles",
|
||||||
|
"price_overrides": "Price overrides",
|
||||||
|
"pricing": "Pricing",
|
||||||
|
"subscription_invoicing": "Subscription invoicing",
|
||||||
|
"subtitle": "Overview of customer data, access, pricing, vehicles, and subscription invoicing.",
|
||||||
|
"vehicles_load_failed": "Vehicles could not be loaded.",
|
||||||
|
"workspace_shortcuts": "Workspace shortcuts"
|
||||||
|
},
|
||||||
"permissions": "@.capitalize:{'terms.glossary.permissions'}",
|
"permissions": "@.capitalize:{'terms.glossary.permissions'}",
|
||||||
"user_data": "@.capitalize:{'terms.glossary.user'} @:{'terms.glossary.data'}",
|
"user_data": "@.capitalize:{'terms.glossary.user'} @:{'terms.glossary.data'}",
|
||||||
"user_id": "@.capitalize:{'terms.glossary.user'} @.upper:{'terms.glossary.id'}",
|
"user_id": "@.capitalize:{'terms.glossary.user'} @.upper:{'terms.glossary.id'}",
|
||||||
|
|||||||
@@ -76,7 +76,13 @@
|
|||||||
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
|
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
|
||||||
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
|
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
|
||||||
"edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}",
|
"edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}",
|
||||||
"deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}"
|
"deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}",
|
||||||
|
"login_link": "@:{'phrases.compat.limited_backoffice.employees.login_link'}",
|
||||||
|
"login_link_title": "@:{'phrases.compat.limited_backoffice.employees.login_link_title'}",
|
||||||
|
"login_link_qr_alt": "@:{'phrases.compat.limited_backoffice.employees.login_link_qr_alt'}",
|
||||||
|
"login_link_url": "@:{'phrases.compat.limited_backoffice.employees.login_link_url'}",
|
||||||
|
"copy_login_link": "@:{'phrases.compat.limited_backoffice.employees.copy_login_link'}",
|
||||||
|
"login_link_copied": "@:{'phrases.compat.limited_backoffice.employees.login_link_copied'}"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -106,7 +112,8 @@
|
|||||||
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
|
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
|
||||||
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
|
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
|
||||||
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
|
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
|
||||||
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}"
|
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}",
|
||||||
|
"generate_employee_login_link": "@:{'phrases.compat.limited_backoffice.errors.generate_employee_login_link'}"
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
|
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
|
||||||
@@ -321,7 +328,13 @@
|
|||||||
"active": "Active",
|
"active": "Active",
|
||||||
"inactive": "@.capitalize:{'terms.glossary.inactive'}",
|
"inactive": "@.capitalize:{'terms.glossary.inactive'}",
|
||||||
"edit": "@.capitalize:{'terms.glossary.edit'}",
|
"edit": "@.capitalize:{'terms.glossary.edit'}",
|
||||||
"deactivate": "@.capitalize:{'terms.glossary.deactivate'}"
|
"deactivate": "@.capitalize:{'terms.glossary.deactivate'}",
|
||||||
|
"login_link": "QR login",
|
||||||
|
"login_link_title": "Employee login link",
|
||||||
|
"login_link_qr_alt": "QR code login link",
|
||||||
|
"login_link_url": "Login link",
|
||||||
|
"copy_login_link": "Copy link",
|
||||||
|
"login_link_copied": "Link copied."
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -351,7 +364,8 @@
|
|||||||
"save_prices": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.save'} @:{'terms.glossary.department'} @:{'terms.glossary.prices'}.",
|
"save_prices": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.save'} @:{'terms.glossary.department'} @:{'terms.glossary.prices'}.",
|
||||||
"load_employees": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.load'} @:{'terms.glossary.employees'}.",
|
"load_employees": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.load'} @:{'terms.glossary.employees'}.",
|
||||||
"save_employee": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.save'} @:{'terms.glossary.employee'}.",
|
"save_employee": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.save'} @:{'terms.glossary.employee'}.",
|
||||||
"deactivate_employee": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.deactivate'} @:{'terms.glossary.employee'}."
|
"deactivate_employee": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.deactivate'} @:{'terms.glossary.employee'}.",
|
||||||
|
"generate_employee_login_link": "Could not create employee login link."
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "Role permissions",
|
"title": "Role permissions",
|
||||||
|
|||||||
@@ -34,6 +34,14 @@
|
|||||||
"subtitle_suffix": "@:{'phrases.compat.admin.daily_report.product_sales.subtitle_suffix'}",
|
"subtitle_suffix": "@:{'phrases.compat.admin.daily_report.product_sales.subtitle_suffix'}",
|
||||||
"title": "@:{'phrases.compat.admin.daily_report.product_sales.title'}"
|
"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": {
|
"reports": {
|
||||||
"avg_revenue": {
|
"avg_revenue": {
|
||||||
"label": "@:{'phrases.compat.department_reports.avg_revenue_per_wash'}",
|
"label": "@:{'phrases.compat.department_reports.avg_revenue_per_wash'}",
|
||||||
@@ -293,6 +301,7 @@
|
|||||||
"delete": "@:{'phrases.compat.global.delete'}",
|
"delete": "@:{'phrases.compat.global.delete'}",
|
||||||
"details": "@:{'phrases.compat.admin.pos.details'}",
|
"details": "@:{'phrases.compat.admin.pos.details'}",
|
||||||
"download": "@:{'phrases.compat.guest.pwa.title'}",
|
"download": "@:{'phrases.compat.guest.pwa.title'}",
|
||||||
|
"ean": "@:{'phrases.compat.admin.pos.ean'}",
|
||||||
"drafts_assignment": {
|
"drafts_assignment": {
|
||||||
"button": "@:{'phrases.compat.admin.pos.drafts_assignment.button'}",
|
"button": "@:{'phrases.compat.admin.pos.drafts_assignment.button'}",
|
||||||
"create_invoice_collection": "@:{'phrases.compat.admin.pos.drafts_assignment.create_invoice_collection'}",
|
"create_invoice_collection": "@:{'phrases.compat.admin.pos.drafts_assignment.create_invoice_collection'}",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"benefit_wash": "@:{'phrases.compat.tables.bookings.wash'}",
|
"benefit_wash": "@:{'phrases.compat.tables.bookings.wash'}",
|
||||||
"benefit_wash_desc": "@:{'phrases.compat.tables.bookings.wash_desc'}",
|
"benefit_wash_desc": "@:{'phrases.compat.tables.bookings.wash_desc'}",
|
||||||
"cvr_label": "@:{'phrases.compat.customers.cvr'}",
|
"cvr_label": "@:{'phrases.compat.customers.cvr'}",
|
||||||
|
"ean_label": "@:{'phrases.compat.customer_creation.customer.ean_label'}",
|
||||||
"email_label": "@:{'phrases.compat.customer_creation.customer.email_label'}",
|
"email_label": "@:{'phrases.compat.customer_creation.customer.email_label'}",
|
||||||
"intro": "@:{'phrases.compat.customer_creation.customer.intro'}",
|
"intro": "@:{'phrases.compat.customer_creation.customer.intro'}",
|
||||||
"phone_label": "@:{'phrases.compat.customer_creation.customer.phone_label'}",
|
"phone_label": "@:{'phrases.compat.customer_creation.customer.phone_label'}",
|
||||||
|
|||||||
@@ -44,8 +44,8 @@
|
|||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
"modules": "Modules",
|
"modules": "Modules",
|
||||||
"branding": "Profile & Branding",
|
"branding": "Profile & Branding",
|
||||||
"gateways": "Gateways",
|
"gateways": "@.capitalize:{'terms.glossary.gateways'}",
|
||||||
"stripe": "Stripe",
|
"stripe": "@:{'terms.glossary.stripe'}",
|
||||||
"pricing": "Pricing",
|
"pricing": "Pricing",
|
||||||
"categories": "Categories"
|
"categories": "Categories"
|
||||||
},
|
},
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"department_id": "Department ID",
|
"department_id": "Department ID",
|
||||||
"economic_department_id": "Economic department",
|
"economic_department_id": "Economic department",
|
||||||
"branding": "Branding",
|
"branding": "Branding",
|
||||||
"created_at": "Created",
|
"created_at": "@:common.created",
|
||||||
"updated_at": "Updated"
|
"updated_at": "Updated"
|
||||||
},
|
},
|
||||||
"hardware": {
|
"hardware": {
|
||||||
@@ -104,12 +104,12 @@
|
|||||||
"quick_links": {
|
"quick_links": {
|
||||||
"title": "Department tools",
|
"title": "Department tools",
|
||||||
"subtitle": "Open the focused setup areas for this department",
|
"subtitle": "Open the focused setup areas for this department",
|
||||||
"modules": "Modules",
|
"modules": "@:superuser_dashboard.department_navigation.modules",
|
||||||
"branding": "Branding",
|
"branding": "@:superuser_dashboard.department_overview.profile.branding",
|
||||||
"gateways": "Gateways",
|
"gateways": "@:superuser_dashboard.department_navigation.gateways",
|
||||||
"stripe": "Stripe",
|
"stripe": "@:superuser_dashboard.department_navigation.stripe",
|
||||||
"pricing": "Pricing",
|
"pricing": "@:superuser_dashboard.department_navigation.pricing",
|
||||||
"categories": "Categories"
|
"categories": "@:superuser_dashboard.department_navigation.categories"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"invalid_department": "A valid department is required",
|
"invalid_department": "A valid department is required",
|
||||||
|
|||||||
@@ -22,6 +22,31 @@
|
|||||||
"transaction_history": "@:{'phrases.compat.user_admin.orders.transaction_history'}"
|
"transaction_history": "@:{'phrases.compat.user_admin.orders.transaction_history'}"
|
||||||
},
|
},
|
||||||
"other": "@:{'phrases.compat.user_admin.other'}",
|
"other": "@:{'phrases.compat.user_admin.other'}",
|
||||||
|
"overview": {
|
||||||
|
"active_vehicles": "@:{'phrases.compat.user_admin.overview.active_vehicles'}",
|
||||||
|
"attributes": "@:{'phrases.compat.user_admin.overview.attributes'}",
|
||||||
|
"connected": "@:{'phrases.compat.user_admin.overview.connected'}",
|
||||||
|
"customer_management": "@:{'phrases.compat.user_admin.overview.customer_management'}",
|
||||||
|
"customer_number_required": "@:{'phrases.compat.user_admin.overview.customer_number_required'}",
|
||||||
|
"customer_rules": "@:{'phrases.compat.user_admin.overview.customer_rules'}",
|
||||||
|
"effective_access": "@:{'phrases.compat.user_admin.overview.effective_access'}",
|
||||||
|
"loaded": "@:{'phrases.compat.user_admin.overview.loaded'}",
|
||||||
|
"load_failed": "@:{'phrases.compat.user_admin.overview.load_failed'}",
|
||||||
|
"loading_user": "@:{'phrases.compat.user_admin.overview.loading_user'}",
|
||||||
|
"management_hub": "@:{'phrases.compat.user_admin.overview.management_hub'}",
|
||||||
|
"missing": "@:{'phrases.compat.user_admin.overview.missing'}",
|
||||||
|
"more_permissions": "@:{'phrases.compat.user_admin.overview.more_permissions'}",
|
||||||
|
"no_subscription_transactions": "@:{'phrases.compat.user_admin.overview.no_subscription_transactions'}",
|
||||||
|
"not_invoiced": "@:{'phrases.compat.user_admin.overview.not_invoiced'}",
|
||||||
|
"open_draft": "@:{'phrases.compat.user_admin.overview.open_draft'}",
|
||||||
|
"open_vehicles": "@:{'phrases.compat.user_admin.overview.open_vehicles'}",
|
||||||
|
"price_overrides": "@:{'phrases.compat.user_admin.overview.price_overrides'}",
|
||||||
|
"pricing": "@:{'phrases.compat.user_admin.overview.pricing'}",
|
||||||
|
"subscription_invoicing": "@:{'phrases.compat.user_admin.overview.subscription_invoicing'}",
|
||||||
|
"subtitle": "@:{'phrases.compat.user_admin.overview.subtitle'}",
|
||||||
|
"vehicles_load_failed": "@:{'phrases.compat.user_admin.overview.vehicles_load_failed'}",
|
||||||
|
"workspace_shortcuts": "@:{'phrases.compat.user_admin.overview.workspace_shortcuts'}"
|
||||||
|
},
|
||||||
"permissions": "@:{'phrases.compat.user_admin.permissions'}",
|
"permissions": "@:{'phrases.compat.user_admin.permissions'}",
|
||||||
"subtitle": "@:user_admin.user_data",
|
"subtitle": "@:user_admin.user_data",
|
||||||
"title": "@:{'phrases.compat.common.user'}",
|
"title": "@:{'phrases.compat.common.user'}",
|
||||||
|
|||||||
@@ -27,6 +27,14 @@
|
|||||||
"subtitle_suffix": "@:{'terms.glossary.produktsalg'}",
|
"subtitle_suffix": "@:{'terms.glossary.produktsalg'}",
|
||||||
"title": "@.capitalize:{'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": {
|
"reports": {
|
||||||
"bookings": {
|
"bookings": {
|
||||||
"completed_count": "@.capitalize:{'terms.glossary.fullført'} @:{'terms.glossary.telling'}",
|
"completed_count": "@.capitalize:{'terms.glossary.fullført'} @:{'terms.glossary.telling'}",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"company_phone": "Firmatelefon",
|
"company_phone": "Firmatelefon",
|
||||||
"contact_email": "@.capitalize:{'terms.glossary.kontakt'} @:{'terms.glossary.e'}-@:{'terms.glossary.post'}",
|
"contact_email": "@.capitalize:{'terms.glossary.kontakt'} @:{'terms.glossary.e'}-@:{'terms.glossary.post'}",
|
||||||
"contact_phone": "@.capitalize:{'terms.glossary.kontakt'} @:{'terms.glossary.telefon'}",
|
"contact_phone": "@.capitalize:{'terms.glossary.kontakt'} @:{'terms.glossary.telefon'}",
|
||||||
|
"ean": "EAN",
|
||||||
"created_by": "Laget @:{'terms.glossary.av'}",
|
"created_by": "Laget @:{'terms.glossary.av'}",
|
||||||
"customer_conflict": {
|
"customer_conflict": {
|
||||||
"help_text": "@.capitalize:{'terms.glossary.det'} @:{'terms.glossary.innskrevne'} @:{'terms.glossary.skiltet'} @:{'terms.glossary.tilhoerer'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.annen'} @:{'terms.glossary.lagret'} @:{'terms.glossary.kunde'}. @.capitalize:{'terms.glossary.velg'} @:{'terms.glossary.om'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.valgte'} @:{'terms.glossary.kunden'} @:{'terms.glossary.skal'} @:{'terms.glossary.beholdes'} @:{'terms.glossary.eller'} @:{'terms.glossary.om'} @:{'terms.glossary.det'} @:{'terms.glossary.skal'} @:{'terms.glossary.byttes'} @:{'terms.glossary.til'} @:{'terms.glossary.kjoretoykunden'}.",
|
"help_text": "@.capitalize:{'terms.glossary.det'} @:{'terms.glossary.innskrevne'} @:{'terms.glossary.skiltet'} @:{'terms.glossary.tilhoerer'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.annen'} @:{'terms.glossary.lagret'} @:{'terms.glossary.kunde'}. @.capitalize:{'terms.glossary.velg'} @:{'terms.glossary.om'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.valgte'} @:{'terms.glossary.kunden'} @:{'terms.glossary.skal'} @:{'terms.glossary.beholdes'} @:{'terms.glossary.eller'} @:{'terms.glossary.om'} @:{'terms.glossary.det'} @:{'terms.glossary.skal'} @:{'terms.glossary.byttes'} @:{'terms.glossary.til'} @:{'terms.glossary.kjoretoykunden'}.",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"benefit_portal": "@:{'terms.glossary.kundeportal'}",
|
"benefit_portal": "@:{'terms.glossary.kundeportal'}",
|
||||||
"benefit_portal_desc": "@.capitalize:{'terms.glossary.bruk'} kundeportalen @:{'terms.glossary.til'} @:{'terms.glossary.a'} @:{'terms.glossary.legge'} @:{'terms.glossary.til'} @:{'terms.glossary.kjøretøy'}, bokvaske, @:{'terms.glossary.laste'} @:{'terms.glossary.ned'} @:{'terms.glossary.fakturaer'}, @:{'terms.glossary.vaskesertifikater'} @:{'terms.glossary.og'} @:{'terms.glossary.informasjon'} @:{'terms.glossary.for'} @:{'terms.glossary.historiske'} @:{'terms.glossary.vask'}.",
|
"benefit_portal_desc": "@.capitalize:{'terms.glossary.bruk'} kundeportalen @:{'terms.glossary.til'} @:{'terms.glossary.a'} @:{'terms.glossary.legge'} @:{'terms.glossary.til'} @:{'terms.glossary.kjøretøy'}, bokvaske, @:{'terms.glossary.laste'} @:{'terms.glossary.ned'} @:{'terms.glossary.fakturaer'}, @:{'terms.glossary.vaskesertifikater'} @:{'terms.glossary.og'} @:{'terms.glossary.informasjon'} @:{'terms.glossary.for'} @:{'terms.glossary.historiske'} @:{'terms.glossary.vask'}.",
|
||||||
"benefit_wash_desc": "@.capitalize:{'terms.glossary.bruk'} @:{'terms.glossary.vart'} @:{'terms.glossary.nasjonale'} @:{'terms.glossary.nettverk'} @:{'terms.glossary.av'} @:{'terms.glossary.vaskehaller'} @:{'terms.glossary.til'} @:{'terms.glossary.konkurransedyktige'} @:{'terms.glossary.priser'}",
|
"benefit_wash_desc": "@.capitalize:{'terms.glossary.bruk'} @:{'terms.glossary.vart'} @:{'terms.glossary.nasjonale'} @:{'terms.glossary.nettverk'} @:{'terms.glossary.av'} @:{'terms.glossary.vaskehaller'} @:{'terms.glossary.til'} @:{'terms.glossary.konkurransedyktige'} @:{'terms.glossary.priser'}",
|
||||||
|
"ean_label": "@:{'phrases.compat.admin.pos.ean'}",
|
||||||
"email_label": "Firmaets @:{'terms.glossary.e'}-@:{'terms.glossary.post'}",
|
"email_label": "Firmaets @:{'terms.glossary.e'}-@:{'terms.glossary.post'}",
|
||||||
"intro": "@.capitalize:{'terms.glossary.som'} @:{'terms.glossary.kunde'} @:{'terms.glossary.hos'} @:{'terms.glossary.truck'} @.capitalize:{'terms.glossary.wash'} @:{'terms.glossary.far'} @:{'terms.glossary.du'} @:{'terms.glossary.tilgang'} @:{'terms.glossary.til'}:",
|
"intro": "@.capitalize:{'terms.glossary.som'} @:{'terms.glossary.kunde'} @:{'terms.glossary.hos'} @:{'terms.glossary.truck'} @.capitalize:{'terms.glossary.wash'} @:{'terms.glossary.far'} @:{'terms.glossary.du'} @:{'terms.glossary.tilgang'} @:{'terms.glossary.til'}:",
|
||||||
"phone_label": "@:{'phrases.compat.admin.pos.company_phone'}",
|
"phone_label": "@:{'phrases.compat.admin.pos.company_phone'}",
|
||||||
|
|||||||
@@ -21,6 +21,13 @@
|
|||||||
"opening_hours": {
|
"opening_hours": {
|
||||||
"closed": "Stengt"
|
"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'}",
|
"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'}",
|
"select_department": "@.capitalize:{'terms.glossary.velg'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.avdeling'}",
|
||||||
"subtitle": "@.capitalize:{'terms.glossary.administrer'} @:{'terms.glossary.avdelinger'}",
|
"subtitle": "@.capitalize:{'terms.glossary.administrer'} @:{'terms.glossary.avdelinger'}",
|
||||||
|
|||||||
@@ -140,6 +140,7 @@
|
|||||||
"departments": {
|
"departments": {
|
||||||
"columns": {
|
"columns": {
|
||||||
"archived": "Arkivert",
|
"archived": "Arkivert",
|
||||||
|
"custom_pricing_only": "@:{'phrases.compat.departments.pricing.custom_pricing_only'}",
|
||||||
"dimension": "Dimensjon",
|
"dimension": "Dimensjon",
|
||||||
"economic_department": "@.upper:{'terms.glossary.e'}-@:{'terms.glossary.økonomisk'} @:{'terms.glossary.avdeling'}",
|
"economic_department": "@.upper:{'terms.glossary.e'}-@:{'terms.glossary.økonomisk'} @:{'terms.glossary.avdeling'}",
|
||||||
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
|
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
|
||||||
|
|||||||
@@ -14,6 +14,31 @@
|
|||||||
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
|
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
|
||||||
},
|
},
|
||||||
"other": "@:{'phrases.compat.global.other'}",
|
"other": "@:{'phrases.compat.global.other'}",
|
||||||
|
"overview": {
|
||||||
|
"active_vehicles": "Aktive kjøretøy",
|
||||||
|
"attributes": "Attributter",
|
||||||
|
"connected": "Tilkoblet",
|
||||||
|
"customer_management": "Kundestyring",
|
||||||
|
"customer_number_required": "Et kundenummer kreves før kundeinnstillinger kan administreres.",
|
||||||
|
"customer_rules": "Kunderegler",
|
||||||
|
"effective_access": "Effektiv tilgang",
|
||||||
|
"loaded": "Lastet",
|
||||||
|
"load_failed": "Brukerdata kunne ikke lastes",
|
||||||
|
"loading_user": "Laster bruker",
|
||||||
|
"management_hub": "Administrasjon",
|
||||||
|
"missing": "Mangler",
|
||||||
|
"more_permissions": "+{count} flere tillatelser",
|
||||||
|
"no_subscription_transactions": "Ingen vaskeabonnementstransaksjoner er registrert for kunden.",
|
||||||
|
"not_invoiced": "Ikke fakturert",
|
||||||
|
"open_draft": "Åpent utkast",
|
||||||
|
"open_vehicles": "Åpne kjøretøy",
|
||||||
|
"price_overrides": "Prisoverstyringer",
|
||||||
|
"pricing": "Priser",
|
||||||
|
"subscription_invoicing": "Abonnementsfakturering",
|
||||||
|
"subtitle": "Oversikt over kundedata, tilgang, priser, kjøretøy og abonnementsfakturering.",
|
||||||
|
"vehicles_load_failed": "Kjøretøy kunne ikke lastes.",
|
||||||
|
"workspace_shortcuts": "Arbeidsområde"
|
||||||
|
},
|
||||||
"permissions": "@.capitalize:{'terms.glossary.tillatelser'}",
|
"permissions": "@.capitalize:{'terms.glossary.tillatelser'}",
|
||||||
"user_data": "@.capitalize:{'terms.glossary.brukerdata'}",
|
"user_data": "@.capitalize:{'terms.glossary.brukerdata'}",
|
||||||
"user_id": "@.capitalize:{'terms.glossary.bruker'}-@.upper:{'terms.glossary.id'}",
|
"user_id": "@.capitalize:{'terms.glossary.bruker'}-@.upper:{'terms.glossary.id'}",
|
||||||
|
|||||||
@@ -2,6 +2,20 @@
|
|||||||
"compat": {
|
"compat": {
|
||||||
"vehicles": {
|
"vehicles": {
|
||||||
"add": "@:{'terms.glossary.lagg'} @:{'terms.glossary.till'} @:{'terms.glossary.fordon'}",
|
"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",
|
"brand": "Märke",
|
||||||
"color": "Färg",
|
"color": "Färg",
|
||||||
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
|
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
|
||||||
|
|||||||
@@ -76,7 +76,13 @@
|
|||||||
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
|
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
|
||||||
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
|
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
|
||||||
"edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}",
|
"edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}",
|
||||||
"deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}"
|
"deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}",
|
||||||
|
"login_link": "@:{'phrases.compat.limited_backoffice.employees.login_link'}",
|
||||||
|
"login_link_title": "@:{'phrases.compat.limited_backoffice.employees.login_link_title'}",
|
||||||
|
"login_link_qr_alt": "@:{'phrases.compat.limited_backoffice.employees.login_link_qr_alt'}",
|
||||||
|
"login_link_url": "@:{'phrases.compat.limited_backoffice.employees.login_link_url'}",
|
||||||
|
"copy_login_link": "@:{'phrases.compat.limited_backoffice.employees.copy_login_link'}",
|
||||||
|
"login_link_copied": "@:{'phrases.compat.limited_backoffice.employees.login_link_copied'}"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -106,7 +112,8 @@
|
|||||||
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
|
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
|
||||||
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
|
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
|
||||||
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
|
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
|
||||||
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}"
|
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}",
|
||||||
|
"generate_employee_login_link": "@:{'phrases.compat.limited_backoffice.errors.generate_employee_login_link'}"
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
|
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
|
||||||
@@ -321,7 +328,13 @@
|
|||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
"inactive": "Inaktiv",
|
"inactive": "Inaktiv",
|
||||||
"edit": "@:{'terms.glossary.rediger'}",
|
"edit": "@:{'terms.glossary.rediger'}",
|
||||||
"deactivate": "Deaktiver"
|
"deactivate": "Deaktiver",
|
||||||
|
"login_link": "QR-innlogging",
|
||||||
|
"login_link_title": "Innloggingslenke for medarbeider",
|
||||||
|
"login_link_qr_alt": "QR-kode for innloggingslenke",
|
||||||
|
"login_link_url": "Innloggingslenke",
|
||||||
|
"copy_login_link": "Kopier lenke",
|
||||||
|
"login_link_copied": "Lenken er kopiert."
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -351,7 +364,8 @@
|
|||||||
"save_prices": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.lagre'} @:{'terms.glossary.avdelingspriser'}.",
|
"save_prices": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.lagre'} @:{'terms.glossary.avdelingspriser'}.",
|
||||||
"load_employees": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.laste'} @:{'terms.glossary.medarbeidere'}.",
|
"load_employees": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.laste'} @:{'terms.glossary.medarbeidere'}.",
|
||||||
"save_employee": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.lagre'} @:{'terms.glossary.medarbeideren'}.",
|
"save_employee": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.lagre'} @:{'terms.glossary.medarbeideren'}.",
|
||||||
"deactivate_employee": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} deaktivere @:{'terms.glossary.medarbeideren'}."
|
"deactivate_employee": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} deaktivere @:{'terms.glossary.medarbeideren'}.",
|
||||||
|
"generate_employee_login_link": "Kunne ikke opprette innloggingslenke for medarbeideren."
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "Rolletillatelser",
|
"title": "Rolletillatelser",
|
||||||
|
|||||||
@@ -27,6 +27,14 @@
|
|||||||
"subtitle_suffix": "@:{'terms.glossary.produktforsaljning'}",
|
"subtitle_suffix": "@:{'terms.glossary.produktforsaljning'}",
|
||||||
"title": "@.capitalize:{'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": {
|
"reports": {
|
||||||
"bookings": {
|
"bookings": {
|
||||||
"completed_count": "@.capitalize:{'terms.glossary.antal'} @:{'terms.glossary.slutforda'}",
|
"completed_count": "@.capitalize:{'terms.glossary.antal'} @:{'terms.glossary.slutforda'}",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"company_phone": "Färetagstelefon",
|
"company_phone": "Färetagstelefon",
|
||||||
"contact_email": "@.capitalize:{'terms.glossary.kontakt'}-@:{'terms.glossary.e'}-@:{'terms.glossary.post'}",
|
"contact_email": "@.capitalize:{'terms.glossary.kontakt'}-@:{'terms.glossary.e'}-@:{'terms.glossary.post'}",
|
||||||
"contact_phone": "Kontakttelefon",
|
"contact_phone": "Kontakttelefon",
|
||||||
|
"ean": "EAN",
|
||||||
"created_by": "@.capitalize:{'terms.glossary.skapad'} @:{'terms.glossary.av'}",
|
"created_by": "@.capitalize:{'terms.glossary.skapad'} @:{'terms.glossary.av'}",
|
||||||
"customer_conflict": {
|
"customer_conflict": {
|
||||||
"help_text": "@.capitalize:{'terms.glossary.det'} @:{'terms.glossary.angivna'} @:{'terms.glossary.registreringsnumret'} @:{'terms.glossary.tillhor'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.annan'} @:{'terms.glossary.sparad'} @:{'terms.glossary.kund'}. @:{'terms.glossary.valj_2'} @:{'terms.glossary.om'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.valda'} @:{'terms.glossary.kunden'} @:{'terms.glossary.ska'} @:{'terms.glossary.behallas'} @:{'terms.glossary.eller'} @:{'terms.glossary.om'} @:{'terms.glossary.bytet'} @:{'terms.glossary.ska'} @:{'terms.glossary.goras'} @:{'terms.glossary.till'} @:{'terms.glossary.fordonskunden'}.",
|
"help_text": "@.capitalize:{'terms.glossary.det'} @:{'terms.glossary.angivna'} @:{'terms.glossary.registreringsnumret'} @:{'terms.glossary.tillhor'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.annan'} @:{'terms.glossary.sparad'} @:{'terms.glossary.kund'}. @:{'terms.glossary.valj_2'} @:{'terms.glossary.om'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.valda'} @:{'terms.glossary.kunden'} @:{'terms.glossary.ska'} @:{'terms.glossary.behallas'} @:{'terms.glossary.eller'} @:{'terms.glossary.om'} @:{'terms.glossary.bytet'} @:{'terms.glossary.ska'} @:{'terms.glossary.goras'} @:{'terms.glossary.till'} @:{'terms.glossary.fordonskunden'}.",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"benefit_portal": "@:{'terms.glossary.kundportal'}",
|
"benefit_portal": "@:{'terms.glossary.kundportal'}",
|
||||||
"benefit_portal_desc": "@.capitalize:{'terms.glossary.anvand'} kundportalen @:{'terms.glossary.for'} @:{'terms.glossary.att'} @:{'terms.glossary.lagga'} @:{'terms.glossary.till'} @:{'terms.glossary.fordon'}, @:{'terms.glossary.boka'} @:{'terms.glossary.tvattar'}, @:{'terms.glossary.ladda'} @:{'terms.glossary.ner'} @:{'terms.glossary.fakturor'}, @:{'terms.glossary.tvattcertifikat'} @:{'terms.glossary.och'} @:{'terms.glossary.information'} @:{'terms.glossary.om'} @:{'terms.glossary.historiska'} @:{'terms.glossary.tvattar'}",
|
"benefit_portal_desc": "@.capitalize:{'terms.glossary.anvand'} kundportalen @:{'terms.glossary.for'} @:{'terms.glossary.att'} @:{'terms.glossary.lagga'} @:{'terms.glossary.till'} @:{'terms.glossary.fordon'}, @:{'terms.glossary.boka'} @:{'terms.glossary.tvattar'}, @:{'terms.glossary.ladda'} @:{'terms.glossary.ner'} @:{'terms.glossary.fakturor'}, @:{'terms.glossary.tvattcertifikat'} @:{'terms.glossary.och'} @:{'terms.glossary.information'} @:{'terms.glossary.om'} @:{'terms.glossary.historiska'} @:{'terms.glossary.tvattar'}",
|
||||||
"benefit_wash_desc": "@.capitalize:{'terms.glossary.anvand'} @:{'terms.glossary.vart'} @:{'terms.glossary.nationella'} @:{'terms.glossary.natverk'} @:{'terms.glossary.av'} @:{'terms.glossary.tvatthallar'} @:{'terms.glossary.till'} @:{'terms.glossary.konkurrenskraftiga'} @:{'terms.glossary.priser'}",
|
"benefit_wash_desc": "@.capitalize:{'terms.glossary.anvand'} @:{'terms.glossary.vart'} @:{'terms.glossary.nationella'} @:{'terms.glossary.natverk'} @:{'terms.glossary.av'} @:{'terms.glossary.tvatthallar'} @:{'terms.glossary.till'} @:{'terms.glossary.konkurrenskraftiga'} @:{'terms.glossary.priser'}",
|
||||||
|
"ean_label": "@:{'phrases.compat.admin.pos.ean'}",
|
||||||
"email_label": "Företagets @:{'terms.glossary.e'}-@:{'terms.glossary.post'}",
|
"email_label": "Företagets @:{'terms.glossary.e'}-@:{'terms.glossary.post'}",
|
||||||
"intro": "@.capitalize:{'terms.glossary.som'} @:{'terms.glossary.kund'} @:{'terms.glossary.hos'} @:{'terms.glossary.truck'} @.capitalize:{'terms.glossary.wash'} @:{'terms.glossary.far'} @:{'terms.glossary.du'} @:{'terms.glossary.tillgang'} @:{'terms.glossary.till'}:",
|
"intro": "@.capitalize:{'terms.glossary.som'} @:{'terms.glossary.kund'} @:{'terms.glossary.hos'} @:{'terms.glossary.truck'} @.capitalize:{'terms.glossary.wash'} @:{'terms.glossary.far'} @:{'terms.glossary.du'} @:{'terms.glossary.tillgang'} @:{'terms.glossary.till'}:",
|
||||||
"phone_label": "@:{'phrases.compat.admin.pos.company_phone'}",
|
"phone_label": "@:{'phrases.compat.admin.pos.company_phone'}",
|
||||||
|
|||||||
@@ -21,6 +21,13 @@
|
|||||||
"opening_hours": {
|
"opening_hours": {
|
||||||
"closed": "Stengt"
|
"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'}",
|
"search_placeholder": "@:{'terms.glossary.sok'} @:{'terms.glossary.efter'} @:{'terms.glossary.avdelningsnamn'}",
|
||||||
"select_department": "@:{'terms.glossary.velg'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.avdeling'}",
|
"select_department": "@:{'terms.glossary.velg'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.avdeling'}",
|
||||||
"subtitle": "@:{'terms.glossary.administrer'} @:{'terms.glossary.avdelinger'}",
|
"subtitle": "@:{'terms.glossary.administrer'} @:{'terms.glossary.avdelinger'}",
|
||||||
|
|||||||
@@ -140,6 +140,7 @@
|
|||||||
"departments": {
|
"departments": {
|
||||||
"columns": {
|
"columns": {
|
||||||
"archived": "Arkiverad",
|
"archived": "Arkiverad",
|
||||||
|
"custom_pricing_only": "@:{'phrases.compat.departments.pricing.custom_pricing_only'}",
|
||||||
"dimension": "Dimension",
|
"dimension": "Dimension",
|
||||||
"economic_department": "@:{'phrases.compat.objects.economic_departments.single'}",
|
"economic_department": "@:{'phrases.compat.objects.economic_departments.single'}",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
|
|||||||
@@ -14,6 +14,31 @@
|
|||||||
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
|
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
|
||||||
},
|
},
|
||||||
"other": "Andet",
|
"other": "Andet",
|
||||||
|
"overview": {
|
||||||
|
"active_vehicles": "Aktiva fordon",
|
||||||
|
"attributes": "Attribut",
|
||||||
|
"connected": "Ansluten",
|
||||||
|
"customer_management": "Kundhantering",
|
||||||
|
"customer_number_required": "Ett kundnummer krävs innan kundinställningar kan hanteras.",
|
||||||
|
"customer_rules": "Kundregler",
|
||||||
|
"effective_access": "Effektiv åtkomst",
|
||||||
|
"loaded": "Inläst",
|
||||||
|
"load_failed": "Användardata kunde inte läsas in",
|
||||||
|
"loading_user": "Läser in användare",
|
||||||
|
"management_hub": "Administration",
|
||||||
|
"missing": "Saknas",
|
||||||
|
"more_permissions": "+{count} fler behörigheter",
|
||||||
|
"no_subscription_transactions": "Inga tvättabonnemangstransaktioner är registrerade för kunden.",
|
||||||
|
"not_invoiced": "Ej fakturerat",
|
||||||
|
"open_draft": "Öppet utkast",
|
||||||
|
"open_vehicles": "Öppna fordon",
|
||||||
|
"price_overrides": "Prisöverskrivningar",
|
||||||
|
"pricing": "Priser",
|
||||||
|
"subscription_invoicing": "Abonnemangsfakturering",
|
||||||
|
"subtitle": "Översikt över kunddata, åtkomst, priser, fordon och abonnemangsfakturering.",
|
||||||
|
"vehicles_load_failed": "Fordon kunde inte läsas in.",
|
||||||
|
"workspace_shortcuts": "Arbetsyta"
|
||||||
|
},
|
||||||
"permissions": "Tilladelser",
|
"permissions": "Tilladelser",
|
||||||
"user_data": "Brugerdata",
|
"user_data": "Brugerdata",
|
||||||
"user_id": "Användar-@.upper:{'terms.glossary.id'}",
|
"user_id": "Användar-@.upper:{'terms.glossary.id'}",
|
||||||
|
|||||||
@@ -2,6 +2,20 @@
|
|||||||
"compat": {
|
"compat": {
|
||||||
"vehicles": {
|
"vehicles": {
|
||||||
"add": "@:{'terms.glossary.lagg'} @:{'terms.glossary.till'} @:{'terms.glossary.fordon'}",
|
"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",
|
"brand": "Märke",
|
||||||
"color": "Färg",
|
"color": "Färg",
|
||||||
"delete_vehicle": "@.capitalize:{'terms.glossary.ta'} @:{'terms.glossary.bort'} @:{'terms.glossary.fordon'}",
|
"delete_vehicle": "@.capitalize:{'terms.glossary.ta'} @:{'terms.glossary.bort'} @:{'terms.glossary.fordon'}",
|
||||||
|
|||||||
@@ -76,7 +76,13 @@
|
|||||||
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
|
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
|
||||||
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
|
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
|
||||||
"edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}",
|
"edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}",
|
||||||
"deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}"
|
"deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}",
|
||||||
|
"login_link": "@:{'phrases.compat.limited_backoffice.employees.login_link'}",
|
||||||
|
"login_link_title": "@:{'phrases.compat.limited_backoffice.employees.login_link_title'}",
|
||||||
|
"login_link_qr_alt": "@:{'phrases.compat.limited_backoffice.employees.login_link_qr_alt'}",
|
||||||
|
"login_link_url": "@:{'phrases.compat.limited_backoffice.employees.login_link_url'}",
|
||||||
|
"copy_login_link": "@:{'phrases.compat.limited_backoffice.employees.copy_login_link'}",
|
||||||
|
"login_link_copied": "@:{'phrases.compat.limited_backoffice.employees.login_link_copied'}"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -106,7 +112,8 @@
|
|||||||
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
|
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
|
||||||
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
|
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
|
||||||
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
|
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
|
||||||
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}"
|
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}",
|
||||||
|
"generate_employee_login_link": "@:{'phrases.compat.limited_backoffice.errors.generate_employee_login_link'}"
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
|
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
|
||||||
@@ -321,7 +328,13 @@
|
|||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
"inactive": "Inaktiv",
|
"inactive": "Inaktiv",
|
||||||
"edit": "@:{'terms.glossary.redigera'}",
|
"edit": "@:{'terms.glossary.redigera'}",
|
||||||
"deactivate": "@.capitalize:{'terms.glossary.inaktivera'}"
|
"deactivate": "@.capitalize:{'terms.glossary.inaktivera'}",
|
||||||
|
"login_link": "QR-inloggning",
|
||||||
|
"login_link_title": "Inloggningslänk för medarbetare",
|
||||||
|
"login_link_qr_alt": "QR-kod för inloggningslänk",
|
||||||
|
"login_link_url": "Inloggningslänk",
|
||||||
|
"copy_login_link": "Kopiera länk",
|
||||||
|
"login_link_copied": "Länken har kopierats."
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"viewer": {
|
"viewer": {
|
||||||
@@ -351,7 +364,8 @@
|
|||||||
"save_prices": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.spara'} @:{'terms.glossary.avdelningspriser'}.",
|
"save_prices": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.spara'} @:{'terms.glossary.avdelningspriser'}.",
|
||||||
"load_employees": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.ladda'} @:{'terms.glossary.medarbetare'}.",
|
"load_employees": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.ladda'} @:{'terms.glossary.medarbetare'}.",
|
||||||
"save_employee": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.spara'} @:{'terms.glossary.medarbetaren'}.",
|
"save_employee": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.spara'} @:{'terms.glossary.medarbetaren'}.",
|
||||||
"deactivate_employee": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.inaktivera'} @:{'terms.glossary.medarbetaren'}."
|
"deactivate_employee": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.inaktivera'} @:{'terms.glossary.medarbetaren'}.",
|
||||||
|
"generate_employee_login_link": "Kunde inte skapa inloggningslänk för medarbetaren."
|
||||||
},
|
},
|
||||||
"role_permissions": {
|
"role_permissions": {
|
||||||
"title": "Rollbehörigheter",
|
"title": "Rollbehörigheter",
|
||||||
|
|||||||
@@ -442,6 +442,12 @@ export const router = createRouter({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'limitedBackofficeEmployees',
|
name: 'limitedBackofficeEmployees',
|
||||||
|
path: '/backoffice/departments/:departmentId/employees',
|
||||||
|
component: LimitedBackofficeEmployees,
|
||||||
|
meta: { middleware: authMiddleware, titleKey: 'templates.limited_backoffice.employees.title' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'limitedBackofficeEmployeesLegacy',
|
||||||
path: '/backoffice/employees',
|
path: '/backoffice/employees',
|
||||||
component: LimitedBackofficeEmployees,
|
component: LimitedBackofficeEmployees,
|
||||||
meta: { middleware: authMiddleware, titleKey: 'templates.limited_backoffice.employees.title' }
|
meta: { middleware: authMiddleware, titleKey: 'templates.limited_backoffice.employees.title' }
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export function normalizeEconomicEan(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value).replace(/\D+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function economicEanPayloadValue(value) {
|
||||||
|
const normalized = normalizeEconomicEan(value);
|
||||||
|
return normalized === "" ? undefined : normalized;
|
||||||
|
}
|
||||||
@@ -24,6 +24,9 @@ export const createLimitedBackofficeEmployee = (payload) =>
|
|||||||
export const updateLimitedBackofficeEmployee = (employeeId, payload) =>
|
export const updateLimitedBackofficeEmployee = (employeeId, payload) =>
|
||||||
limitedBackofficeRequest(`/employees/${encodeURIComponent(String(employeeId))}`, "PUT", payload);
|
limitedBackofficeRequest(`/employees/${encodeURIComponent(String(employeeId))}`, "PUT", payload);
|
||||||
|
|
||||||
|
export const generateLimitedBackofficeEmployeeLoginLink = (employeeId) =>
|
||||||
|
limitedBackofficeRequest(`/employees/${encodeURIComponent(String(employeeId))}/login-link`, "POST");
|
||||||
|
|
||||||
export const deactivateLimitedBackofficeEmployee = (employeeId) =>
|
export const deactivateLimitedBackofficeEmployee = (employeeId) =>
|
||||||
limitedBackofficeRequest(`/employees/${encodeURIComponent(String(employeeId))}`, "DELETE");
|
limitedBackofficeRequest(`/employees/${encodeURIComponent(String(employeeId))}`, "DELETE");
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import LimitedBackofficeLayout from "@/views/backoffice/components/LimitedBackofficeLayout.vue";
|
import LimitedBackofficeLayout from "@/views/backoffice/components/LimitedBackofficeLayout.vue";
|
||||||
import {
|
import {
|
||||||
createLimitedBackofficeEmployee,
|
createLimitedBackofficeEmployee,
|
||||||
deactivateLimitedBackofficeEmployee,
|
deactivateLimitedBackofficeEmployee,
|
||||||
|
generateLimitedBackofficeEmployeeLoginLink,
|
||||||
getLimitedBackofficeDepartments,
|
getLimitedBackofficeDepartments,
|
||||||
getLimitedBackofficeEmployees,
|
getLimitedBackofficeEmployees,
|
||||||
getLimitedBackofficeRoles,
|
getLimitedBackofficeRoles,
|
||||||
@@ -22,8 +24,24 @@ const PHONE_COUNTRY_CODES = [
|
|||||||
{ value: "47", flag: "🇳🇴", labelKey: "norway" },
|
{ value: "47", flag: "🇳🇴", labelKey: "norway" },
|
||||||
{ value: "358", flag: "🇫🇮", labelKey: "finland" },
|
{ value: "358", flag: "🇫🇮", labelKey: "finland" },
|
||||||
];
|
];
|
||||||
|
let qrCodeModulePromise = null;
|
||||||
|
|
||||||
|
const loadQRCodeModule = () => {
|
||||||
|
if (!qrCodeModulePromise) {
|
||||||
|
qrCodeModulePromise = import("qrcode");
|
||||||
|
}
|
||||||
|
return qrCodeModulePromise;
|
||||||
|
};
|
||||||
|
|
||||||
const { t } = useI18n();
|
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 departments = ref([]);
|
||||||
const roles = ref([]);
|
const roles = ref([]);
|
||||||
const employees = ref([]);
|
const employees = ref([]);
|
||||||
@@ -35,6 +53,12 @@ const successMessage = ref("");
|
|||||||
const editingEmployee = ref(null);
|
const editingEmployee = ref(null);
|
||||||
const formAttempted = ref(false);
|
const formAttempted = ref(false);
|
||||||
const showRolePermissions = ref(false);
|
const showRolePermissions = ref(false);
|
||||||
|
const loginLinkModalOpen = ref(false);
|
||||||
|
const loginLinkEmployee = ref(null);
|
||||||
|
const loginLinkUrl = ref("");
|
||||||
|
const loginLinkQrSrc = ref("");
|
||||||
|
const loginLinkGeneratingId = ref(null);
|
||||||
|
const loginLinkCopyMessage = ref("");
|
||||||
const form = ref({
|
const form = ref({
|
||||||
display_name: "",
|
display_name: "",
|
||||||
email: "",
|
email: "",
|
||||||
@@ -45,6 +69,19 @@ const form = ref({
|
|||||||
department_ids: [],
|
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(() => ({
|
const roleMessages = computed(() => ({
|
||||||
viewer: {
|
viewer: {
|
||||||
label: t("templates.limited_backoffice.roles.viewer.label"),
|
label: t("templates.limited_backoffice.roles.viewer.label"),
|
||||||
@@ -241,7 +278,7 @@ const countryCodeLabel = (countryCode) => {
|
|||||||
if (!option) {
|
if (!option) {
|
||||||
return "";
|
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) => {
|
const formatEmployeePhone = (employee) => {
|
||||||
@@ -255,11 +292,25 @@ const sanitizePhone = (event) => {
|
|||||||
form.value.phone = String(event?.target?.value || "").replace(/\D/g, "");
|
form.value.phone = String(event?.target?.value || "").replace(/\D/g, "");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 loadEmployees = async () => {
|
||||||
const response = await getLimitedBackofficeEmployees({ includeInactive: includeInactive.value });
|
const response = await getLimitedBackofficeEmployees({ includeInactive: includeInactive.value });
|
||||||
employees.value = unwrapLimitedBackofficeResponse(response) || [];
|
employees.value = unwrapLimitedBackofficeResponse(response) || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultDepartmentIds = () => (selectedDepartment.value?.id ? [Number(selectedDepartment.value.id)] : []);
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
errorMessage.value = "";
|
errorMessage.value = "";
|
||||||
@@ -274,6 +325,18 @@ const loadData = async () => {
|
|||||||
if (!ROLE_KEYS.includes(form.value.role_key) || !roles.value.some((role) => role.key === form.value.role_key)) {
|
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";
|
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();
|
await loadEmployees();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = limitedBackofficeErrorMessage(
|
errorMessage.value = limitedBackofficeErrorMessage(
|
||||||
@@ -295,7 +358,7 @@ const resetForm = () => {
|
|||||||
phone: "",
|
phone: "",
|
||||||
password: "",
|
password: "",
|
||||||
role_key: roles.value[0]?.key || "viewer",
|
role_key: roles.value[0]?.key || "viewer",
|
||||||
department_ids: [],
|
department_ids: defaultDepartmentIds(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -396,7 +459,82 @@ const deactivateEmployee = async (employee) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showEmployeeLoginLink = async (employee) => {
|
||||||
|
successMessage.value = "";
|
||||||
|
errorMessage.value = "";
|
||||||
|
loginLinkCopyMessage.value = "";
|
||||||
|
loginLinkGeneratingId.value = employee.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await generateLimitedBackofficeEmployeeLoginLink(employee.id);
|
||||||
|
const payload = unwrapLimitedBackofficeResponse(response) || {};
|
||||||
|
const loginPath = String(payload.login_path || "");
|
||||||
|
if (!loginPath) {
|
||||||
|
throw new Error(t("templates.limited_backoffice.errors.generate_employee_login_link"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginUrl = absoluteLoginLink(loginPath);
|
||||||
|
const { toDataURL } = await loadQRCodeModule();
|
||||||
|
|
||||||
|
loginLinkEmployee.value = employee;
|
||||||
|
loginLinkUrl.value = loginUrl;
|
||||||
|
loginLinkQrSrc.value = await toDataURL(loginUrl, { margin: 2, width: 240 });
|
||||||
|
loginLinkModalOpen.value = true;
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = limitedBackofficeErrorMessage(
|
||||||
|
error,
|
||||||
|
t("templates.limited_backoffice.errors.generate_employee_login_link")
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
loginLinkGeneratingId.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyText = async (text) => {
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// Fall back for browser contexts where the Clipboard API is present but denied.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.setAttribute("readonly", "");
|
||||||
|
textarea.style.position = "fixed";
|
||||||
|
textarea.style.opacity = "0";
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
textarea.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyEmployeeLoginLink = async () => {
|
||||||
|
if (!loginLinkUrl.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await copyText(loginLinkUrl.value);
|
||||||
|
loginLinkCopyMessage.value = t("templates.limited_backoffice.employees.login_link_copied");
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEmployeeLoginLink = () => {
|
||||||
|
loginLinkModalOpen.value = false;
|
||||||
|
loginLinkCopyMessage.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeDepartment = async (departmentId) => {
|
||||||
|
await router.push(`/backoffice/departments/${departmentId}/employees`);
|
||||||
|
};
|
||||||
|
|
||||||
const reloadEmployees = async () => {
|
const reloadEmployees = async () => {
|
||||||
|
if (hasForbiddenDepartment.value) {
|
||||||
|
employees.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
errorMessage.value = "";
|
errorMessage.value = "";
|
||||||
try {
|
try {
|
||||||
@@ -414,12 +552,36 @@ const reloadEmployees = async () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadData();
|
void loadData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedDepartmentId.value,
|
||||||
|
async (nextDepartmentId, previousDepartmentId) => {
|
||||||
|
if (nextDepartmentId === previousDepartmentId || loading.value || departments.value.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm();
|
||||||
|
await reloadEmployees();
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RestrictedPageWrapper :hasPermission="SessionUser.hasPermission('limited_backoffice_access')">
|
<RestrictedPageWrapper :hasPermission="SessionUser.hasPermission('limited_backoffice_access')">
|
||||||
<LimitedBackofficeLayout active-tab="employees" :departments="departments">
|
<LimitedBackofficeLayout
|
||||||
<div class="columns is-variable is-5">
|
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">
|
<div class="column is-5">
|
||||||
<form class="box limited-employee-form" data-testid="limited-employee-form" @submit.prevent="saveEmployee">
|
<form class="box limited-employee-form" data-testid="limited-employee-form" @submit.prevent="saveEmployee">
|
||||||
<h2 class="title is-5">
|
<h2 class="title is-5">
|
||||||
@@ -643,14 +805,19 @@ onMounted(() => {
|
|||||||
{{ successMessage }}
|
{{ successMessage }}
|
||||||
</div>
|
</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") }}
|
{{ t("templates.limited_backoffice.employees.empty") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!loading && employees.length > 0" class="table-container">
|
<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">
|
<table class="table is-fullwidth is-hoverable limited-employees-table" data-testid="limited-employees-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>{{ t("templates.limited_backoffice.employees.user_id") }}</th>
|
||||||
<th>{{ t("templates.limited_backoffice.employees.name") }}</th>
|
<th>{{ t("templates.limited_backoffice.employees.name") }}</th>
|
||||||
<th>{{ t("templates.limited_backoffice.employees.role") }}</th>
|
<th>{{ t("templates.limited_backoffice.employees.role") }}</th>
|
||||||
<th>{{ t("templates.limited_backoffice.employees.departments") }}</th>
|
<th>{{ t("templates.limited_backoffice.employees.departments") }}</th>
|
||||||
@@ -659,7 +826,12 @@ onMounted(() => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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 }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ employee.display_name }}</strong>
|
<strong>{{ employee.display_name }}</strong>
|
||||||
<p v-if="employee.email" class="has-text-grey is-size-7">{{ employee.email }}</p>
|
<p v-if="employee.email" class="has-text-grey is-size-7">{{ employee.email }}</p>
|
||||||
@@ -691,6 +863,18 @@ onMounted(() => {
|
|||||||
</td>
|
</td>
|
||||||
<td class="limited-employees-table__actions">
|
<td class="limited-employees-table__actions">
|
||||||
<div class="buttons are-small">
|
<div class="buttons are-small">
|
||||||
|
<button
|
||||||
|
v-if="employee.active"
|
||||||
|
type="button"
|
||||||
|
class="button is-info is-light"
|
||||||
|
:class="{ 'is-loading': loginLinkGeneratingId === employee.id }"
|
||||||
|
:disabled="saving || loginLinkGeneratingId === employee.id"
|
||||||
|
:data-testid="`limited-employee-login-link-${employee.id}`"
|
||||||
|
@click="showEmployeeLoginLink(employee)"
|
||||||
|
>
|
||||||
|
<span class="icon is-small"><i class="fas fa-qrcode" aria-hidden="true"></i></span>
|
||||||
|
<span>{{ t("templates.limited_backoffice.employees.login_link") }}</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="button is-light"
|
class="button is-light"
|
||||||
@@ -807,6 +991,77 @@ onMounted(() => {
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
|
|
||||||
|
<b-modal
|
||||||
|
v-model="loginLinkModalOpen"
|
||||||
|
:width="420"
|
||||||
|
:can-cancel="['escape', 'x', 'outside']"
|
||||||
|
trap-focus
|
||||||
|
aria-role="dialog"
|
||||||
|
aria-modal
|
||||||
|
>
|
||||||
|
<div class="modal-card limited-login-link-modal" data-testid="limited-login-link-modal">
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<div>
|
||||||
|
<p class="modal-card-title">{{ t("templates.limited_backoffice.employees.login_link_title") }}</p>
|
||||||
|
<p v-if="loginLinkEmployee" class="is-size-7 has-text-grey">
|
||||||
|
{{ loginLinkEmployee.display_name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="delete"
|
||||||
|
:aria-label="t('templates.limited_backoffice.role_permissions.close')"
|
||||||
|
data-testid="limited-login-link-close"
|
||||||
|
@click="closeEmployeeLoginLink"
|
||||||
|
></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<div class="limited-login-link-modal__qr">
|
||||||
|
<img
|
||||||
|
v-if="loginLinkQrSrc"
|
||||||
|
:src="loginLinkQrSrc"
|
||||||
|
:alt="t('templates.limited_backoffice.employees.login_link_qr_alt')"
|
||||||
|
data-testid="limited-login-link-qr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="limited-login-link-url">
|
||||||
|
{{ t("templates.limited_backoffice.employees.login_link_url") }}
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
id="limited-login-link-url"
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
:value="loginLinkUrl"
|
||||||
|
data-testid="limited-login-link-url"
|
||||||
|
@focus="$event.target.select()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="loginLinkCopyMessage" class="help is-success" data-testid="limited-login-link-copy-message">
|
||||||
|
{{ loginLinkCopyMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer class="modal-card-foot is-justify-content-flex-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-info"
|
||||||
|
data-testid="limited-login-link-copy"
|
||||||
|
@click="copyEmployeeLoginLink"
|
||||||
|
>
|
||||||
|
<span class="icon is-small"><i class="fas fa-copy" aria-hidden="true"></i></span>
|
||||||
|
<span>{{ t("templates.limited_backoffice.employees.copy_login_link") }}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button is-light" @click="closeEmployeeLoginLink">
|
||||||
|
{{ t("templates.limited_backoffice.role_permissions.close") }}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</b-modal>
|
||||||
</LimitedBackofficeLayout>
|
</LimitedBackofficeLayout>
|
||||||
</RestrictedPageWrapper>
|
</RestrictedPageWrapper>
|
||||||
</template>
|
</template>
|
||||||
@@ -876,7 +1131,24 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.limited-employees-table__actions {
|
.limited-employees-table__actions {
|
||||||
width: 190px;
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limited-login-link-modal {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limited-login-link-modal__qr {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limited-login-link-modal__qr img {
|
||||||
|
width: 240px;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.limited-role-permissions-modal {
|
.limited-role-permissions-modal {
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ const departments = ref([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorMessage = ref("");
|
const errorMessage = ref("");
|
||||||
|
|
||||||
|
const changeDepartment = async (departmentId) => {
|
||||||
|
await router.push(`/backoffice/departments/${departmentId}/prices`);
|
||||||
|
};
|
||||||
|
|
||||||
const loadDepartments = async () => {
|
const loadDepartments = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
errorMessage.value = "";
|
errorMessage.value = "";
|
||||||
@@ -38,10 +42,6 @@ const loadDepartments = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeDepartment = async (departmentId) => {
|
|
||||||
await router.push(`/backoffice/departments/${departmentId}/prices`);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadDepartments();
|
void loadDepartments();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ const pricesRoute = computed(() =>
|
|||||||
: "/backoffice"
|
: "/backoffice"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const employeesRoute = computed(() =>
|
||||||
|
props.selectedDepartmentId
|
||||||
|
? `/backoffice/departments/${encodeURIComponent(String(props.selectedDepartmentId))}/employees`
|
||||||
|
: "/backoffice/employees"
|
||||||
|
);
|
||||||
|
|
||||||
const tabs = computed(() => [
|
const tabs = computed(() => [
|
||||||
{
|
{
|
||||||
key: "prices",
|
key: "prices",
|
||||||
@@ -44,7 +50,7 @@ const tabs = computed(() => [
|
|||||||
{
|
{
|
||||||
key: "employees",
|
key: "employees",
|
||||||
label: t("templates.limited_backoffice.nav.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">
|
<section class="section limited-backoffice" data-testid="limited-backoffice">
|
||||||
<PageTitle
|
<PageTitle
|
||||||
:title="t('templates.limited_backoffice.title')"
|
: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="limited-backoffice__toolbar">
|
||||||
<div class="tabs is-toggle is-small limited-backoffice__tabs" data-testid="limited-backoffice-tabs">
|
<div class="tabs is-toggle is-small limited-backoffice__tabs" data-testid="limited-backoffice-tabs">
|
||||||
@@ -75,30 +110,6 @@ const selectDepartment = (event) => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
@@ -112,10 +123,8 @@ const selectDepartment = (event) => {
|
|||||||
|
|
||||||
.limited-backoffice__toolbar {
|
.limited-backoffice__toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
gap: 1rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,8 +132,12 @@ const selectDepartment = (event) => {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.limited-backoffice__subtitle {
|
||||||
|
margin-top: -0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.limited-backoffice__department-field {
|
.limited-backoffice__department-field {
|
||||||
min-width: min(100%, 260px);
|
min-width: min(42vw, 280px);
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,11 +151,18 @@ const selectDepartment = (event) => {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.limited-backoffice__tabs,
|
.limited-backoffice__tabs {
|
||||||
.limited-backoffice__department-field {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.limited-backoffice__department-field {
|
||||||
|
min-width: 9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limited-backoffice__subtitle {
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.limited-backoffice__tabs :deep(ul) {
|
.limited-backoffice__tabs :deep(ul) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||||
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
|
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
|
||||||
import SubuserGrantSelector from "@/components/session/subuser/SubuserGrantSelector.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 { IS_DEV } from '@/config.js';
|
||||||
import {BLoading} from "buefy";
|
import {BLoading} from "buefy";
|
||||||
|
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subuser grant selection gate
|
* Subuser grant selection gate
|
||||||
@@ -76,6 +77,59 @@ const canShowVehicleShortcut = computed(() => SessionUser.canAccessCustomerFeatu
|
|||||||
const canShowOrderShortcut = computed(() => SessionUser.canAccessCustomerFeature("orders", "list"));
|
const canShowOrderShortcut = computed(() => SessionUser.canAccessCustomerFeature("orders", "list"));
|
||||||
const canShowSelfServeShortcut = computed(() => SessionUser.canAccessCustomerFeature("selfserve", "list"));
|
const canShowSelfServeShortcut = computed(() => SessionUser.canAccessCustomerFeature("selfserve", "list"));
|
||||||
const canShowInvoiceShortcut = computed(() => canShowClassicCustomerShortcut());
|
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 windowInnerWidth = ref(window.innerWidth);
|
||||||
const updateWindowInnerWidth = () => {
|
const updateWindowInnerWidth = () => {
|
||||||
@@ -291,9 +345,26 @@ const isPermissionsLoading = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="card-footer-item">
|
<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') }}
|
{{ $t('user_home.download_invoices') }}
|
||||||
</router-link>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</WhiteBoxCard>
|
</WhiteBoxCard>
|
||||||
|
|||||||
@@ -370,8 +370,12 @@ const isInvoiced = () => {
|
|||||||
return isInvoicedWithEconomic() || isInvoicedWithStripe();
|
return isInvoicedWithEconomic() || isInvoicedWithStripe();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isBookedWithEconomic = () => {
|
const hasEconomicInvoiceDownloadContext = () => {
|
||||||
return economicModule.value.invoice_id !== null;
|
const module = economicModule.value || {};
|
||||||
|
return (
|
||||||
|
Object.prototype.hasOwnProperty.call(module, 'invoice_id') ||
|
||||||
|
Object.prototype.hasOwnProperty.call(module, 'invoice_draft_id')
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isCompleted = () => {
|
const isCompleted = () => {
|
||||||
@@ -1376,7 +1380,11 @@ const isDisplayingReceipt = () => {
|
|||||||
|
|
||||||
<template #rail-actions>
|
<template #rail-actions>
|
||||||
<ButtonsBox class="pos-actions pos-actions--rail">
|
<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
|
<div
|
||||||
v-if="isEconomicExportBlocked"
|
v-if="isEconomicExportBlocked"
|
||||||
class="message is-warning is-light"
|
class="message is-warning is-light"
|
||||||
|
|||||||
+11
@@ -77,6 +77,15 @@ const normalizeMetricState = (metric, fallbackState = "ready", fallbackMessage =
|
|||||||
message: metric?.message || 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 = () => {
|
const resetOverviewState = () => {
|
||||||
count_transactions.value = 0;
|
count_transactions.value = 0;
|
||||||
count_products_sold.value = 0;
|
count_products_sold.value = 0;
|
||||||
@@ -149,6 +158,8 @@ const applyOverviewResponse = (overview) => {
|
|||||||
value: Number(product.value || 0),
|
value: Number(product.value || 0),
|
||||||
out_of: Number(product.out_of || 0),
|
out_of: Number(product.out_of || 0),
|
||||||
state: product.state || "ready",
|
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">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted, computed } from 'vue';
|
import { ref, watch, onMounted, computed, nextTick } from 'vue';
|
||||||
import {BSkeleton} from "buefy";
|
import {BSkeleton} from "buefy";
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
@@ -76,6 +76,18 @@ const props = defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
|
canEditTarget: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
targetPercentage: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
saveTargetFunction: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
styleContent: {
|
styleContent: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {
|
default: () => {
|
||||||
@@ -84,6 +96,12 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const percentageElement = ref<HTMLElement|null>(null);
|
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 = () => {
|
const getPercentage = () => {
|
||||||
if (props.state !== "ready") {
|
if (props.state !== "ready") {
|
||||||
return null;
|
return null;
|
||||||
@@ -93,6 +111,91 @@ const getPercentage = () => {
|
|||||||
}
|
}
|
||||||
return null;
|
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 = () => {
|
const getAveragePercentage = () => {
|
||||||
return null; // TODO: Average percentage
|
return null; // TODO: Average percentage
|
||||||
}
|
}
|
||||||
@@ -166,6 +269,11 @@ watch(() => props.count, (newVal) => {
|
|||||||
autoAdjustedFontSize.value = determineFontSize(newVal);
|
autoAdjustedFontSize.value = determineFontSize(newVal);
|
||||||
determineTitleFontSize(props.title);
|
determineTitleFontSize(props.title);
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
watch(() => [props.targetPercentage, isTargetEditorOpen.value], () => {
|
||||||
|
nextTick(() => {
|
||||||
|
autoAdjustedFontSizeTitle.value = determineTitleFontSize(props.title);
|
||||||
|
});
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
autoAdjustedFontSizeTitle.value = determineTitleFontSize(props.title);
|
autoAdjustedFontSizeTitle.value = determineTitleFontSize(props.title);
|
||||||
@@ -180,12 +288,19 @@ const isUnavailable = computed(() => props.state === "unavailable");
|
|||||||
<template>
|
<template>
|
||||||
<div class="card" @click="clickFunction" :data-testid="dataTestid || null">
|
<div class="card" @click="clickFunction" :data-testid="dataTestid || null">
|
||||||
<div class="card-content" :style="styleContent">
|
<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">
|
<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 class="button is-small is-info" :class="{
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-small is-info"
|
||||||
|
:class="{
|
||||||
'has-background-success has-text-black': getAveragePercentage() !== null && isAverage(),
|
'has-background-success has-text-black': getAveragePercentage() !== null && isAverage(),
|
||||||
'has-background-danger has-text-black': getAveragePercentage() !== null && !isAverage(),
|
'has-background-danger has-text-black': getAveragePercentage() !== null && !isAverage(),
|
||||||
'is-outlined': getAveragePercentage() === null
|
'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">
|
<!--<span class="icon is-small">
|
||||||
<i class="fas fa-sync-alt"></i>
|
<i class="fas fa-sync-alt"></i>
|
||||||
</span>-->
|
</span>-->
|
||||||
@@ -197,6 +312,75 @@ const isUnavailable = computed(() => props.state === "unavailable");
|
|||||||
</template>
|
</template>
|
||||||
</button>
|
</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">
|
<button v-if="getAveragePercentage()" class="button is-small is-info is-outlined">
|
||||||
<!--<span class="icon is-small">
|
<!--<span class="icon is-small">
|
||||||
<i class="fas fa-sync-alt"></i>
|
<i class="fas fa-sync-alt"></i>
|
||||||
@@ -282,5 +466,56 @@ const isUnavailable = computed(() => props.state === "unavailable");
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
||||||
|
|||||||
+66
-1
@@ -1,9 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { Colors } from "@/ThemeConfig.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";
|
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 = {
|
const FALLBACK_PRODUCT_TITLES = {
|
||||||
24: "Spot Free (Lastbil)",
|
24: "Spot Free (Lastbil)",
|
||||||
25: "Fælg flex pr. enhed",
|
25: "Fælg flex pr. enhed",
|
||||||
@@ -51,6 +58,8 @@ const productTile = computed(() => {
|
|||||||
out_of: 0,
|
out_of: 0,
|
||||||
state: "ready",
|
state: "ready",
|
||||||
message: null,
|
message: null,
|
||||||
|
target_percentage: null,
|
||||||
|
target_department_id: null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,6 +79,59 @@ const resolvedTitle = computed(() => {
|
|||||||
|
|
||||||
return FALLBACK_PRODUCT_TITLES[props.product_id] || null;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -83,6 +145,9 @@ const resolvedTitle = computed(() => {
|
|||||||
:isLoading="is_loading"
|
:isLoading="is_loading"
|
||||||
:state="productTile.state || 'ready'"
|
:state="productTile.state || 'ready'"
|
||||||
:unavailable-message="productTile.message || 'Ikke tilgængelig'"
|
:unavailable-message="productTile.message || 'Ikke tilgængelig'"
|
||||||
|
:can-edit-target="canEditProductTarget"
|
||||||
|
:target-percentage="targetPercentage"
|
||||||
|
:save-target-function="saveProductTarget"
|
||||||
:style-content="styleContent"
|
:style-content="styleContent"
|
||||||
:data-testid="dataTestid"
|
:data-testid="dataTestid"
|
||||||
/>
|
/>
|
||||||
|
|||||||
+11
-11
@@ -14,6 +14,16 @@ import NotificationsPhonePagination
|
|||||||
:title="$t('admin.notifications.title')"
|
:title="$t('admin.notifications.title')"
|
||||||
:subtitle="$t('admin.notifications.subtitle')"
|
: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')">
|
<NotFoundFallBackPageWrapper :exists="SessionUser.functions.getDepartmentIdFromUrl() && SessionUser.canAccessDepartment(SessionUser.functions.getDepartmentIdFromUrl())" :error="$t('admin.errors.select_department')">
|
||||||
<element-tabs-box
|
<element-tabs-box
|
||||||
defaultActiveTab="sms"
|
defaultActiveTab="sms"
|
||||||
@@ -24,16 +34,6 @@ import NotificationsPhonePagination
|
|||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<template #sms>
|
<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 />
|
<NotificationsPhonePagination />
|
||||||
</template>
|
</template>
|
||||||
</element-tabs-box>
|
</element-tabs-box>
|
||||||
@@ -44,4 +44,4 @@ import NotificationsPhonePagination
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+2
-1
@@ -23,6 +23,7 @@ import {
|
|||||||
} from "../../imports/InvoicingBillingPeriodImportPaging.js";
|
} from "../../imports/InvoicingBillingPeriodImportPaging.js";
|
||||||
import { buildPossibleDuplicateGroups } from "../../utils/possibleDuplicateGroups.js";
|
import { buildPossibleDuplicateGroups } from "../../utils/possibleDuplicateGroups.js";
|
||||||
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
|
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
|
||||||
|
import { SELFWASH_PERIOD_ALL_LIMIT } from "@/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js";
|
||||||
|
|
||||||
const period_result = ref<any>(null);
|
const period_result = ref<any>(null);
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -416,7 +417,7 @@ const loadSelfWashCounts = async ({ forceRefresh = false } = {}) => {
|
|||||||
'GET',
|
'GET',
|
||||||
{
|
{
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 500,
|
limit: SELFWASH_PERIOD_ALL_LIMIT,
|
||||||
filters,
|
filters,
|
||||||
order: 'StartTime:desc',
|
order: 'StartTime:desc',
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ export const PERIOD_CACHE_TTL_MS = 10 * 60 * 1000;
|
|||||||
export const PERIOD_LIMIT_OPTIONS = [10, 25, 50, 100, 200, 500, "all"];
|
export const PERIOD_LIMIT_OPTIONS = [10, 25, 50, 100, 200, 500, "all"];
|
||||||
|
|
||||||
const CACHE_PREFIX = "invoicing-period-page:v2:";
|
const CACHE_PREFIX = "invoicing-period-page:v2:";
|
||||||
const SELF_WASH_COUNTS_CACHE_PREFIX = "invoicing-period-self-wash-counts:";
|
const SELF_WASH_COUNTS_CACHE_PREFIX = "invoicing-period-self-wash-counts:v2:";
|
||||||
const DEFAULT_LIMIT = 100;
|
const DEFAULT_LIMIT = 100;
|
||||||
const ALL_LIMIT = "all";
|
const ALL_LIMIT = "all";
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useRouter} from "vue-router";
|
import { computed } from "vue";
|
||||||
import { ref } from 'vue';
|
import { useRoute } from "vue-router";
|
||||||
import { showAuthSignOutForm } from '@/components/forms/auth/authSignOutForm.vue';
|
import { useI18n } from "vue-i18n";
|
||||||
import UserNavigation from "@/components/global/UserNavigation.vue";
|
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
const router = useRouter();
|
|
||||||
// Get the user from the route
|
|
||||||
const userId = ref(router.currentRoute.value.params.userId)
|
|
||||||
|
|
||||||
const tabs = [
|
const route = useRoute();
|
||||||
{ name: 'Overview', path: '/superuser/users/' + userId.value },
|
const { t } = useI18n();
|
||||||
{ name: 'Orders', path: '/superuser/users/' + userId.value + '/orders' },
|
|
||||||
{ name: 'Pricing', path: '/superuser/users/' + userId.value + '/pricing' },
|
|
||||||
{ name: 'Other', path: '/superuser/users/' + userId.value + '/other' },
|
|
||||||
{ name: 'Vehicles', path: '/superuser/users/' + userId.value + '/vehicles' },
|
|
||||||
{ name: SessionUser.superUser.modules.xlvask.meta.title, path: '/superuser/users/' + userId.value + '/xlvask' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Get the current path
|
const userId = computed(() => route.params.userId);
|
||||||
const currentPath = ref(router.currentRoute.value.path);
|
const basePath = computed(() => `/superuser/users/${userId.value}`);
|
||||||
|
|
||||||
// Get the index of the active tab
|
const tabs = computed(() => [
|
||||||
const activeTab = tabs.findIndex(tab => tab.path === currentPath.value);
|
{ key: "overview", name: t("nav.overview"), path: basePath.value },
|
||||||
|
{ key: "orders", name: t("superuser.nav.orders"), path: `${basePath.value}/orders` },
|
||||||
// Change the tab
|
{ key: "pricing", name: t("user_admin.overview.pricing"), path: `${basePath.value}/pricing` },
|
||||||
const changeTab = (index) => {
|
{ key: "other", name: t("user_admin.other"), path: `${basePath.value}/other` },
|
||||||
router.push(tabs[index].path);
|
{ key: "vehicles", name: t("common.vehicles"), path: `${basePath.value}/vehicles` },
|
||||||
};
|
{ key: "xlvask", name: SessionUser.superUser.modules.xlvask.meta.title, path: `${basePath.value}/xlvask` },
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<nav class="user-detail-tabs" data-testid="superuser-user-tabs">
|
||||||
<div class="tabs is-right">
|
<div class="tabs is-right is-boxed">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(tab, index) in tabs" :key="index" :class="{'is-active': activeTab === index}" @click="changeTab(index)">
|
<li v-for="tab in tabs" :key="tab.path" :class="{ 'is-active': route.path === tab.path }">
|
||||||
<a>{{ tab.name }}</a>
|
<router-link :to="tab.path" :data-testid="`superuser-user-tab-${tab.key}`">
|
||||||
|
{{ tab.name }}
|
||||||
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
</style>
|
.user-detail-tabs {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs ul {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,15 +1,91 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { ref, watch } from "vue";
|
||||||
import {ref, watch} from "vue";
|
|
||||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||||
// Get the user from the route
|
|
||||||
export const userId = ref(0);
|
export const userId = ref(0);
|
||||||
|
export const isUserLoading = ref(false);
|
||||||
|
export const userLoadError = ref(null);
|
||||||
|
export const selectedUserLoaded = ref(false);
|
||||||
|
|
||||||
|
let activeLoadPromise = null;
|
||||||
|
let activeLoadUserId = null;
|
||||||
|
let loadRequestSequence = 0;
|
||||||
|
|
||||||
|
const normalizeUserId = (id) => {
|
||||||
|
const parsed = Number.parseInt(String(id ?? ""), 10);
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultKeys = () => ({
|
||||||
|
open_invoice_draft: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const economicFields = {
|
||||||
|
customerNumber: null,
|
||||||
|
name: null,
|
||||||
|
address: null,
|
||||||
|
zip: null,
|
||||||
|
city: null,
|
||||||
|
mobilePhone: null,
|
||||||
|
email: null,
|
||||||
|
cvr: null,
|
||||||
|
currency: null,
|
||||||
|
country: null,
|
||||||
|
barred: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const setEconomicData = (economicCustomer = {}) => {
|
||||||
|
user.economicData.customerNumber.value = economicCustomer?.customerNumber ?? null;
|
||||||
|
user.economicData.name.value = economicCustomer?.name ?? null;
|
||||||
|
user.economicData.address.value = economicCustomer?.address ?? null;
|
||||||
|
user.economicData.zip.value = economicCustomer?.zip ?? null;
|
||||||
|
user.economicData.city.value = economicCustomer?.city ?? null;
|
||||||
|
user.economicData.mobilePhone.value = economicCustomer?.mobilePhone ?? null;
|
||||||
|
user.economicData.email.value = economicCustomer?.email ?? null;
|
||||||
|
user.economicData.cvr.value = economicCustomer?.corporateIdentificationNumber ?? economicCustomer?.cvr ?? null;
|
||||||
|
user.economicData.currency.value = economicCustomer?.currency ?? null;
|
||||||
|
user.economicData.country.value = economicCustomer?.country ?? null;
|
||||||
|
user.economicData.barred.value = economicCustomer?.barred ?? false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetUser = ({ preserveUserId = false } = {}) => {
|
||||||
|
if (!preserveUserId) {
|
||||||
|
userId.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.id.value = "";
|
||||||
|
user.customer_number.value = "";
|
||||||
|
user.display_name.value = "";
|
||||||
|
user.email.value = "";
|
||||||
|
user.phone.value = null;
|
||||||
|
user.group_id.value = "";
|
||||||
|
user.created_at.value = "";
|
||||||
|
user.updated_at.value = "";
|
||||||
|
setEconomicData(economicFields);
|
||||||
|
user.permissions.value = [];
|
||||||
|
user.attributes.value = [];
|
||||||
|
user.discounts.value = [];
|
||||||
|
user.orders_not_invoiced.value = [];
|
||||||
|
user.keys.value = defaultKeys();
|
||||||
|
user.wash_subscription_transactions.value = [];
|
||||||
|
selectedUserLoaded.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
export const setUser = (id) => {
|
export const setUser = (id) => {
|
||||||
userId.value = id;
|
const normalizedId = normalizeUserId(id);
|
||||||
// Load the user data
|
if (!normalizedId) {
|
||||||
getUserData();
|
userLoadError.value = new Error("Invalid user id");
|
||||||
}
|
resetUser();
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId.value !== normalizedId) {
|
||||||
|
resetUser();
|
||||||
|
userId.value = normalizedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getUserData(normalizedId);
|
||||||
|
};
|
||||||
|
|
||||||
export const onUserChange = (callback) => {
|
export const onUserChange = (callback) => {
|
||||||
watch(userId, (newValue) => {
|
watch(userId, (newValue) => {
|
||||||
@@ -17,48 +93,89 @@ export const onUserChange = (callback) => {
|
|||||||
callback(newValue);
|
callback(newValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getUserCustomerNumber = () => {
|
export const getUserCustomerNumber = () => {
|
||||||
return user.customer_number.value;
|
return user.customer_number.value;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const applyUserData = (data = {}) => {
|
||||||
|
user.id.value = data.id ?? "";
|
||||||
|
user.customer_number.value = data.customer_number ?? "";
|
||||||
|
user.display_name.value = data.display_name ?? "";
|
||||||
|
user.email.value = data.email ?? "";
|
||||||
|
user.phone.value = data.phone ?? null;
|
||||||
|
user.group_id.value = data.group_id ?? "";
|
||||||
|
user.created_at.value = data.created_at ?? "";
|
||||||
|
user.updated_at.value = data.updated_at ?? "";
|
||||||
|
setEconomicData(data.economic_customer ?? {});
|
||||||
|
user.permissions.value = Array.isArray(data.permissions) ? data.permissions : [];
|
||||||
|
user.attributes.value = Array.isArray(data.attributes) ? data.attributes : [];
|
||||||
|
user.discounts.value = Array.isArray(data.discounts) ? data.discounts : [];
|
||||||
|
user.orders_not_invoiced.value = Array.isArray(data.orders_not_invoiced) ? data.orders_not_invoiced : [];
|
||||||
|
user.keys.value = data.keys && typeof data.keys === "object" ? { ...defaultKeys(), ...data.keys } : defaultKeys();
|
||||||
|
user.wash_subscription_transactions.value = Array.isArray(data.wash_subscription_transactions)
|
||||||
|
? data.wash_subscription_transactions
|
||||||
|
: [];
|
||||||
|
selectedUserLoaded.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
// Get the user data
|
// Get the user data
|
||||||
export const getUserData = async () => {
|
export const getUserData = async (id = userId.value) => {
|
||||||
return await authenticatedRequest(`/superuser/user?user_id=${userId.value}`, "GET")
|
const normalizedId = normalizeUserId(id);
|
||||||
|
if (!normalizedId) {
|
||||||
|
userLoadError.value = new Error("Invalid user id");
|
||||||
|
resetUser();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeLoadPromise && activeLoadUserId === normalizedId) {
|
||||||
|
return activeLoadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = ++loadRequestSequence;
|
||||||
|
activeLoadUserId = normalizedId;
|
||||||
|
isUserLoading.value = true;
|
||||||
|
userLoadError.value = null;
|
||||||
|
|
||||||
|
activeLoadPromise = authenticatedRequest(`/superuser/user?user_id=${normalizedId}`, "GET")
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
user.id.value = response.data.data.id;
|
if (requestId !== loadRequestSequence) {
|
||||||
user.customer_number.value = response.data.data.customer_number;
|
return user;
|
||||||
user.group_id.value = response.data.data.group_id;
|
|
||||||
user.created_at.value = response.data.data.created_at;
|
|
||||||
user.updated_at.value = response.data.data.updated_at;
|
|
||||||
if (response.data.data.economic_customer.name) {
|
|
||||||
user.economicData.customerNumber.value = response.data.data.economic_customer.customerNumber;
|
|
||||||
user.economicData.name.value = response.data.data.economic_customer.name;
|
|
||||||
user.economicData.address.value = response.data.data.economic_customer.address;
|
|
||||||
user.economicData.zip.value = response.data.data.economic_customer.zip;
|
|
||||||
user.economicData.city.value = response.data.data.economic_customer.city;
|
|
||||||
user.economicData.mobilePhone.value = response.data.data.economic_customer.mobilePhone;
|
|
||||||
user.economicData.email.value = response.data.data.economic_customer.email;
|
|
||||||
user.economicData.cvr.value = response.data.data.economic_customer.corporateIdentificationNumber;
|
|
||||||
user.economicData.currency.value = response.data.data.economic_customer.currency;
|
|
||||||
user.economicData.country.value = response.data.data.economic_customer.country;
|
|
||||||
user.economicData.barred.value = response.data.data.economic_customer.barred ?? false;
|
|
||||||
}
|
}
|
||||||
user.permissions.value = response.data.data.permissions;
|
|
||||||
user.attributes.value = response.data.data.attributes;
|
applyUserData(response?.data?.data ?? {});
|
||||||
user.discounts.value = response.data.data.discounts;
|
return user;
|
||||||
user.orders_not_invoiced.value = response.data.data.orders_not_invoiced || null;
|
|
||||||
user.keys.value = response.data.data.keys;
|
|
||||||
user.wash_subscription_transactions.value = response.data.data.wash_subscription_transactions || null;
|
|
||||||
console.log(user.discounts.value);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
if (requestId === loadRequestSequence) {
|
||||||
|
resetUser({ preserveUserId: true });
|
||||||
|
userLoadError.value = error;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (requestId === loadRequestSequence) {
|
||||||
|
isUserLoading.value = false;
|
||||||
|
activeLoadPromise = null;
|
||||||
|
activeLoadUserId = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return activeLoadPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDirectProductDiscount = (discount, productId) => {
|
||||||
|
return discount?.product_or_category_id == productId && Number(discount?.is_category) === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseFixedPriceValue = (value) => {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number.parseInt(String(value), 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Get user product discount */
|
/** Get user product discount */
|
||||||
@@ -133,12 +250,25 @@ export const getUserProductOnlyDiscount = (product) => {
|
|||||||
// Find the discount for the product
|
// Find the discount for the product
|
||||||
let discount = null;
|
let discount = null;
|
||||||
try {
|
try {
|
||||||
discount = user.discounts.value.find(discount => discount.product_or_category_id == productId && !discount.is_category) || null;
|
discount = user.discounts.value.find(discount => isDirectProductDiscount(discount, productId)) || null;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
// If the discount is not found, return 0
|
// If the discount is not found, return 0
|
||||||
return discount ? discount.percentage : 0;
|
return discount ? discount.percentage : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUserProductFixedPrice = (product) => {
|
||||||
|
const productId = product.id;
|
||||||
|
let fixedPriceDiscount = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fixedPriceDiscount = user.discounts.value.find((discount) => (
|
||||||
|
isDirectProductDiscount(discount, productId) && parseFixedPriceValue(discount.fixed_price) !== null
|
||||||
|
)) || null;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
return fixedPriceDiscount ? parseFixedPriceValue(fixedPriceDiscount.fixed_price) : null;
|
||||||
|
};
|
||||||
|
|
||||||
export const isProductAllowedToApplyCategoryDiscounts = (product) => {
|
export const isProductAllowedToApplyCategoryDiscounts = (product) => {
|
||||||
// Check if the product is allowed to apply category discounts
|
// Check if the product is allowed to apply category discounts
|
||||||
return parseInt(product.apply_category_discount) === 1;
|
return parseInt(product.apply_category_discount) === 1;
|
||||||
@@ -166,9 +296,27 @@ export const getProductBestApplicableDiscount = (product) => {
|
|||||||
return Math.max(productDiscount, categoryDiscount, globalDiscount);
|
return Math.max(productDiscount, categoryDiscount, globalDiscount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getProductEffectivePrice = (product) => {
|
||||||
|
const fixedPrice = getUserProductFixedPrice(product);
|
||||||
|
if (fixedPrice !== null) {
|
||||||
|
return fixedPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePrice = Number(product.price ?? 0);
|
||||||
|
const discount = Number(getProductBestApplicableDiscount(product) || 0);
|
||||||
|
if (!Number.isFinite(basePrice)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number((basePrice - (basePrice * (discount / 100))).toFixed(0));
|
||||||
|
}
|
||||||
|
|
||||||
export const user = {
|
export const user = {
|
||||||
id: ref(''),
|
id: ref(''),
|
||||||
customer_number: ref(''),
|
customer_number: ref(''),
|
||||||
|
display_name: ref(''),
|
||||||
|
email: ref(''),
|
||||||
|
phone: ref(null),
|
||||||
group_id: ref(''),
|
group_id: ref(''),
|
||||||
created_at: ref(''),
|
created_at: ref(''),
|
||||||
updated_at: ref(''),
|
updated_at: ref(''),
|
||||||
@@ -185,23 +333,23 @@ export const user = {
|
|||||||
country: ref(null),
|
country: ref(null),
|
||||||
barred: ref(null),
|
barred: ref(null),
|
||||||
},
|
},
|
||||||
permissions: ref(null),
|
permissions: ref([]),
|
||||||
attributes: ref(null),
|
attributes: ref([]),
|
||||||
discounts: ref(null),
|
discounts: ref([]),
|
||||||
orders_not_invoiced: ref(null),
|
orders_not_invoiced: ref([]),
|
||||||
keys: ref({
|
keys: ref(defaultKeys()),
|
||||||
open_invoice_draft: ref(null),
|
wash_subscription_transactions: ref([]),
|
||||||
}),
|
|
||||||
wash_subscription_transactions: ref(null),
|
|
||||||
functions: {
|
functions: {
|
||||||
getUserData: getUserData,
|
getUserData: getUserData,
|
||||||
getUserProductDiscount: getUserProductDiscount,
|
getUserProductDiscount: getUserProductDiscount,
|
||||||
getUserCategoryDiscount: getUserCategoryDiscount,
|
getUserCategoryDiscount: getUserCategoryDiscount,
|
||||||
getUserGlobalDiscount: getUserGlobalDiscount,
|
getUserGlobalDiscount: getUserGlobalDiscount,
|
||||||
getUserProductOnlyDiscount: getUserProductOnlyDiscount,
|
getUserProductOnlyDiscount: getUserProductOnlyDiscount,
|
||||||
|
getUserProductFixedPrice: getUserProductFixedPrice,
|
||||||
isProductAllowedToApplyCategoryDiscounts: isProductAllowedToApplyCategoryDiscounts,
|
isProductAllowedToApplyCategoryDiscounts: isProductAllowedToApplyCategoryDiscounts,
|
||||||
getProductCategory: getProductCategory,
|
getProductCategory: getProductCategory,
|
||||||
getProductBestApplicableDiscount: getProductBestApplicableDiscount,
|
getProductBestApplicableDiscount: getProductBestApplicableDiscount,
|
||||||
|
getProductEffectivePrice: getProductEffectivePrice,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,133 +1,506 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
|
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||||
import PageTitle from "@/components/global/PageTitle.vue";
|
import PageTitle from "@/components/global/PageTitle.vue";
|
||||||
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import UserSubPageWrapper from "@/views/dashboards/superUserDashboard/user/UserSubPageWrapper.vue";
|
import UserSubPageWrapper from "@/views/dashboards/superUserDashboard/user/UserSubPageWrapper.vue";
|
||||||
// Get the user from the route
|
import {
|
||||||
const router = useRouter()
|
isUserLoading,
|
||||||
import {user, getUserData, userId, setUser} from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
selectedUserLoaded,
|
||||||
import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
|
setUser,
|
||||||
import UserVehicleSubscriptionsDisplay
|
user,
|
||||||
from "@/views/dashboards/superUserDashboard/user/displays/vehicles/UserVehicleSubscriptionsDisplay.vue";
|
userId,
|
||||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
userLoadError,
|
||||||
import UserFixedPricing from "@/views/dashboards/superUserDashboard/user/displays/UserFixedPricing.vue";
|
} from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
||||||
import UserDefaultDepartment from "@/views/dashboards/superUserDashboard/user/displays/UserDefaultDepartment.vue";
|
import UserDefaultDepartment from "@/views/dashboards/superUserDashboard/user/displays/UserDefaultDepartment.vue";
|
||||||
|
import UserFixedPricing from "@/views/dashboards/superUserDashboard/user/displays/UserFixedPricing.vue";
|
||||||
|
import UserOtherSpecialArrangement from "@/views/dashboards/superUserDashboard/user/displays/other/UserOtherSpecialArrangement.vue";
|
||||||
|
import UserOtherVaskeabonnement from "@/views/dashboards/superUserDashboard/user/displays/other/UserOtherVaskeabonnement.vue";
|
||||||
|
import UserVehicleSubscriptionsDisplay from "@/views/dashboards/superUserDashboard/user/displays/vehicles/UserVehicleSubscriptionsDisplay.vue";
|
||||||
|
|
||||||
// Set the user
|
const route = useRoute();
|
||||||
setUser(router.currentRoute.value.params.userId);
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const vehicles = ref([]);
|
||||||
|
const vehiclesLoading = ref(false);
|
||||||
|
const vehiclesError = ref(null);
|
||||||
|
|
||||||
|
const customerNumber = computed(() => Number.parseInt(String(user.customer_number.value || 0), 10) || 0);
|
||||||
|
const basePath = computed(() => `/superuser/users/${userId.value || route.params.userId}`);
|
||||||
|
const permissions = computed(() => (Array.isArray(user.permissions.value) ? user.permissions.value : []));
|
||||||
|
const attributes = computed(() => (Array.isArray(user.attributes.value) ? user.attributes.value : []));
|
||||||
|
const discounts = computed(() => (Array.isArray(user.discounts.value) ? user.discounts.value : []));
|
||||||
|
const ordersNotInvoiced = computed(() =>
|
||||||
|
Array.isArray(user.orders_not_invoiced.value) ? user.orders_not_invoiced.value : []
|
||||||
|
);
|
||||||
|
const washSubscriptionTransactions = computed(() =>
|
||||||
|
Array.isArray(user.wash_subscription_transactions.value) ? user.wash_subscription_transactions.value : []
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayName = computed(() => {
|
||||||
|
return (
|
||||||
|
user.economicData.name.value ||
|
||||||
|
user.display_name.value ||
|
||||||
|
(userId.value ? `${t("user_admin.user_id")} ${userId.value}` : t("user_admin.overview.loading_user"))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const hasCustomerNumber = computed(() => customerNumber.value > 0);
|
||||||
|
const hasEconomicData = computed(() => Boolean(user.economicData.name.value || user.economicData.customerNumber.value));
|
||||||
|
const isEconomicBarred = computed(() => [true, 1, "1", "true"].includes(user.economicData.barred.value));
|
||||||
|
const activeVehicleSubscriptions = computed(() => vehicles.value.filter((vehicle) => vehicle?.wash_subscription).length);
|
||||||
|
const selfServiceVehicles = computed(() => vehicles.value.filter((vehicle) => vehicle?.xlvask).length);
|
||||||
|
const visibleVehicles = computed(() => vehicles.value.slice(0, 8));
|
||||||
|
const visiblePermissions = computed(() => permissions.value.slice(0, 12));
|
||||||
|
const hiddenPermissionCount = computed(() => Math.max(permissions.value.length - visiblePermissions.value.length, 0));
|
||||||
|
|
||||||
|
const userDetails = computed(() => [
|
||||||
|
{ label: t("user_admin.user_id"), value: userId.value },
|
||||||
|
{ label: t("user_admin.customer_number"), value: customerNumber.value || null },
|
||||||
|
{ label: t("user_admin.group_id"), value: user.group_id.value },
|
||||||
|
{ label: t("common.email"), value: user.email?.value || user.economicData.email.value },
|
||||||
|
{ label: t("common.created"), value: user.created_at.value },
|
||||||
|
{ label: t("user_admin.updated_at"), value: user.updated_at.value },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const economicDetails = computed(() => [
|
||||||
|
{ label: t("user_admin.customer_number"), value: user.economicData.customerNumber.value },
|
||||||
|
{ label: t("common.name"), value: user.economicData.name.value },
|
||||||
|
{ label: t("common.address"), value: user.economicData.address.value },
|
||||||
|
{ label: t("user_admin.zip"), value: user.economicData.zip.value },
|
||||||
|
{ label: t("user_admin.city"), value: user.economicData.city.value },
|
||||||
|
{ label: t("user_admin.mobile_phone"), value: user.economicData.mobilePhone.value },
|
||||||
|
{ label: t("common.email"), value: user.economicData.email.value },
|
||||||
|
{ label: t("user_admin.cvr"), value: user.economicData.cvr.value },
|
||||||
|
{ label: t("user_admin.currency"), value: user.economicData.currency.value },
|
||||||
|
{ label: t("common.country"), value: user.economicData.country.value },
|
||||||
|
{ label: t("user_admin.barred"), value: isEconomicBarred.value ? t("common.yes") : t("common.no") },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const overviewMetrics = computed(() => [
|
||||||
|
{
|
||||||
|
key: "customer",
|
||||||
|
icon: "fas fa-id-card",
|
||||||
|
label: t("user_admin.customer_number"),
|
||||||
|
value: customerNumber.value || t("global.no_data"),
|
||||||
|
status: hasCustomerNumber.value ? t("common.active") : t("user_admin.overview.missing"),
|
||||||
|
tone: hasCustomerNumber.value ? "is-success" : "is-warning",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "economic",
|
||||||
|
icon: "fas fa-building",
|
||||||
|
label: t("user_admin.economic_data"),
|
||||||
|
value: hasEconomicData.value ? t("user_admin.overview.connected") : t("global.no_data"),
|
||||||
|
status: isEconomicBarred.value ? t("user_admin.barred") : t("common.active"),
|
||||||
|
tone: isEconomicBarred.value ? "is-danger" : "is-success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "vehicles",
|
||||||
|
icon: "fas fa-car",
|
||||||
|
label: t("common.vehicles"),
|
||||||
|
value: vehicles.value.length,
|
||||||
|
status: vehiclesLoading.value ? t("common.loading") : t("user_admin.overview.loaded"),
|
||||||
|
tone: vehiclesError.value ? "is-warning" : "is-info",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "subscriptions",
|
||||||
|
icon: "fas fa-car-side",
|
||||||
|
label: t("user_admin.wash_subscriptions"),
|
||||||
|
value: activeVehicleSubscriptions.value,
|
||||||
|
status: t("user_admin.overview.active_vehicles"),
|
||||||
|
tone: activeVehicleSubscriptions.value > 0 ? "is-success" : "is-light",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "orders",
|
||||||
|
icon: "fas fa-file-invoice-dollar",
|
||||||
|
label: t("user_admin.overview.not_invoiced"),
|
||||||
|
value: ordersNotInvoiced.value.length,
|
||||||
|
status: user.keys.value?.open_invoice_draft ? t("user_admin.overview.open_draft") : t("global.no_data"),
|
||||||
|
tone: ordersNotInvoiced.value.length > 0 ? "is-warning" : "is-success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "discounts",
|
||||||
|
icon: "fas fa-tags",
|
||||||
|
label: t("common.discount"),
|
||||||
|
value: discounts.value.length,
|
||||||
|
status: t("user_admin.overview.price_overrides"),
|
||||||
|
tone: discounts.value.length > 0 ? "is-link" : "is-light",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "attributes",
|
||||||
|
icon: "fas fa-sliders-h",
|
||||||
|
label: t("user_admin.overview.attributes"),
|
||||||
|
value: attributes.value.length,
|
||||||
|
status: t("user_admin.overview.customer_rules"),
|
||||||
|
tone: attributes.value.length > 0 ? "is-info" : "is-light",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "permissions",
|
||||||
|
icon: "fas fa-shield-alt",
|
||||||
|
label: t("user_admin.permissions"),
|
||||||
|
value: permissions.value.length,
|
||||||
|
status: t("user_admin.overview.effective_access"),
|
||||||
|
tone: permissions.value.length > 0 ? "is-info" : "is-warning",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const hubLinks = computed(() => [
|
||||||
|
{ key: "orders", icon: "fas fa-file-alt", label: t("superuser.nav.orders"), to: `${basePath.value}/orders` },
|
||||||
|
{ key: "pricing", icon: "fas fa-tags", label: t("user_admin.overview.pricing"), to: `${basePath.value}/pricing` },
|
||||||
|
{ key: "other", icon: "fas fa-sliders-h", label: t("user_admin.other"), to: `${basePath.value}/other` },
|
||||||
|
{ key: "vehicles", icon: "fas fa-car", label: t("common.vehicles"), to: `${basePath.value}/vehicles` },
|
||||||
|
{
|
||||||
|
key: "xlvask",
|
||||||
|
icon: "fas fa-water",
|
||||||
|
label: SessionUser.superUser.modules.xlvask.meta.title,
|
||||||
|
to: `${basePath.value}/xlvask`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const valueOrEmpty = (value) => {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return t("global.no_data");
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadVehicles = async () => {
|
||||||
|
if (!hasCustomerNumber.value) {
|
||||||
|
vehicles.value = [];
|
||||||
|
vehiclesError.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vehiclesLoading.value = true;
|
||||||
|
vehiclesError.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await SessionUser.objects.vehicles.get.user(customerNumber.value);
|
||||||
|
vehicles.value = Array.isArray(response) ? response : [];
|
||||||
|
} catch (error) {
|
||||||
|
vehicles.value = [];
|
||||||
|
vehiclesError.value = error;
|
||||||
|
} finally {
|
||||||
|
vehiclesLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadOverview = async () => {
|
||||||
|
await setUser(route.params.userId);
|
||||||
|
await loadVehicles();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
customerNumber,
|
||||||
|
() => {
|
||||||
|
loadVehicles();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
|
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
|
||||||
<UserSubPageWrapper>
|
<UserSubPageWrapper>
|
||||||
<template #title>
|
<template #title>
|
||||||
<PageTitle :title="$t('user_admin.title')" :subtitle="$t('user_admin.subtitle')" />
|
<PageTitle :title="$t('user_admin.title')" :subtitle="$t('user_admin.overview.subtitle')" />
|
||||||
</template>
|
</template>
|
||||||
<div class="columns is-multiline">
|
|
||||||
<!-- Customer small -->
|
<section class="user-overview" data-testid="superuser-user-overview-page">
|
||||||
<div class="column is-12">
|
<div v-if="isUserLoading && !selectedUserLoaded" class="box" data-testid="superuser-user-overview-loading">
|
||||||
<div class="card">
|
<p class="has-text-weight-semibold">{{ $t("user_admin.overview.loading_user") }}</p>
|
||||||
<div class="card-header">
|
<p class="has-text-grey">{{ $t("user_admin.orders.loading_subtitle") }}</p>
|
||||||
<!-- User icon -->
|
</div>
|
||||||
<div class="card-header-icon">
|
|
||||||
<span class="icon">
|
<div v-else-if="userLoadError" class="notification is-danger" data-testid="superuser-user-overview-error">
|
||||||
<i class="fas fa-user"></i>
|
<p class="has-text-weight-semibold">{{ $t("user_admin.overview.load_failed") }}</p>
|
||||||
|
<p>{{ userLoadError?.response?.data?.message || userLoadError?.message || $t("common.unknown_error") }}</p>
|
||||||
|
<button class="button is-light mt-3" type="button" data-testid="superuser-user-overview-retry" @click="reloadOverview">
|
||||||
|
{{ $t("common.retry") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else-if="selectedUserLoaded">
|
||||||
|
<div class="user-overview__header box" data-testid="superuser-user-overview-header">
|
||||||
|
<div>
|
||||||
|
<p class="heading">{{ $t("user_admin.overview.management_hub") }}</p>
|
||||||
|
<h2 class="title is-3 mb-2">{{ displayName }}</h2>
|
||||||
|
<p class="subtitle is-6 mb-0">
|
||||||
|
{{ $t("user_admin.user_id") }} {{ userId }}
|
||||||
|
<span v-if="hasCustomerNumber">· {{ $t("user_admin.customer_number") }} {{ customerNumber }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="user-overview__header-actions">
|
||||||
|
<ActionSettingsWheelButton
|
||||||
|
:user_id="userId"
|
||||||
|
:customer_number="customerNumber || null"
|
||||||
|
data-testid="superuser-user-overview-actions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-overview__metrics columns is-multiline" data-testid="superuser-user-overview-metrics">
|
||||||
|
<div v-for="metric in overviewMetrics" :key="metric.key" class="column is-3-desktop is-6-tablet">
|
||||||
|
<div class="box user-overview__metric" :data-testid="`superuser-user-overview-metric-${metric.key}`">
|
||||||
|
<div class="user-overview__metric-top">
|
||||||
|
<span class="icon"><i :class="metric.icon"></i></span>
|
||||||
|
<span class="tag is-light" :class="metric.tone">{{ metric.status }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="heading">{{ metric.label }}</p>
|
||||||
|
<p class="title is-4">{{ metric.value }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box" data-testid="superuser-user-overview-shortcuts">
|
||||||
|
<div class="level is-mobile user-overview__section-title">
|
||||||
|
<div class="level-left">
|
||||||
|
<h3 class="title is-5 mb-0">{{ $t("user_admin.overview.workspace_shortcuts") }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<router-link
|
||||||
|
v-for="link in hubLinks"
|
||||||
|
:key="link.key"
|
||||||
|
class="button is-light"
|
||||||
|
:to="link.to"
|
||||||
|
:data-testid="`superuser-user-overview-link-${link.key}`"
|
||||||
|
>
|
||||||
|
<span class="icon"><i :class="link.icon"></i></span>
|
||||||
|
<span>{{ link.label }}</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div class="column is-6-desktop">
|
||||||
|
<section class="box" data-testid="superuser-user-overview-account">
|
||||||
|
<h3 class="title is-5">{{ $t("user_admin.user_data") }}</h3>
|
||||||
|
<dl class="user-overview__details">
|
||||||
|
<template v-for="detail in userDetails" :key="detail.label">
|
||||||
|
<dt>{{ detail.label }}</dt>
|
||||||
|
<dd>{{ valueOrEmpty(detail.value) }}</dd>
|
||||||
|
</template>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6-desktop">
|
||||||
|
<section class="box" data-testid="superuser-user-overview-economic">
|
||||||
|
<h3 class="title is-5">{{ $t("user_admin.economic_data") }}</h3>
|
||||||
|
<dl class="user-overview__details">
|
||||||
|
<template v-for="detail in economicDetails" :key="detail.label">
|
||||||
|
<dt>{{ detail.label }}</dt>
|
||||||
|
<dd>{{ valueOrEmpty(detail.value) }}</dd>
|
||||||
|
</template>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div class="column is-6-desktop">
|
||||||
|
<section class="box" data-testid="superuser-user-overview-access">
|
||||||
|
<h3 class="title is-5">{{ $t("user_admin.permissions") }}</h3>
|
||||||
|
<div v-if="visiblePermissions.length" class="tags">
|
||||||
|
<span
|
||||||
|
v-for="permission in visiblePermissions"
|
||||||
|
:key="permission"
|
||||||
|
class="tag is-info is-light"
|
||||||
|
:data-testid="`superuser-user-overview-permission-${permission}`"
|
||||||
|
>
|
||||||
|
{{ permission }}
|
||||||
|
</span>
|
||||||
|
<span v-if="hiddenPermissionCount > 0" class="tag">
|
||||||
|
{{ $t("user_admin.overview.more_permissions", { count: hiddenPermissionCount }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p v-else class="has-text-grey">{{ $t("global.no_data") }}</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6-desktop">
|
||||||
|
<section class="box" data-testid="superuser-user-overview-rules">
|
||||||
|
<h3 class="title is-5">{{ $t("user_admin.overview.customer_rules") }}</h3>
|
||||||
|
<ActionSettingsWheelButton
|
||||||
|
v-if="hasCustomerNumber"
|
||||||
|
:user_id="userId"
|
||||||
|
:customer_number="customerNumber"
|
||||||
|
:display-actions-directly="true"
|
||||||
|
data-testid="superuser-user-overview-direct-actions"
|
||||||
|
/>
|
||||||
|
<p v-else class="has-text-grey">{{ $t("user_admin.overview.customer_number_required") }}</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section v-if="hasCustomerNumber" class="box" data-testid="superuser-user-overview-customer-management">
|
||||||
|
<h3 class="title is-5">{{ $t("user_admin.overview.customer_management") }}</h3>
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div class="column is-6-desktop">
|
||||||
|
<UserFixedPricing :key="`fixed-${customerNumber}`" :customer_number="customerNumber" />
|
||||||
|
</div>
|
||||||
|
<div class="column is-6-desktop">
|
||||||
|
<UserDefaultDepartment :key="`department-${customerNumber}`" :customer_number="customerNumber" />
|
||||||
|
</div>
|
||||||
|
<div class="column is-6-desktop">
|
||||||
|
<UserOtherSpecialArrangement :key="`special-${userId}`" :user_id="userId" />
|
||||||
|
</div>
|
||||||
|
<div class="column is-6-desktop">
|
||||||
|
<UserOtherVaskeabonnement :key="`subscription-note-${userId}`" :user_id="userId" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else class="notification is-warning" data-testid="superuser-user-overview-no-customer-number">
|
||||||
|
{{ $t("user_admin.overview.customer_number_required") }}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="hasCustomerNumber" class="box" data-testid="superuser-user-overview-vehicles">
|
||||||
|
<div class="level is-mobile user-overview__section-title">
|
||||||
|
<div class="level-left">
|
||||||
|
<h3 class="title is-5 mb-0">{{ $t("common.vehicles") }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<button
|
||||||
|
class="button is-small is-light"
|
||||||
|
type="button"
|
||||||
|
:disabled="vehiclesLoading"
|
||||||
|
data-testid="superuser-user-overview-vehicles-reload"
|
||||||
|
@click="loadVehicles"
|
||||||
|
>
|
||||||
|
{{ $t("global.reload") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="vehiclesError" class="notification is-warning" data-testid="superuser-user-overview-vehicles-error">
|
||||||
|
{{ $t("user_admin.overview.vehicles_load_failed") }}
|
||||||
|
</div>
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div class="column is-4">
|
||||||
|
<div class="box user-overview__compact-stat">
|
||||||
|
<p class="heading">{{ $t("common.vehicles") }}</p>
|
||||||
|
<p class="title is-4">{{ vehicles.length }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4">
|
||||||
|
<div class="box user-overview__compact-stat">
|
||||||
|
<p class="heading">{{ $t("user_admin.wash_subscriptions") }}</p>
|
||||||
|
<p class="title is-4">{{ activeVehicleSubscriptions }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4">
|
||||||
|
<div class="box user-overview__compact-stat">
|
||||||
|
<p class="heading">{{ SessionUser.superUser.modules.xlvask.meta.title }}</p>
|
||||||
|
<p class="title is-4">{{ selfServiceVehicles }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="visibleVehicles.length" class="tags" data-testid="superuser-user-overview-vehicle-preview">
|
||||||
|
<span
|
||||||
|
v-for="vehicle in visibleVehicles"
|
||||||
|
:key="vehicle.id || vehicle.reg"
|
||||||
|
class="tag is-light"
|
||||||
|
:data-testid="`superuser-user-overview-vehicle-${vehicle.id || vehicle.reg}`"
|
||||||
|
>
|
||||||
|
{{ vehicle.reg || vehicle.reference || `#${vehicle.id}` }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- User name -->
|
<p v-else-if="!vehiclesLoading" class="has-text-grey">{{ $t("global.no_data") }}</p>
|
||||||
<div class="card-header-title">
|
<div class="buttons">
|
||||||
{{ user.economicData.name }}
|
<router-link class="button is-light" :to="`${basePath}/vehicles`" data-testid="superuser-user-overview-open-vehicles">
|
||||||
|
<span class="icon"><i class="fas fa-list"></i></span>
|
||||||
|
<span>{{ $t("user_admin.overview.open_vehicles") }}</span>
|
||||||
|
</router-link>
|
||||||
|
<button
|
||||||
|
class="button is-light"
|
||||||
|
type="button"
|
||||||
|
data-testid="superuser-user-overview-add-vehicle"
|
||||||
|
@click="SessionUser.objects.vehicles.functions.showCreateObjectForCustomerForm(customerNumber, loadVehicles)"
|
||||||
|
>
|
||||||
|
<span class="icon"><i class="fas fa-plus"></i></span>
|
||||||
|
<span>{{ $t("user_vehicles.add_vehicle") }}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- User settings wheel -->
|
</section>
|
||||||
<div class="card-header-icon">
|
|
||||||
<ActionSettingsWheelButton
|
<section v-if="hasCustomerNumber" class="box" data-testid="superuser-user-overview-subscriptions">
|
||||||
:user_id="userId"
|
<h3 class="title is-5">{{ $t("user_admin.overview.subscription_invoicing") }}</h3>
|
||||||
>
|
<UserVehicleSubscriptionsDisplay :key="`subscriptions-${customerNumber}`" :user="user" />
|
||||||
<template #actions>
|
<p v-if="washSubscriptionTransactions.length === 0" class="has-text-grey">
|
||||||
<!-- Since there's no additional actions, we can use the default slot -->
|
{{ $t("user_admin.overview.no_subscription_transactions") }}
|
||||||
</template>
|
</p>
|
||||||
</ActionSettingsWheelButton>
|
</section>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-12">
|
|
||||||
<div class="box">
|
|
||||||
<!-- User fixed pricing -->
|
|
||||||
<div class="mb-2">
|
|
||||||
<UserFixedPricing v-if="user.customer_number.value && user.customer_number.value > 0" v-bind:customer_number="user.customer_number.value" />
|
|
||||||
</div>
|
|
||||||
<!-- Default department -->
|
|
||||||
<div class="mb-2">
|
|
||||||
<UserDefaultDepartment v-if="user.customer_number.value && user.customer_number.value > 0" v-bind:customer_number="user.customer_number.value" />
|
|
||||||
</div>
|
|
||||||
<!-- Vehicle subscriptions -->
|
|
||||||
<UserVehicleSubscriptionsDisplay
|
|
||||||
v-if="user.customer_number.value && user.customer_number.value > 0"
|
|
||||||
:user="user"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-6">
|
|
||||||
<div class="box">
|
|
||||||
<h3 class="title is-4">{{ $t('user_admin.user_data') }}</h3>
|
|
||||||
<div class="content">
|
|
||||||
<p>{{ $t('user_admin.user_id') }}: {{ userId }}</p>
|
|
||||||
<p>{{ $t('user_admin.customer_number') }}: {{ user.customer_number }}</p>
|
|
||||||
<p>{{ $t('user_admin.group_id') }}: {{ user.group_id }}</p>
|
|
||||||
<p>{{ $t('common.created') }}: {{ user.created_at }}</p>
|
|
||||||
<p>{{ $t('user_admin.updated_at') }}: {{ user.updated_at }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Economic data -->
|
|
||||||
<div class="column is-6">
|
|
||||||
<div class="box">
|
|
||||||
<h3 class="title is-4">{{ $t('user_admin.economic_data') }} ({{ user.economicData.name ? user.economicData.name : $t('user_admin.no_data') }})</h3>
|
|
||||||
<div class="content">
|
|
||||||
<p>{{ $t('user_admin.customer_number') }}: {{ user.economicData.customerNumber }}</p>
|
|
||||||
<p>{{ $t('common.name') }}: {{ user.economicData.name }}</p>
|
|
||||||
<p>{{ $t('common.address') }}: {{ user.economicData.address }}</p>
|
|
||||||
<p>{{ $t('user_admin.zip') }}: {{ user.economicData.zip }}</p>
|
|
||||||
<p>{{ $t('user_admin.city') }}: {{ user.economicData.city }}</p>
|
|
||||||
<p>{{ $t('user_admin.mobile_phone') }}: {{ user.economicData.mobilePhone }}</p>
|
|
||||||
<p>{{ $t('common.email') }}: {{ user.economicData.email }}</p>
|
|
||||||
<p>{{ $t('user_admin.cvr') }}: {{ user.economicData.cvr }}</p>
|
|
||||||
<p>{{ $t('user_admin.currency') }}: {{ user.economicData.currency }}</p>
|
|
||||||
<p>{{ $t('common.country') }}: {{ user.economicData.country }}</p>
|
|
||||||
<p>{{ $t('user_admin.barred') }}: {{user.economicData.barred }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Permissions -->
|
|
||||||
<div class="column is-12">
|
|
||||||
<div class="box">
|
|
||||||
<h3 class="title is-4">{{ $t('user_admin.permissions') }}</h3>
|
|
||||||
<div class="content">
|
|
||||||
<ul>
|
|
||||||
<li v-for="permission in user.permissions.value" :key="permission.id">{{ permission }}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Variables -->
|
|
||||||
<div class="column is-12">
|
|
||||||
<div class="box">
|
|
||||||
<h3 class="title is-4">{{ $t('user_admin.variables') }}</h3>
|
|
||||||
<div class="content">
|
|
||||||
<p>SessionUser: {{ SessionUser.valueOf() }}</p>
|
|
||||||
<p>userId: {{ userId }}</p>
|
|
||||||
<p>user: {{ user.valueOf() }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</UserSubPageWrapper>
|
</UserSubPageWrapper>
|
||||||
</RestrictedPageWrapper>
|
</RestrictedPageWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.user-overview {
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
.user-overview__header {
|
||||||
|
align-items: flex-start;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-overview__header-actions {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-overview__metric,
|
||||||
|
.user-overview__compact-stat {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-overview__metric-top {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-overview__details {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem 1rem;
|
||||||
|
grid-template-columns: minmax(8rem, 38%) 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-overview__details dt {
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-overview__details dd {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-overview__section-title {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.user-overview__header {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-overview__header-actions {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-overview__details {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import UserSubPageWrapper from "@/views/dashboards/superUserDashboard/user/UserSubPageWrapper.vue";
|
import UserSubPageWrapper from "@/views/dashboards/superUserDashboard/user/UserSubPageWrapper.vue";
|
||||||
import PageTitle from "@/components/global/PageTitle.vue";
|
import PageTitle from "@/components/global/PageTitle.vue";
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { user, getUserData, userId, setUser, getUserProductDiscount, getUserGlobalDiscount, getProductBestApplicableDiscount, getUserProductOnlyDiscount, getProductCategory, getUserCategoryDiscount, isProductAllowedToApplyCategoryDiscounts } from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
import { user, getUserData, userId, setUser, getUserProductDiscount, getUserGlobalDiscount, getProductBestApplicableDiscount, getUserProductOnlyDiscount, getUserProductFixedPrice, getProductEffectivePrice, getProductCategory, getUserCategoryDiscount, isProductAllowedToApplyCategoryDiscounts } from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
||||||
import { getProducts } from "@/components/shop/Products.vue";
|
import { getProducts } from "@/components/shop/Products.vue";
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||||
@@ -56,6 +56,72 @@ const editDiscount = (product, isCategory) => {
|
|||||||
allowOutsideClick: () => !Swal.isLoading()
|
allowOutsideClick: () => !Swal.isLoading()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatPrice = (price) => `${Number(price || 0).toFixed(0)} Kr.`;
|
||||||
|
|
||||||
|
const getProductPriceDisplay = (product) => {
|
||||||
|
const fixedPrice = getUserProductFixedPrice(product);
|
||||||
|
if (fixedPrice !== null) {
|
||||||
|
return `${formatPrice(fixedPrice)} (fast pris)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const discount = getProductBestApplicableDiscount(product);
|
||||||
|
if (discount === 0) {
|
||||||
|
return formatPrice(product.price);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${formatPrice(getProductEffectivePrice(product))} (${discount}% rabat)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editFixedPrice = (product) => {
|
||||||
|
const fixedPrice = getUserProductFixedPrice(product);
|
||||||
|
Swal.fire({
|
||||||
|
title: product.name + ' ( product: ' + product.id + ' )',
|
||||||
|
input: 'number',
|
||||||
|
inputValue: fixedPrice === null ? '' : fixedPrice,
|
||||||
|
inputLabel: 'Fast pris (Kr.)',
|
||||||
|
inputAttributes: {
|
||||||
|
autocapitalize: 'off',
|
||||||
|
min: 0,
|
||||||
|
step: 1
|
||||||
|
},
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Save',
|
||||||
|
showLoaderOnConfirm: true,
|
||||||
|
inputValidator: (value) => {
|
||||||
|
const normalizedValue = String(value ?? '').trim();
|
||||||
|
if (normalizedValue === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedValue = Number(normalizedValue);
|
||||||
|
if (!Number.isInteger(parsedValue) || parsedValue < 0) {
|
||||||
|
return 'Fast pris skal være et heltal eller tom.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
preConfirm: (value) => {
|
||||||
|
const normalizedValue = String(value ?? '').trim();
|
||||||
|
const fixedPriceValue = normalizedValue === '' ? null : Number.parseInt(normalizedValue, 10);
|
||||||
|
return authenticatedRequest(`/superuser/user/discounts`, "POST", {
|
||||||
|
user_id: userId.value,
|
||||||
|
object_id: product.id,
|
||||||
|
discount: getUserProductOnlyDiscount(product),
|
||||||
|
fixed_price: fixedPriceValue,
|
||||||
|
is_category: false
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response);
|
||||||
|
getUserData();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
allowOutsideClick: () => !Swal.isLoading()
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -74,6 +140,7 @@ const editDiscount = (product, isCategory) => {
|
|||||||
<th>{{ $t('tables.common.category') }}</th>
|
<th>{{ $t('tables.common.category') }}</th>
|
||||||
<th>{{ $t('tables.common.product_name') }}</th>
|
<th>{{ $t('tables.common.product_name') }}</th>
|
||||||
<th>{{ $t('tables.common.price') }}</th>
|
<th>{{ $t('tables.common.price') }}</th>
|
||||||
|
<th>Fast pris</th>
|
||||||
<th>{{ $t('tables.common.discount_item') }}</th>
|
<th>{{ $t('tables.common.discount_item') }}</th>
|
||||||
<th>{{ $t('tables.common.discount_category') }}</th>
|
<th>{{ $t('tables.common.discount_category') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -84,8 +151,16 @@ const editDiscount = (product, isCategory) => {
|
|||||||
<td>{{ getProductCategory(product)}}</td>
|
<td>{{ getProductCategory(product)}}</td>
|
||||||
<td>{{ product.name }}</td>
|
<td>{{ product.name }}</td>
|
||||||
<!-- Price -->
|
<!-- Price -->
|
||||||
<td v-if="getProductBestApplicableDiscount(product) === 0">{{ product.price }} Kr.</td>
|
<td>{{ getProductPriceDisplay(product) }}</td>
|
||||||
<td v-else>{{ (product.price - (product.price * (getProductBestApplicableDiscount(product) / 100))).toFixed(0) }} Kr. ({{ getProductBestApplicableDiscount(product) }}% rabat)</td>
|
|
||||||
|
<!-- Fixed price -->
|
||||||
|
<td
|
||||||
|
class="is-clickable"
|
||||||
|
@click="editFixedPrice(product)"
|
||||||
|
:data-testid="`customer-fixed-price-${product.id}`"
|
||||||
|
><template v-if="getUserProductFixedPrice(product) !== null">{{ getUserProductFixedPrice(product) }} Kr.</template><template v-else>-</template>
|
||||||
|
<i class="is-pulled-right fas fa-edit"></i>
|
||||||
|
</td>
|
||||||
|
|
||||||
<!-- Discount: Item -->
|
<!-- Discount: Item -->
|
||||||
<td
|
<td
|
||||||
@@ -122,4 +197,4 @@ const editDiscount = (product, isCategory) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { watch } from "vue";
|
||||||
import SuperUserDashboardUserNavigation from "@/views/dashboards/superUserDashboard/user/SuperUserDashboardUserNavigation.vue";
|
import SuperUserDashboardUserNavigation from "@/views/dashboards/superUserDashboard/user/SuperUserDashboardUserNavigation.vue";
|
||||||
import SuperUserDashboardNavigation from "@/views/dashboards/superUserDashboard/SuperUserDashboardNavigation.vue";
|
import SuperUserDashboardNavigation from "@/views/dashboards/superUserDashboard/SuperUserDashboardNavigation.vue";
|
||||||
import {user, getUserData, userId, setUser, getUserCustomerNumber} from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
import { setUser } from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
||||||
import {useRouter} from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
const router = useRouter()
|
|
||||||
setUser(router.currentRoute.value.params.userId);
|
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.params.userId,
|
||||||
|
(nextUserId) => {
|
||||||
|
setUser(nextUserId);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -21,4 +28,4 @@ setUser(router.currentRoute.value.params.userId);
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import ConfigurationCategory from "@/components/displays/superuser/configuration/ConfigurationCategory.vue";
|
import ConfigurationCategory from "@/components/displays/superuser/configuration/ConfigurationCategory.vue";
|
||||||
import ConfigurationInputNumber from "@/components/displays/superuser/configuration/ConfigurationInputNumber.vue";
|
|
||||||
import ConfigurationInput from "@/components/displays/superuser/configuration/ConfigurationInput.vue";
|
import ConfigurationInput from "@/components/displays/superuser/configuration/ConfigurationInput.vue";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import ConfigurationSelect from "@/components/displays/superuser/configuration/ConfigurationSelect.vue";
|
import ConfigurationSelect from "@/components/displays/superuser/configuration/ConfigurationSelect.vue";
|
||||||
@@ -26,7 +25,6 @@ const getDefaultDepartment = async () => {
|
|||||||
customer_number: props.customer_number,
|
customer_number: props.customer_number,
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
let responseData = response.data.data;
|
let responseData = response.data.data;
|
||||||
console.log(response.data.data, 'Default department');
|
|
||||||
department_value.value = responseData.department;
|
department_value.value = responseData.department;
|
||||||
hasDefaultDepartment.value = true;
|
hasDefaultDepartment.value = true;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@@ -40,8 +38,7 @@ const onClickSubmit = async () => {
|
|||||||
await SessionUser.request('/customer/department/default', 'POST', {
|
await SessionUser.request('/customer/department/default', 'POST', {
|
||||||
customer_number: props.customer_number,
|
customer_number: props.customer_number,
|
||||||
department: department_value.value,
|
department: department_value.value,
|
||||||
}).then((response) => {
|
}).then(() => {
|
||||||
console.log(response.data.data, 'Created default department');
|
|
||||||
// Get the newly created fixed pricing
|
// Get the newly created fixed pricing
|
||||||
getDefaultDepartment();
|
getDefaultDepartment();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@@ -75,8 +72,7 @@ const onDeleteConfirmed = async () => {
|
|||||||
// Delete the fixed pricing
|
// Delete the fixed pricing
|
||||||
await SessionUser.request('/customer/department/default', 'DELETE', {
|
await SessionUser.request('/customer/department/default', 'DELETE', {
|
||||||
customer_number: props.customer_number,
|
customer_number: props.customer_number,
|
||||||
}).then((response) => {
|
}).then(() => {
|
||||||
console.log(response.data.data, 'Deleted default department');
|
|
||||||
// Get the newly created fixed pricing
|
// Get the newly created fixed pricing
|
||||||
getDefaultDepartment();
|
getDefaultDepartment();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@@ -163,4 +159,4 @@ getDepartmentOptions();
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import ConfigurationCategory from "@/components/displays/superuser/configuration/ConfigurationCategory.vue";
|
import ConfigurationCategory from "@/components/displays/superuser/configuration/ConfigurationCategory.vue";
|
||||||
import ConfigurationInputNumber from "@/components/displays/superuser/configuration/ConfigurationInputNumber.vue";
|
|
||||||
import ConfigurationInput from "@/components/displays/superuser/configuration/ConfigurationInput.vue";
|
import ConfigurationInput from "@/components/displays/superuser/configuration/ConfigurationInput.vue";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
@@ -27,8 +26,6 @@ const getFixedPricing = async () => {
|
|||||||
await SessionUser.request('/customer/pricing/fixed', 'GET', {
|
await SessionUser.request('/customer/pricing/fixed', 'GET', {
|
||||||
customer_number: props.customer_number,
|
customer_number: props.customer_number,
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
let responseData = response.data.data;
|
|
||||||
console.log(response.data.data, 'Fixed pricing');
|
|
||||||
pricing.value = response.data.data.price;
|
pricing.value = response.data.data.price;
|
||||||
description.value = response.data.data.description;
|
description.value = response.data.data.description;
|
||||||
hasFixedPricing.value = true;
|
hasFixedPricing.value = true;
|
||||||
@@ -44,8 +41,7 @@ const onClickSubmit = async () => {
|
|||||||
customer_number: props.customer_number,
|
customer_number: props.customer_number,
|
||||||
price: inputPrice.value,
|
price: inputPrice.value,
|
||||||
description: inputDescription.value,
|
description: inputDescription.value,
|
||||||
}).then((response) => {
|
}).then(() => {
|
||||||
console.log(response.data.data, 'Fixed pricing');
|
|
||||||
// Get the newly created fixed pricing
|
// Get the newly created fixed pricing
|
||||||
getFixedPricing();
|
getFixedPricing();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@@ -81,8 +77,7 @@ const onDeleteConfirmed = async () => {
|
|||||||
// Delete the fixed pricing
|
// Delete the fixed pricing
|
||||||
await SessionUser.request('/customer/pricing/fixed', 'DELETE', {
|
await SessionUser.request('/customer/pricing/fixed', 'DELETE', {
|
||||||
customer_number: props.customer_number,
|
customer_number: props.customer_number,
|
||||||
}).then((response) => {
|
}).then(() => {
|
||||||
console.log(response.data.data, 'Deleted fixed pricing');
|
|
||||||
// Get the newly created fixed pricing
|
// Get the newly created fixed pricing
|
||||||
getFixedPricing();
|
getFixedPricing();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@@ -167,4 +162,4 @@ getFixedPricing();
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+59
-58
@@ -1,8 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref, watch } from "vue";
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -10,79 +10,73 @@ const props = defineProps({
|
|||||||
user_id: Number,
|
user_id: Number,
|
||||||
readOnly: {
|
readOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
textOnly: {
|
textOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const OtherSpecialArrangementValue = ref(null);
|
const OtherSpecialArrangementValue = ref(null);
|
||||||
|
const userKeys = ref({});
|
||||||
|
|
||||||
const userKeys = ref([{
|
const showLoadError = () => {
|
||||||
key: '',
|
Swal.fire({
|
||||||
value: ''
|
title: t("common.error"),
|
||||||
}]);
|
text: t("common.unable_to_load"),
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonText: t("common.ok"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showSaveError = () => {
|
||||||
|
Swal.fire({
|
||||||
|
title: t("common.error"),
|
||||||
|
text: t("common.unknown_error"),
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonText: t("common.ok"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getKeys = async () => {
|
const getKeys = async () => {
|
||||||
// Get the special arrangement
|
if (!props.user_id) {
|
||||||
await SessionUser.request('/superuser/user/keys?user_id=' + props.user_id, 'GET').then((response) => {
|
userKeys.value = {};
|
||||||
userKeys.value = response.data.data;
|
OtherSpecialArrangementValue.value = null;
|
||||||
console.log(userKeys.value);
|
return;
|
||||||
// Check if the key exists
|
}
|
||||||
try {
|
|
||||||
if (userKeys.value.OtherSpecialArrangement) {
|
try {
|
||||||
OtherSpecialArrangementValue.value = userKeys.value.OtherSpecialArrangement;
|
const response = await SessionUser.request(`/superuser/user/keys?user_id=${props.user_id}`, "GET");
|
||||||
}
|
userKeys.value = response?.data?.data || {};
|
||||||
} catch (e) {
|
OtherSpecialArrangementValue.value = userKeys.value.OtherSpecialArrangement || null;
|
||||||
console.log('Key does not exist');
|
} catch (error) {
|
||||||
}
|
showLoadError();
|
||||||
}).catch((error) => {
|
}
|
||||||
Swal.fire({
|
|
||||||
title: 'Fejl',
|
|
||||||
html: '<p>Der skete en fejl ved hentning af dataen. <br>Prøv igen, eller kontakt support.</p>',
|
|
||||||
icon: 'error',
|
|
||||||
confirmButtonText: 'OK'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setKey = async (key, value) => {
|
const setKey = async (key, value) => {
|
||||||
// Set the key
|
await SessionUser.request("/superuser/user/keys", "POST", {
|
||||||
await SessionUser.request('/superuser/user/keys', 'POST', {
|
|
||||||
user_id: props.user_id,
|
user_id: props.user_id,
|
||||||
key: key,
|
key: key,
|
||||||
value: value
|
value: value,
|
||||||
}).then((response) => {
|
|
||||||
console.log(response);
|
|
||||||
getKeys();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getKeys();
|
|
||||||
const saveForm = async () => {
|
const saveForm = async () => {
|
||||||
if (props.readOnly) {
|
if (props.readOnly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Save the form
|
|
||||||
console.log('Save the form');
|
|
||||||
await setKey('OtherSpecialArrangement', OtherSpecialArrangementValue.value ?? '').then((response) => {
|
|
||||||
console.log(response);
|
|
||||||
}).catch((error) => {
|
|
||||||
// Show the error with a sweetalert
|
|
||||||
Swal.fire({
|
|
||||||
title: 'Fejl',
|
|
||||||
html: '<p>Der skete en fejl ved gemningen af dataen. <br>Prøv igen, eller kontakt support.</p>',
|
|
||||||
icon: 'error',
|
|
||||||
confirmButtonText: 'OK'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseLineBreaks = (text) => {
|
try {
|
||||||
return text.replace(/\n/g, '<br>');
|
await setKey("OtherSpecialArrangement", OtherSpecialArrangementValue.value ?? "");
|
||||||
}
|
await getKeys();
|
||||||
|
} catch (error) {
|
||||||
|
showSaveError();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.user_id, getKeys, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -90,7 +84,12 @@ const parseLineBreaks = (text) => {
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-size-3">{{ t('superuser.user.special_arrangement.label') }}</label>
|
<label class="label is-size-3">{{ t('superuser.user.special_arrangement.label') }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea" v-model="OtherSpecialArrangementValue" placeholder="Udfyld beskrivelse, hvis der er en intern aftale"></textarea>
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
v-model="OtherSpecialArrangementValue"
|
||||||
|
:placeholder="t('superuser.user.special_arrangement.label')"
|
||||||
|
data-testid="superuser-user-special-arrangement-input"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field" v-if="!props.readOnly">
|
<div class="field" v-if="!props.readOnly">
|
||||||
@@ -103,7 +102,7 @@ const parseLineBreaks = (text) => {
|
|||||||
<div class="message is-info" v-if="OtherSpecialArrangementValue">
|
<div class="message is-info" v-if="OtherSpecialArrangementValue">
|
||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
<span><strong>{{ t('superuser.user.special_arrangement.label') }}</strong><br></span>
|
<span><strong>{{ t('superuser.user.special_arrangement.label') }}</strong><br></span>
|
||||||
<span v-if="OtherSpecialArrangementValue" v-html="parseLineBreaks(OtherSpecialArrangementValue)"></span>
|
<span v-if="OtherSpecialArrangementValue" class="preserve-lines">{{ OtherSpecialArrangementValue }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,7 +116,7 @@ const parseLineBreaks = (text) => {
|
|||||||
<div
|
<div
|
||||||
class="message-body"
|
class="message-body"
|
||||||
>
|
>
|
||||||
<p v-if="OtherSpecialArrangementValue" v-html="parseLineBreaks(OtherSpecialArrangementValue)"></p>
|
<p v-if="OtherSpecialArrangementValue" class="preserve-lines">{{ OtherSpecialArrangementValue }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,5 +124,7 @@ const parseLineBreaks = (text) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.preserve-lines {
|
||||||
</style>
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
+59
-62
@@ -1,8 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from "vue";
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -10,91 +10,86 @@ const props = defineProps({
|
|||||||
user_id: Number,
|
user_id: Number,
|
||||||
readOnly: {
|
readOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
textOnly: {
|
textOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const OtherVaskeabonnementValue = ref(null);
|
const OtherVaskeabonnementValue = ref(null);
|
||||||
|
const userKeys = ref({});
|
||||||
|
|
||||||
const userKeys = ref([{
|
const showLoadError = () => {
|
||||||
key: '',
|
Swal.fire({
|
||||||
value: ''
|
title: t("common.error"),
|
||||||
}]);
|
text: t("common.unable_to_load"),
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonText: t("common.ok"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showSaveError = () => {
|
||||||
|
Swal.fire({
|
||||||
|
title: t("common.error"),
|
||||||
|
text: t("common.unknown_error"),
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonText: t("common.ok"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getKeys = async () => {
|
const getKeys = async () => {
|
||||||
// Get the special arrangement
|
if (!props.user_id) {
|
||||||
await SessionUser.request('/superuser/user/keys?user_id=' + props.user_id, 'GET').then((response) => {
|
userKeys.value = {};
|
||||||
userKeys.value = response.data.data;
|
OtherVaskeabonnementValue.value = null;
|
||||||
console.log(userKeys.value);
|
return;
|
||||||
// Check if the key exists
|
}
|
||||||
try {
|
|
||||||
if (userKeys.value.OtherVaskeabonnement) {
|
try {
|
||||||
OtherVaskeabonnementValue.value = userKeys.value.OtherVaskeabonnement;
|
const response = await SessionUser.request(`/superuser/user/keys?user_id=${props.user_id}`, "GET");
|
||||||
}
|
userKeys.value = response?.data?.data || {};
|
||||||
} catch (e) {
|
OtherVaskeabonnementValue.value = userKeys.value.OtherVaskeabonnement || null;
|
||||||
console.log('Key does not exist');
|
} catch (error) {
|
||||||
}
|
showLoadError();
|
||||||
}).catch((error) => {
|
}
|
||||||
Swal.fire({
|
|
||||||
title: 'Fejl',
|
|
||||||
html: '<p>Der skete en fejl ved hentning af dataen. <br>Prøv igen, eller kontakt support.</p>',
|
|
||||||
icon: 'error',
|
|
||||||
confirmButtonText: 'OK'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setKey = async (key, value) => {
|
const setKey = async (key, value) => {
|
||||||
// Set the key
|
await SessionUser.request("/superuser/user/keys", "POST", {
|
||||||
await SessionUser.request('/superuser/user/keys', 'POST', {
|
|
||||||
user_id: props.user_id,
|
user_id: props.user_id,
|
||||||
key: key,
|
key: key,
|
||||||
value: value
|
value: value,
|
||||||
}).then((response) => {
|
|
||||||
console.log(response);
|
|
||||||
getKeys();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getKeys();
|
|
||||||
const saveForm = async () => {
|
const saveForm = async () => {
|
||||||
if (props.readOnly) {
|
if (props.readOnly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Save the form
|
|
||||||
console.log('Save the form');
|
|
||||||
await setKey('OtherVaskeabonnement', OtherVaskeabonnementValue.value ?? '').then((response) => {
|
|
||||||
console.log(response);
|
|
||||||
}).catch((error) => {
|
|
||||||
// Show the error with a sweetalert
|
|
||||||
Swal.fire({
|
|
||||||
title: 'Fejl',
|
|
||||||
html: '<p>Der skete en fejl ved gemningen af dataen. <br>Prøv igen, eller kontakt support.</p>',
|
|
||||||
icon: 'error',
|
|
||||||
confirmButtonText: 'OK'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseLineBreaks = (text) => {
|
try {
|
||||||
return text.replace(/\n/g, '<br>');
|
await setKey("OtherVaskeabonnement", OtherVaskeabonnementValue.value ?? "");
|
||||||
}
|
await getKeys();
|
||||||
|
} catch (error) {
|
||||||
|
showSaveError();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
watch(() => props.user_id, () => {
|
watch(() => props.user_id, getKeys, { immediate: true });
|
||||||
getKeys();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form class="form" @submit.prevent="saveForm" v-if="!readOnly && !textOnly">
|
<form class="form" @submit.prevent="saveForm" v-if="!props.readOnly && !props.textOnly">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-size-3">{{ t('superuser.user.wash_subscription.label') }}</label>
|
<label class="label is-size-3">{{ t('superuser.user.wash_subscription.label') }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea" v-model="OtherVaskeabonnementValue" placeholder="Udfyld beskrivelse, hvis der er en intern aftale"></textarea>
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
v-model="OtherVaskeabonnementValue"
|
||||||
|
:placeholder="t('superuser.user.wash_subscription.label')"
|
||||||
|
data-testid="superuser-user-wash-subscription-note-input"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -107,7 +102,7 @@ watch(() => props.user_id, () => {
|
|||||||
<div class="message is-info" v-if="OtherVaskeabonnementValue">
|
<div class="message is-info" v-if="OtherVaskeabonnementValue">
|
||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
<span><strong>{{ t('superuser.user.wash_subscription.label') }}</strong><br></span>
|
<span><strong>{{ t('superuser.user.wash_subscription.label') }}</strong><br></span>
|
||||||
<span v-if="OtherVaskeabonnementValue" v-html="parseLineBreaks(OtherVaskeabonnementValue)"></span>
|
<span v-if="OtherVaskeabonnementValue" class="preserve-lines">{{ OtherVaskeabonnementValue }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,7 +116,7 @@ watch(() => props.user_id, () => {
|
|||||||
<div
|
<div
|
||||||
class="message-body"
|
class="message-body"
|
||||||
>
|
>
|
||||||
<p v-if="OtherVaskeabonnementValue" v-html="parseLineBreaks(OtherVaskeabonnementValue)"></p>
|
<p v-if="OtherVaskeabonnementValue" class="preserve-lines">{{ OtherVaskeabonnementValue }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,5 +124,7 @@ watch(() => props.user_id, () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.preserve-lines {
|
||||||
</style>
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
-7
@@ -35,7 +35,6 @@ const washSubscriptionVehicles = ref(null);
|
|||||||
|
|
||||||
const load = () => {
|
const load = () => {
|
||||||
if (!props.user.customer_number.value) {
|
if (!props.user.customer_number.value) {
|
||||||
console.log('No customer number found');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Load the vehicles
|
// Load the vehicles
|
||||||
@@ -142,13 +141,8 @@ const showAddSubscriptionInvoiceFormOtherMonth = () => {
|
|||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
const selected = result.value;
|
const selected = result.value;
|
||||||
console.log(selected);
|
|
||||||
const monthNumber = selected.substring(5, 7);
|
const monthNumber = selected.substring(5, 7);
|
||||||
const yearNumber = selected.substring(0, 4);
|
const yearNumber = selected.substring(0, 4);
|
||||||
// Redirect to the invoice creation page
|
|
||||||
console.log(
|
|
||||||
'Month: ' + monthNumber + ', Year: ' + yearNumber
|
|
||||||
);
|
|
||||||
createVehicleSubscriptionInvoice(monthNumber, yearNumber);
|
createVehicleSubscriptionInvoice(monthNumber, yearNumber);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -164,7 +158,6 @@ const createVehicleSubscriptionInvoice = (month, year) => {
|
|||||||
year: year,
|
year: year,
|
||||||
}
|
}
|
||||||
).then((response) => {
|
).then((response) => {
|
||||||
console.log(response);
|
|
||||||
// Reload the data
|
// Reload the data
|
||||||
load();
|
load();
|
||||||
// Redirect to the invoice creation page
|
// Redirect to the invoice creation page
|
||||||
|
|||||||
@@ -1369,6 +1369,7 @@ const scheduleRecentCompletedWashRefresh = (attempt = 0) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
recentCompletedRefreshTimeout.value = hostWindow.setTimeout(() => {
|
recentCompletedRefreshTimeout.value = hostWindow.setTimeout(() => {
|
||||||
|
recentCompletedRefreshTimeout.value = null;
|
||||||
if (isMyWashStartUnmounted.value) {
|
if (isMyWashStartUnmounted.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { isCompactProject } from "./support/projects";
|
|||||||
|
|
||||||
const adminPermissions = [
|
const adminPermissions = [
|
||||||
"admin",
|
"admin",
|
||||||
|
"list_department_daily_reports",
|
||||||
|
"list_bookings",
|
||||||
"create_department_daily_report_complaints",
|
"create_department_daily_report_complaints",
|
||||||
"department_access_1",
|
"department_access_1",
|
||||||
"department_access_2",
|
"department_access_2",
|
||||||
@@ -26,8 +28,26 @@ const buildOverviewPayload = ({
|
|||||||
complaintsMessage = null,
|
complaintsMessage = null,
|
||||||
metrics = {},
|
metrics = {},
|
||||||
products = [
|
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,
|
product_id: 27,
|
||||||
slug: "extraordinary-10-min",
|
slug: "extraordinary-10-min",
|
||||||
@@ -35,6 +55,8 @@ const buildOverviewPayload = ({
|
|||||||
state: "ready",
|
state: "ready",
|
||||||
value: 1,
|
value: 1,
|
||||||
out_of: 8,
|
out_of: 8,
|
||||||
|
target_percentage: null,
|
||||||
|
target_department_id: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
product_id: 26,
|
product_id: 26,
|
||||||
@@ -43,8 +65,19 @@ const buildOverviewPayload = ({
|
|||||||
state: "ready",
|
state: "ready",
|
||||||
value: 4,
|
value: 4,
|
||||||
out_of: 8,
|
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,
|
product_id: 22,
|
||||||
slug: "double-duty-kemi",
|
slug: "double-duty-kemi",
|
||||||
@@ -52,6 +85,8 @@ const buildOverviewPayload = ({
|
|||||||
state: "ready",
|
state: "ready",
|
||||||
value: 5,
|
value: 5,
|
||||||
out_of: 8,
|
out_of: 8,
|
||||||
|
target_percentage: null,
|
||||||
|
target_department_id: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
...overrides
|
...overrides
|
||||||
@@ -92,6 +127,9 @@ async function mockDailyReportDependencies(
|
|||||||
body: Record<string, unknown>
|
body: Record<string, unknown>
|
||||||
) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>;
|
) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>;
|
||||||
complaintCustomerLookupHandler?: (url: URL) => 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[];
|
permissions?: string[];
|
||||||
}
|
}
|
||||||
| ((url: URL) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>)
|
| ((url: URL) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>)
|
||||||
@@ -114,6 +152,16 @@ async function mockDailyReportDependencies(
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
const complaintCustomerLookupHandler = options.complaintCustomerLookupHandler || (async () => json({ data: [] }));
|
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 seedAuthenticatedState(page);
|
||||||
await mockApi(page, {
|
await mockApi(page, {
|
||||||
@@ -167,6 +215,12 @@ async function mockDailyReportDependencies(
|
|||||||
await route.fulfill(response);
|
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) => {
|
await page.route(/\/departments\/daily-reports(?:\/overview)?(\?.*)?$/i, async (route) => {
|
||||||
const url = new URL(route.request().url());
|
const url = new URL(route.request().url());
|
||||||
if (url.pathname.endsWith("/departments/daily-reports/overview")) {
|
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 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.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-bookings")).toContainText("4");
|
||||||
await expect(page.getByTestId("daily-report-tile-complaints")).toContainText("2");
|
await expect(page.getByTestId("daily-report-tile-complaints")).toContainText("2");
|
||||||
await expect(page.getByTestId("daily-report-complaints-add-button")).toBeVisible();
|
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");
|
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 }) => {
|
test("renders rounded-up weather hours and keeps unknown productivity gray", async ({ page }) => {
|
||||||
await mockDailyReportDependencies(page, {
|
await mockDailyReportDependencies(page, {
|
||||||
overviewHandler: async () => json(buildOverviewPayload()),
|
overviewHandler: async () => json(buildOverviewPayload()),
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ const json = (body: unknown, status = 200) => ({
|
|||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
|
const pageReadyTimeout = process.env.CI ? 30_000 : 15_000;
|
||||||
|
|
||||||
|
const pageBootstrapTimeoutMs = 45_000;
|
||||||
|
|
||||||
const setupNotificationSmsApi = async (page: Page) => {
|
const setupNotificationSmsApi = async (page: Page) => {
|
||||||
let listRequestCount = 0;
|
let listRequestCount = 0;
|
||||||
@@ -115,9 +118,25 @@ test("@smoke department notification SMS active toggle and delete refresh the ta
|
|||||||
const notificationSmsApi = await setupNotificationSmsApi(page);
|
const notificationSmsApi = await setupNotificationSmsApi(page);
|
||||||
|
|
||||||
await page.goto("/admin/1/modules/notifications", { waitUntil: "domcontentloaded" });
|
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" });
|
const row = page.locator("tr", { hasText: "Dispatch line" });
|
||||||
await expect(row).toBeVisible();
|
await expect(row).toBeVisible({ timeout: pageReadyTimeout });
|
||||||
const enabledToggle = page.getByTestId("department-notification-sms-enabled-toggle-501");
|
const enabledToggle = page.getByTestId("department-notification-sms-enabled-toggle-501");
|
||||||
await expect(enabledToggle).toBeChecked();
|
await expect(enabledToggle).toBeChecked();
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const POS_PERMISSIONS = [
|
|||||||
const SUPERUSER_POS_PERMISSIONS = [...POS_PERMISSIONS, "superuser"];
|
const SUPERUSER_POS_PERMISSIONS = [...POS_PERMISSIONS, "superuser"];
|
||||||
|
|
||||||
const POS_BOOT_URL = "/admin/12/modules/pos?step=1";
|
const POS_BOOT_URL = "/admin/12/modules/pos?step=1";
|
||||||
|
const POS_BOOT_READY_TIMEOUT = 20_000;
|
||||||
|
const POS_ORDER_READY_TIMEOUT = 20_000;
|
||||||
const DRAFT_TRANSACTION_CUSTOMER_ID = 44556677;
|
const DRAFT_TRANSACTION_CUSTOMER_ID = 44556677;
|
||||||
const DRAFT_TRANSACTION_CUSTOMER_NAME = "(TEST) Draft Transaction Customer";
|
const DRAFT_TRANSACTION_CUSTOMER_NAME = "(TEST) Draft Transaction Customer";
|
||||||
|
|
||||||
@@ -49,6 +51,22 @@ function getPosBootStep(page: Page) {
|
|||||||
return page.locator('[data-testid="pos-step-1"]:visible, [data-testid="pos-mobile-step-1"]:visible').first();
|
return page.locator('[data-testid="pos-step-1"]:visible, [data-testid="pos-mobile-step-1"]:visible').first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPosOrderReadyTarget(page: Page) {
|
||||||
|
return page
|
||||||
|
.locator(
|
||||||
|
[
|
||||||
|
'[data-testid="pos-order-card"]:visible',
|
||||||
|
'[data-testid="pos-order-main"]:visible',
|
||||||
|
'[data-testid="pos-order-detail"]:visible',
|
||||||
|
].join(", ")
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectPosOrderReady(page: Page, timeout = POS_ORDER_READY_TIMEOUT) {
|
||||||
|
await expect(getPosOrderReadyTarget(page)).toBeVisible({ timeout });
|
||||||
|
}
|
||||||
|
|
||||||
async function navigateToPosBootPage(page: Page, { waitForSession = false }: { waitForSession?: boolean } = {}) {
|
async function navigateToPosBootPage(page: Page, { waitForSession = false }: { waitForSession?: boolean } = {}) {
|
||||||
let lastError: unknown;
|
let lastError: unknown;
|
||||||
|
|
||||||
@@ -64,9 +82,8 @@ async function navigateToPosBootPage(page: Page, { waitForSession = false }: { w
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await page.goto(POS_BOOT_URL, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
await page.goto(POS_BOOT_URL, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
||||||
await Promise.race([sessionRequest, getPosBootStep(page).waitFor({ state: "visible", timeout: 10_000 })]);
|
|
||||||
await expect(getPosBootStep(page)).toBeVisible({ timeout: 10_000 });
|
|
||||||
await sessionRequest;
|
await sessionRequest;
|
||||||
|
await expect(getPosBootStep(page)).toBeVisible({ timeout: POS_BOOT_READY_TIMEOUT });
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
@@ -162,19 +179,24 @@ async function waitForSwalToClose(page: Page) {
|
|||||||
|
|
||||||
async function openOrderSettings(page: Page, orderId: number) {
|
async function openOrderSettings(page: Page, orderId: number) {
|
||||||
await page.goto(`/admin/12/modules/pos/orders/${orderId}`);
|
await page.goto(`/admin/12/modules/pos/orders/${orderId}`);
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible({ timeout: 20_000 });
|
await expectPosOrderReady(page);
|
||||||
await clickVisibleTestId(page, "pos-order-tab-settings");
|
await clickVisibleTestId(page, "pos-order-tab-settings");
|
||||||
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
|
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function revisitCurrentPage(page: Page) {
|
async function revisitCurrentPage(page: Page) {
|
||||||
await page.goto(page.url(), { waitUntil: "domcontentloaded" });
|
await page.goto(page.url(), { waitUntil: "domcontentloaded" });
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible({ timeout: 20_000 });
|
if (new URL(page.url()).pathname.includes("/modules/pos/orders/")) {
|
||||||
|
await expectPosOrderReady(page);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(getPosBootStep(page)).toBeVisible({ timeout: POS_BOOT_READY_TIMEOUT });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadOrderSettings(page: Page, orderId: number) {
|
async function reloadOrderSettings(page: Page, orderId: number) {
|
||||||
await revisitCurrentPage(page);
|
await page.goto(`/admin/12/modules/pos/orders/${orderId}`, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
await expectPosOrderReady(page);
|
||||||
await clickVisibleTestId(page, "pos-order-tab-settings");
|
await clickVisibleTestId(page, "pos-order-tab-settings");
|
||||||
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
|
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
|
||||||
}
|
}
|
||||||
@@ -313,17 +335,18 @@ async function changeOrderInvoiceCollection(page: Page, closedAt: string) {
|
|||||||
async function openOrderDetail(page: Page, orderId = 54518) {
|
async function openOrderDetail(page: Page, orderId = 54518) {
|
||||||
const orderUrl = `/admin/12/modules/pos/orders/${orderId}`;
|
const orderUrl = `/admin/12/modules/pos/orders/${orderId}`;
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
await page.goto(orderUrl, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
await page.goto(orderUrl, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible({ timeout: 10_000 });
|
await expectPosOrderReady(page);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (attempt === 1) {
|
if (attempt === 2) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
await navigateToPosBootPage(page);
|
await page.goto("about:blank").catch(() => {});
|
||||||
|
await delay(250);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -989,7 +1012,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
|||||||
expect(Math.abs((beforeRailBox?.width ?? 0) - (restoredRailBox?.width ?? 0))).toBeLessThanOrEqual(20);
|
expect(Math.abs((beforeRailBox?.width ?? 0) - (restoredRailBox?.width ?? 0))).toBeLessThanOrEqual(20);
|
||||||
|
|
||||||
await page.goto("/admin/12/modules/pos/orders/54518", { waitUntil: "domcontentloaded" });
|
await page.goto("/admin/12/modules/pos/orders/54518", { waitUntil: "domcontentloaded" });
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
await expectPosOrderReady(page);
|
||||||
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
|
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
|
||||||
await expectOrderTotal(page, 1397);
|
await expectOrderTotal(page, 1397);
|
||||||
});
|
});
|
||||||
@@ -1107,7 +1130,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
|||||||
await expect(noteInput).toBeFocused();
|
await expect(noteInput).toBeFocused();
|
||||||
|
|
||||||
await revisitCurrentPage(page);
|
await revisitCurrentPage(page);
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
await expectPosOrderReady(page);
|
||||||
await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toContainText(referenceValue);
|
await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toContainText(referenceValue);
|
||||||
await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toContainText(poValue);
|
await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toContainText(poValue);
|
||||||
await expect(page.getByTestId("pos-order-metadata-note")).toContainText(noteValue);
|
await expect(page.getByTestId("pos-order-metadata-note")).toContainText(noteValue);
|
||||||
@@ -1160,7 +1183,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await revisitCurrentPage(page);
|
await revisitCurrentPage(page);
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
await expectPosOrderReady(page);
|
||||||
await expect(page.getByTestId("pos-order-customer-wishes-reference")).toContainText("+ Tilføj");
|
await expect(page.getByTestId("pos-order-customer-wishes-reference")).toContainText("+ Tilføj");
|
||||||
await expect(page.getByTestId("pos-order-customer-wishes-po")).toContainText("+ Tilføj");
|
await expect(page.getByTestId("pos-order-customer-wishes-po")).toContainText("+ Tilføj");
|
||||||
});
|
});
|
||||||
@@ -1221,7 +1244,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
|||||||
await expect(reg3Input).toHaveValue("NO9012", { timeout: 10000 });
|
await expect(reg3Input).toHaveValue("NO9012", { timeout: 10000 });
|
||||||
|
|
||||||
await revisitCurrentPage(page);
|
await revisitCurrentPage(page);
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
await expectPosOrderReady(page);
|
||||||
await expect(page.getByTestId("pos-order-registration-1")).toContainText("XY1234");
|
await expect(page.getByTestId("pos-order-registration-1")).toContainText("XY1234");
|
||||||
await expect(page.getByTestId("pos-order-registration-2")).toContainText("TR5678");
|
await expect(page.getByTestId("pos-order-registration-2")).toContainText("TR5678");
|
||||||
await expect(page.getByTestId("pos-order-registration-3")).toContainText("NO9012");
|
await expect(page.getByTestId("pos-order-registration-3")).toContainText("NO9012");
|
||||||
@@ -1249,7 +1272,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
|||||||
await expectOrderTotal(page, 2123);
|
await expectOrderTotal(page, 2123);
|
||||||
|
|
||||||
await revisitCurrentPage(page);
|
await revisitCurrentPage(page);
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
await expectPosOrderReady(page);
|
||||||
await expect(page.getByTestId("pos-order-item-note-trigger-9101")).toBeVisible();
|
await expect(page.getByTestId("pos-order-item-note-trigger-9101")).toBeVisible();
|
||||||
await expect(page.getByTestId("pos-order-item-reference-trigger-9101")).toBeVisible();
|
await expect(page.getByTestId("pos-order-item-reference-trigger-9101")).toBeVisible();
|
||||||
await expect(page.getByTestId("pos-order-item-price-9101")).toContainText("1400");
|
await expect(page.getByTestId("pos-order-item-price-9101")).toContainText("1400");
|
||||||
@@ -1274,7 +1297,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
|||||||
await expectOrderTotal(page, 1423);
|
await expectOrderTotal(page, 1423);
|
||||||
|
|
||||||
await revisitCurrentPage(page);
|
await revisitCurrentPage(page);
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
await expectPosOrderReady(page);
|
||||||
await expect(page.getByTestId("pos-order-item-note-trigger-9102")).toBeVisible();
|
await expect(page.getByTestId("pos-order-item-note-trigger-9102")).toBeVisible();
|
||||||
await expect(page.getByTestId("pos-order-item-price-9102")).toContainText("450");
|
await expect(page.getByTestId("pos-order-item-price-9102")).toContainText("450");
|
||||||
await expectOrderTotal(page, 1423);
|
await expectOrderTotal(page, 1423);
|
||||||
@@ -1324,7 +1347,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
|||||||
await expectOrderTotal(page, 0);
|
await expectOrderTotal(page, 0);
|
||||||
|
|
||||||
await revisitCurrentPage(page);
|
await revisitCurrentPage(page);
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
await expectPosOrderReady(page);
|
||||||
await expect(page.getByTestId("pos-order-empty-state")).toBeVisible();
|
await expect(page.getByTestId("pos-order-empty-state")).toBeVisible();
|
||||||
await expectOrderTotal(page, 0);
|
await expectOrderTotal(page, 0);
|
||||||
});
|
});
|
||||||
@@ -1347,7 +1370,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
|||||||
await expectOrderTotal(page, 2021);
|
await expectOrderTotal(page, 2021);
|
||||||
|
|
||||||
await revisitCurrentPage(page);
|
await revisitCurrentPage(page);
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
await expectPosOrderReady(page);
|
||||||
await expect(page.getByTestId("pos-order-item-edit-9200").first()).toBeVisible();
|
await expect(page.getByTestId("pos-order-item-edit-9200").first()).toBeVisible();
|
||||||
await expectOrderTotal(page, 2021);
|
await expectOrderTotal(page, 2021);
|
||||||
});
|
});
|
||||||
@@ -1432,6 +1455,8 @@ test.describe("Admin POS Orders - desktop settings", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("persists scalar metadata edits across settings and details tabs", async ({ page }) => {
|
test("persists scalar metadata edits across settings and details tabs", async ({ page }) => {
|
||||||
|
test.setTimeout(90_000);
|
||||||
|
|
||||||
const { orderId } = await createDisposableOrder(page);
|
const { orderId } = await createDisposableOrder(page);
|
||||||
const submittedValues = {
|
const submittedValues = {
|
||||||
reference: "UPDATED-REF-2026",
|
reference: "UPDATED-REF-2026",
|
||||||
@@ -1819,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-stripe-email-cancel")).toBeVisible();
|
||||||
await expect(page.getByTestId("pos-order-print-receipt")).toHaveCount(0);
|
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: /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 page.getByTestId("pos-order-stripe-email-cancel").click();
|
||||||
await deleteStripeInvoiceRequest;
|
await deleteStripeInvoiceRequest;
|
||||||
@@ -2925,6 +2950,28 @@ test.describe("Admin POS Orders - desktop locked states", () => {
|
|||||||
await expectOrderTotal(page, 1372);
|
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 }) => {
|
test("hides item mutation controls when the order is already invoiced", async ({ page }) => {
|
||||||
await mockApi(page, {
|
await mockApi(page, {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
@@ -2945,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-edit-9101")).toHaveCount(0);
|
||||||
await expect(page.getByTestId("pos-order-item-delete-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.getByTestId("pos-order-add-item")).toHaveCount(0);
|
||||||
|
await expect(page.locator('[data-action-key="economic-invoice-pdf-download"]')).toBeEnabled();
|
||||||
await expectOrderTotal(page, 1372);
|
await expectOrderTotal(page, 1372);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2964,7 +3012,7 @@ test.describe("Admin POS Orders - mobile smoke", () => {
|
|||||||
|
|
||||||
test("opens the settings tab and renders the new controls", async ({ page }) => {
|
test("opens the settings tab and renders the new controls", async ({ page }) => {
|
||||||
await page.goto("/admin/12/modules/pos/orders/54518");
|
await page.goto("/admin/12/modules/pos/orders/54518");
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
await expectPosOrderReady(page);
|
||||||
|
|
||||||
await clickVisibleTestId(page, "pos-order-tab-settings");
|
await clickVisibleTestId(page, "pos-order-tab-settings");
|
||||||
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
|
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
|
||||||
@@ -3093,7 +3141,7 @@ test.describe("Admin POS Orders - mobile smoke", () => {
|
|||||||
expect(Math.abs((restoredMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0))).toBeLessThanOrEqual(20);
|
expect(Math.abs((restoredMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0))).toBeLessThanOrEqual(20);
|
||||||
|
|
||||||
await revisitCurrentPage(page);
|
await revisitCurrentPage(page);
|
||||||
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
await expectPosOrderReady(page);
|
||||||
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
|
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
|
||||||
await expectOrderTotal(page, 1397);
|
await expectOrderTotal(page, 1397);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -825,7 +825,7 @@ test.describe("Coolify infrastructure management", () => {
|
|||||||
await expect(actions.getByRole("button", { name: "Reconcile Coolify" })).toBeVisible();
|
await expect(actions.getByRole("button", { name: "Reconcile Coolify" })).toBeVisible();
|
||||||
await expect(actions.getByRole("button", { name: "Coolify failover" })).toBeVisible();
|
await expect(actions.getByRole("button", { name: "Coolify failover" })).toBeVisible();
|
||||||
await actions.getByRole("button", { name: "Reconcile Coolify" }).click();
|
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 }) => {
|
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.locator(".action-settings-wheel-trigger").click();
|
||||||
await actions.getByRole("button", { name: /Rename|Omdøb/ }).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");
|
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) {
|
async function settleAuthenticatedNavigation(page: Page, targetPath: string, targetUrl: RegExp) {
|
||||||
await expect.poll(() => readStoredToken(page), { timeout: AUTH_TIMEOUT }).not.toBeNull();
|
await expect.poll(() => readStoredToken(page), { timeout: AUTH_TIMEOUT }).not.toBeNull();
|
||||||
const navigateToTarget = async () => {
|
const navigateToTarget = async () => {
|
||||||
@@ -376,6 +380,8 @@ async function settleAuthenticatedNavigation(page: Page, targetPath: string, tar
|
|||||||
await navigateToTarget();
|
await navigateToTarget();
|
||||||
await expect.poll(() => page.url(), { timeout: AUTH_TIMEOUT }).toMatch(targetUrl);
|
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 ACTIVE_LOCALES = ["da", "en", "sv", "de", "no"] as const;
|
||||||
const GENERATED_LOCALES_DIRECTORY = path.join(process.cwd(), "src", "i18n", "generated");
|
const GENERATED_LOCALES_DIRECTORY = path.join(process.cwd(), "src", "i18n", "generated");
|
||||||
const REVIEWED_NON_LITERAL_CALLS = new Set([
|
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/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|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|te|if (key && te(key)) {",
|
||||||
"src/views/dashboards/superUserDashboard/system/ReplicationManagement.vue|t|return t(key, params);",
|
"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 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|<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/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 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/configuration/ConfigurationReleaseManager.vue|t|const translated = t(fullKey, params);",
|
||||||
"src/views/dashboards/superUserDashboard/ErrorReports.vue|t|const value = t(key);",
|
"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|: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|:aria-label="option.titleKey ? $t(option.titleKey) : null"',
|
||||||
"src/views/dashboards/userDashboard/wash/components/LaneSelectionSection.vue|$t|{{ $t(option.statusLabelKey) }}",
|
"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/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|:vehicle-step-guidance="vehicleStepGuidanceKey ? $t(vehicleStepGuidanceKey) : null"',
|
||||||
"src/views/dashboards/userDashboard/wash/MyWashStart.vue|$t|{{ $t(confirmActionLabelKey) }}",
|
"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) => {
|
const readGeneratedJson = (fileName: string) => {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ async function suppressVueDevtoolsOverlay(page) {
|
|||||||
|
|
||||||
async function primeSuperuserSession(page) {
|
async function primeSuperuserSession(page) {
|
||||||
const token = "superuser-e2e-token";
|
const token = "superuser-e2e-token";
|
||||||
await primeMockSession(page, { token });
|
await primeMockSession(page, { token, bootPath: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prepareInvoiceDistributionPage(page, overrides = {}) {
|
async function prepareInvoiceDistributionPage(page, overrides = {}) {
|
||||||
|
|||||||
@@ -1132,7 +1132,6 @@ test.describe("Invoicing period tab", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await openPeriodView(page);
|
await openPeriodView(page);
|
||||||
await setEntireMarchPeriod(page);
|
|
||||||
|
|
||||||
await page.route("**/modules/xlvask/services/usage/orders**", async (route) => {
|
await page.route("**/modules/xlvask/services/usage/orders**", async (route) => {
|
||||||
const path = new URL(route.request().url()).pathname;
|
const path = new URL(route.request().url()).pathname;
|
||||||
@@ -1242,8 +1241,10 @@ test.describe("Invoicing period tab", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await setEntireMarchPeriod(page);
|
||||||
|
|
||||||
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toBeVisible();
|
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toBeVisible();
|
||||||
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toContainText("Selvvask (0/1)");
|
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toContainText("Selvvask (0/2)");
|
||||||
await expect(page.getByTestId("invoicing-period-view-selector-progress-self_wash")).toBeVisible();
|
await expect(page.getByTestId("invoicing-period-view-selector-progress-self_wash")).toBeVisible();
|
||||||
|
|
||||||
await page.getByTestId("invoicing-period-view-selector-self_wash").click();
|
await page.getByTestId("invoicing-period-view-selector-self_wash").click();
|
||||||
@@ -1273,6 +1274,7 @@ test.describe("Invoicing period tab", () => {
|
|||||||
await page.getByTestId("xlvask-automation-accept-8101").click();
|
await page.getByTestId("xlvask-automation-accept-8101").click();
|
||||||
await expect(page.getByTestId("xlvask-automation-suggestion-8101")).toContainText("Accepteret #7001");
|
await expect(page.getByTestId("xlvask-automation-suggestion-8101")).toContainText("Accepteret #7001");
|
||||||
expect(automationAcceptRequests).toEqual([{ suggestion_id: 7101 }]);
|
expect(automationAcceptRequests).toEqual([{ suggestion_id: 7101 }]);
|
||||||
|
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toContainText("Selvvask (1/2)");
|
||||||
|
|
||||||
await expect.poll(() => usageOrderRequests.length).toBeGreaterThan(0);
|
await expect.poll(() => usageOrderRequests.length).toBeGreaterThan(0);
|
||||||
expect(usageOrderRequests.some((request) => request.limit === "10000")).toBe(true);
|
expect(usageOrderRequests.some((request) => request.limit === "10000")).toBe(true);
|
||||||
@@ -2772,7 +2774,10 @@ test.describe("Invoicing period tab", () => {
|
|||||||
|
|
||||||
await page.goto("/superuser/invoices?activeTab=period");
|
await page.goto("/superuser/invoices?activeTab=period");
|
||||||
await expect(page).toHaveURL(/activeTab=period/);
|
await expect(page).toHaveURL(/activeTab=period/);
|
||||||
await expect.poll(() => requestCount).toBeGreaterThan(0);
|
await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible({
|
||||||
|
timeout: periodRouteReadyTimeout,
|
||||||
|
});
|
||||||
|
await expect.poll(() => requestCount, { timeout: periodRouteReadyTimeout }).toBeGreaterThan(0);
|
||||||
|
|
||||||
await page.getByTestId("invoicing-period-view-selector-all").click();
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
||||||
await expect(page.getByTestId("invoicing-period-customer-queue-6001")).toBeVisible();
|
await expect(page.getByTestId("invoicing-period-customer-queue-6001")).toBeVisible();
|
||||||
|
|||||||
@@ -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 { API_HOST, seedAuthenticatedState } from "./support/network.js";
|
||||||
import { isDesktopProject } from "./support/projects";
|
import { isDesktopProject } from "./support/projects";
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ const limitedManagerPermissions = [
|
|||||||
"limited_backoffice_prices_manage",
|
"limited_backoffice_prices_manage",
|
||||||
"limited_backoffice_employees_manage",
|
"limited_backoffice_employees_manage",
|
||||||
"department_access_1",
|
"department_access_1",
|
||||||
|
"department_access_2",
|
||||||
];
|
];
|
||||||
|
|
||||||
const sessionData = {
|
const sessionData = {
|
||||||
@@ -62,7 +63,10 @@ const adminLimitedSessionData = {
|
|||||||
permissions: ["user", "admin", "limited_backoffice_access", "department_access_1"],
|
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 = {
|
const pricePayload = {
|
||||||
department: { id: 1, name: "Assigned Depot", description: "" },
|
department: { id: 1, name: "Assigned Depot", description: "" },
|
||||||
@@ -235,6 +239,11 @@ const rolePermissionGroups = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const remotePricePayload = {
|
||||||
|
...pricePayload,
|
||||||
|
department: { id: 2, name: "Remote Depot", description: "" },
|
||||||
|
};
|
||||||
|
|
||||||
const rolesPayload = [
|
const rolesPayload = [
|
||||||
{ key: "viewer", label: "Viewer", description: "Can view.", permission_groups: rolePermissionGroups.viewer },
|
{ key: "viewer", label: "Viewer", description: "Can view.", permission_groups: rolePermissionGroups.viewer },
|
||||||
{ key: "cashier", label: "Cashier", description: "Can sell.", permission_groups: rolePermissionGroups.cashier },
|
{ key: "cashier", label: "Cashier", description: "Can sell.", permission_groups: rolePermissionGroups.cashier },
|
||||||
@@ -262,6 +271,7 @@ const rolesPayload = [
|
|||||||
const employeesPayload = [
|
const employeesPayload = [
|
||||||
{
|
{
|
||||||
id: 501,
|
id: 501,
|
||||||
|
user_id: 501,
|
||||||
customer_number: 900000501,
|
customer_number: 900000501,
|
||||||
display_name: "Casey Clerk",
|
display_name: "Casey Clerk",
|
||||||
email: "casey@example.com",
|
email: "casey@example.com",
|
||||||
@@ -273,6 +283,17 @@ const employeesPayload = [
|
|||||||
created_at: "2026-01-01 00:00:00",
|
created_at: "2026-01-01 00:00:00",
|
||||||
updated_at: "2026-01-01 00:00:00",
|
updated_at: "2026-01-01 00:00:00",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 502,
|
||||||
|
customer_number: 900000502,
|
||||||
|
display_name: "Riley Remote",
|
||||||
|
email: "riley@example.com",
|
||||||
|
active: true,
|
||||||
|
role: { key: "viewer", label: "Viewer", description: "Can view." },
|
||||||
|
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") {
|
async function seedLimitedBackofficeSession(page, token = "limited-backoffice-token") {
|
||||||
@@ -288,6 +309,7 @@ async function mockLimitedBackofficeApi(page, authSessionData = sessionData, opt
|
|||||||
const priceUpdateCalls: unknown[] = [];
|
const priceUpdateCalls: unknown[] = [];
|
||||||
const employeeCreateCalls: unknown[] = [];
|
const employeeCreateCalls: unknown[] = [];
|
||||||
const employeeUpdateCalls: unknown[] = [];
|
const employeeUpdateCalls: unknown[] = [];
|
||||||
|
const loginLinkCalls: string[] = [];
|
||||||
const currentPricePayload = {
|
const currentPricePayload = {
|
||||||
value: cloneJson(options.pricePayload ?? pricePayload),
|
value: cloneJson(options.pricePayload ?? pricePayload),
|
||||||
};
|
};
|
||||||
@@ -348,6 +370,11 @@ async function mockLimitedBackofficeApi(page, authSessionData = sessionData, opt
|
|||||||
return;
|
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") {
|
if (pathname.endsWith("/limited-backoffice/departments/1/prices") && method === "PUT") {
|
||||||
const body = request.postDataJSON?.() || null;
|
const body = request.postDataJSON?.() || null;
|
||||||
priceUpdateCalls.push(body);
|
priceUpdateCalls.push(body);
|
||||||
@@ -399,6 +426,27 @@ async function mockLimitedBackofficeApi(page, authSessionData = sessionData, opt
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loginLinkMatch = pathname.match(/\/limited-backoffice\/employees\/(\d+)\/login-link$/);
|
||||||
|
if (loginLinkMatch && method === "POST") {
|
||||||
|
const employeeId = loginLinkMatch[1];
|
||||||
|
const status = options.loginLinkStatus ?? 200;
|
||||||
|
loginLinkCalls.push(employeeId);
|
||||||
|
await route.fulfill(
|
||||||
|
json(
|
||||||
|
status === 200
|
||||||
|
? {
|
||||||
|
data: {
|
||||||
|
employee_id: Number(employeeId),
|
||||||
|
login_path: `/login/qr?token=${"a".repeat(64)}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: { message: options.loginLinkMessage ?? "Could not create employee login link." },
|
||||||
|
status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const employeeMatch = pathname.match(/\/limited-backoffice\/employees\/(\d+)$/);
|
const employeeMatch = pathname.match(/\/limited-backoffice\/employees\/(\d+)$/);
|
||||||
if (employeeMatch && method === "PUT") {
|
if (employeeMatch && method === "PUT") {
|
||||||
const body = request.postDataJSON?.() || {};
|
const body = request.postDataJSON?.() || {};
|
||||||
@@ -435,9 +483,24 @@ async function mockLimitedBackofficeApi(page, authSessionData = sessionData, opt
|
|||||||
priceUpdateCalls,
|
priceUpdateCalls,
|
||||||
employeeCreateCalls,
|
employeeCreateCalls,
|
||||||
employeeUpdateCalls,
|
employeeUpdateCalls,
|
||||||
|
loginLinkCalls,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.describe("Limited backoffice", () => {
|
||||||
test("shows the limited backoffice header shortcut only on department-scoped admin pages", async ({
|
test("shows the limited backoffice header shortcut only on department-scoped admin pages", async ({
|
||||||
page,
|
page,
|
||||||
@@ -515,7 +578,9 @@ test.describe("Limited backoffice", () => {
|
|||||||
await page.goto("/backoffice/departments/1/prices");
|
await page.goto("/backoffice/departments/1/prices");
|
||||||
|
|
||||||
await expect(page.getByTestId("limited-prices-title")).toBeVisible();
|
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("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-backoffice-department-select")).not.toContainText("Other Depot");
|
||||||
await expect(page.getByTestId("limited-price-row-101")).toContainText("Truck wash");
|
await expect(page.getByTestId("limited-price-row-101")).toContainText("Truck wash");
|
||||||
await expect(page.getByTestId("limited-price-input-101")).toHaveValue("125");
|
await expect(page.getByTestId("limited-price-input-101")).toHaveValue("125");
|
||||||
@@ -694,11 +759,16 @@ test.describe("Limited backoffice", () => {
|
|||||||
await seedLimitedBackofficeSession(page);
|
await seedLimitedBackofficeSession(page);
|
||||||
const api = await mockLimitedBackofficeApi(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 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-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-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.locator("#limited-employee-role option")).toHaveCount(5);
|
||||||
await expect(page.getByTestId("limited-employee-role")).not.toContainText("Superuser");
|
await expect(page.getByTestId("limited-employee-role")).not.toContainText("Superuser");
|
||||||
await expect(page.locator("body")).not.toContainText("department_access_1");
|
await expect(page.locator("body")).not.toContainText("department_access_1");
|
||||||
@@ -708,6 +778,49 @@ test.describe("Limited backoffice", () => {
|
|||||||
expect(api.forbiddenCalls).toEqual([]);
|
expect(api.forbiddenCalls).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("generates an employee QR login link", async ({ page }, testInfo) => {
|
||||||
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
||||||
|
|
||||||
|
await seedLimitedBackofficeSession(page);
|
||||||
|
const api = await mockLimitedBackofficeApi(page);
|
||||||
|
|
||||||
|
await page.goto("/backoffice/employees");
|
||||||
|
|
||||||
|
await page.getByTestId("limited-employee-login-link-501").click();
|
||||||
|
|
||||||
|
const modal = page.getByTestId("limited-login-link-modal");
|
||||||
|
await expect(modal).toBeVisible();
|
||||||
|
await expect(modal).toContainText("Employee login link");
|
||||||
|
await expect(modal).toContainText("Casey Clerk");
|
||||||
|
await expect(page.getByTestId("limited-login-link-qr")).toHaveAttribute("src", /^data:image\/png;base64,/);
|
||||||
|
await expect(page.getByTestId("limited-login-link-url")).toHaveValue(/\/login\/qr\?token=a{64}$/);
|
||||||
|
|
||||||
|
await page.getByTestId("limited-login-link-copy").click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId("limited-login-link-copy-message")).toHaveText("Link copied.");
|
||||||
|
expect(api.loginLinkCalls).toEqual(["501"]);
|
||||||
|
expect(api.forbiddenCalls).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows an error when employee QR login link generation fails", async ({ page }, testInfo) => {
|
||||||
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
||||||
|
|
||||||
|
await seedLimitedBackofficeSession(page);
|
||||||
|
const api = await mockLimitedBackofficeApi(page, sessionData, {
|
||||||
|
loginLinkMessage: "Missing employee management permission.",
|
||||||
|
loginLinkStatus: 403,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/backoffice/employees");
|
||||||
|
|
||||||
|
await page.getByTestId("limited-employee-login-link-501").click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId("limited-employees-error")).toHaveText("Missing employee management permission.");
|
||||||
|
await expect(page.getByTestId("limited-login-link-modal")).toHaveCount(0);
|
||||||
|
expect(api.loginLinkCalls).toEqual(["501"]);
|
||||||
|
expect(api.forbiddenCalls).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test("shows grouped human-readable role permission help without raw permission keys", async ({ page }, testInfo) => {
|
test("shows grouped human-readable role permission help without raw permission keys", async ({ page }, testInfo) => {
|
||||||
await seedLimitedBackofficeSession(page);
|
await seedLimitedBackofficeSession(page);
|
||||||
await mockLimitedBackofficeApi(page);
|
await mockLimitedBackofficeApi(page);
|
||||||
@@ -766,15 +879,14 @@ test.describe("Limited backoffice", () => {
|
|||||||
await expect(page.getByTestId("limited-employees-title")).toBeVisible();
|
await expect(page.getByTestId("limited-employees-title")).toBeVisible();
|
||||||
await expect(page.getByTestId("limited-employee-save")).toBeDisabled();
|
await expect(page.getByTestId("limited-employee-save")).toBeDisabled();
|
||||||
await expect(page.getByTestId("limited-employee-save")).toHaveClass(/is-fullwidth/);
|
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 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-name").fill("No Phone Worker");
|
||||||
await page.getByTestId("limited-employee-email").fill("no-phone@example.com");
|
await page.getByTestId("limited-employee-email").fill("no-phone@example.com");
|
||||||
await page.getByTestId("limited-employee-password").fill("Secret123!");
|
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 expect(page.getByTestId("limited-employee-save")).toBeEnabled();
|
||||||
await page.getByTestId("limited-employee-save").click();
|
await page.getByTestId("limited-employee-save").click();
|
||||||
|
|
||||||
@@ -788,14 +900,13 @@ test.describe("Limited backoffice", () => {
|
|||||||
role_key: "viewer",
|
role_key: "viewer",
|
||||||
department_ids: [1],
|
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-name").fill("Phone Worker");
|
||||||
await page.getByTestId("limited-employee-email").fill("phone@example.com");
|
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-country-code").selectOption("358");
|
||||||
await page.getByTestId("limited-employee-phone").fill("87654321");
|
await page.getByTestId("limited-employee-phone").fill("87654321");
|
||||||
await page.getByTestId("limited-employee-password").fill("Secret123!");
|
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 expect(page.getByTestId("limited-employee-save")).toBeEnabled();
|
||||||
await page.getByTestId("limited-employee-save").click();
|
await page.getByTestId("limited-employee-save").click();
|
||||||
|
|
||||||
@@ -809,8 +920,8 @@ test.describe("Limited backoffice", () => {
|
|||||||
role_key: "viewer",
|
role_key: "viewer",
|
||||||
department_ids: [1],
|
department_ids: [1],
|
||||||
});
|
});
|
||||||
await expect(page.getByTestId("limited-employee-row-903")).toContainText("Phone Worker");
|
await expect(page.getByTestId("limited-employee-row-904")).toContainText("Phone Worker");
|
||||||
await expect(page.getByTestId("limited-employee-phone-903")).toHaveText("+358 87654321");
|
await expect(page.getByTestId("limited-employee-phone-904")).toHaveText("+358 87654321");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("edits employee contact details without requiring a new password", async ({ page }) => {
|
test("edits employee contact details without requiring a new password", async ({ page }) => {
|
||||||
@@ -844,19 +955,75 @@ test.describe("Limited backoffice", () => {
|
|||||||
await expect(page.getByTestId("limited-employee-phone-501")).toHaveText("+358 87654321");
|
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("does not render data for a department outside the manager scope", async ({ page }, testInfo) => {
|
||||||
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
||||||
|
|
||||||
await seedLimitedBackofficeSession(page);
|
await seedLimitedBackofficeSession(page);
|
||||||
const api = await mockLimitedBackofficeApi(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.getByTestId("limited-prices-forbidden")).toBeVisible();
|
||||||
await expect(page.locator("body")).not.toContainText("Secret Depot");
|
await expect(page.locator("body")).not.toContainText("Secret Depot");
|
||||||
await expect(page.getByTestId("limited-prices-table")).toHaveCount(0);
|
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([]);
|
expect(api.forbiddenCalls).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user