Compare commits

..
Author SHA1 Message Date
Jeppe Bundgaard 7180bbd6d4 Add limited backoffice role template actions 2026-07-07 03:23:32 +02:00
Jeppe BandJeppe Bundgaard 96ccc01d08 Add limited backoffice employee migration (#164)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-07 00:14:05 +02:00
Jeppe Bundgaard 2108dd6c94 Send Selvvask import period dates
Include selected invoice-period dates when importing XLVask usage from the Selvvask period view.
2026-07-06 23:49:36 +02:00
Jeppe BandJeppe Bundgaard ca2003f2d6 Allow hosted runners for manual frontend tests (#161)
* Allow hosted runners for manual frontend tests

* Fix hosted full E2E i18n failures

* Stabilize release assignment E2E selection

* Gate full E2E on focused regressions

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 22:38:29 +02:00
Jeppe BandJeppe Bundgaard acbbac588f [codex] disable invoice downloads until invoices exist (#158)
* disable invoice downloads until invoices exist

* enable github-hosted frontend ci runners

* Update POS invoice download visual expectations

* Fix full E2E i18n and self-serve coverage

* Review dynamic view i18n keys

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 22:35:58 +02:00
Jeppe B f75ff97042 Merge pull request #139 from copenhagentruckwash/codex/backoffice-title-department-selector
[codex] Move backoffice department selector into title
2026-07-06 22:35:13 +02:00
Jeppe Bundgaard 4aa7af9716 Merge remote-tracking branch 'origin/master' into codex/backoffice-title-department-selector
# Conflicts:
#	.github/workflows/tests.yml
2026-07-06 22:34:43 +02:00
Jeppe BandJeppe Bundgaard d2682da3cc [codex] Improve admin notifications page UI (#145)
* Improve admin notifications page UI

* Stabilize notifications page E2E bootstrap wait

* Fix PR Playwright diff head ref

* Guard MyWashStart timers after test teardown

* Stabilize self-serve wash E2E timing

* Refresh edge gateway fixture runtime state

* Run Playwright E2E on GitHub-hosted runners

* Run all frontend CI on GitHub-hosted runners

* Increase hosted full E2E parallelism

* Stabilize full E2E validation

* Preserve superuser gateway navigation label

* Add targeted Playwright dispatch workflow

* Integrate targeted Playwright dispatch into tests workflow

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 22:33:40 +02:00
Jeppe Bundgaard aa1d2ab623 Merge remote-tracking branch 'origin/master' into codex/backoffice-title-department-selector
# Conflicts:
#	.github/workflows/tests.yml
#	src/i18n/generated/de-v2.json
#	src/i18n/generated/global-v2.json
#	src/i18n/generated/no-v2.json
#	src/i18n/generated/sv-v2.json
#	src/i18n/source/de/phrases/compat/departments/index.json
#	src/i18n/source/de/phrases/compat/objects/index.json
#	src/i18n/source/de/phrases/compat/vehicles/index.json
#	src/i18n/source/global/shared/superuser_dashboard/index.json
#	src/i18n/source/no/phrases/compat/departments/index.json
#	src/i18n/source/no/phrases/compat/objects/index.json
#	src/i18n/source/no/phrases/compat/vehicles/index.json
#	src/i18n/source/sv/phrases/compat/objects/index.json
#	src/i18n/source/sv/phrases/compat/vehicles/index.json
#	tests/e2e/self-serve-wash.spec.js
#	tests/unit/playwright-full-workflow.spec.js
#	tests/unit/playwright-pr-mapping.spec.js
2026-07-06 22:33:27 +02:00
Jeppe Bundgaard 929333d264 Add focused E2E dispatch scope 2026-07-06 22:21:30 +02:00
Jeppe B d2cbf823d8 Fix action wheel menu placement stability
Stabilizes the action settings wheel dropdown placement so the menu does not flip above/below after opening, and adds regression coverage plus hosted full-matrix workflow support.
2026-07-06 22:15:46 +02:00
Jeppe Bundgaard ba580e43e6 Align backoffice selector with title 2026-07-06 21:34:44 +02:00
Jeppe Bundgaard 3c49cec213 Review dynamic i18n view keys 2026-07-06 20:47:51 +02:00
Jeppe B c237fce38d Fix null product query parameters
Fixes product detail requests so null optional query params are omitted while final_price=false is preserved.
2026-07-06 20:14:45 +02:00
Jeppe Bundgaard 8acecc61cd Fix hosted full E2E catalog failures 2026-07-06 20:12:12 +02:00
Jeppe B 63149b7597 Merge pull request #162 from copenhagentruckwash/codex/daily-report-product-targets-ui
[codex] Add daily report product target controls
2026-07-06 19:36:03 +02:00
Jeppe Bundgaard a331eeadcb Speed up hosted full E2E dispatch 2026-07-06 19:27:37 +02:00
Jeppe Bundgaard 33e109b131 Fix rebased limited backoffice home handler 2026-07-06 19:27:37 +02:00
Jeppe Bundgaard 68b4328c72 Allow manual test runs on GitHub-hosted runners 2026-07-06 19:27:35 +02:00
Jeppe Bundgaard dd8a0a1952 Move backoffice department selector into title 2026-07-06 19:27:05 +02:00
Jeppe Bundgaard b58c1eb9ec Add daily report product target controls 2026-07-06 19:10:50 +02:00
Jeppe B 27994d6ac6 Merge pull request #159 from copenhagentruckwash/codex/superuser-user-overview-hub
[codex] Finish superuser user overview hub
2026-07-06 18:57:04 +02:00
Jeppe Bundgaard 9d6978e9c7 Merge remote-tracking branch 'origin/master' into codex/superuser-user-overview-hub
# Conflicts:
#	.github/workflows/tests.yml
2026-07-06 18:38:40 +02:00
Jeppe Bundgaard 62ae4bc890 [codex] Move PR E2E to hosted runners 2026-07-06 18:33:44 +02:00
Jeppe B c2ce6a859b Merge pull request #150 from copenhagentruckwash/codex/customer-product-fixed-price-overrides
Add customer product fixed price UI
2026-07-06 18:15:31 +02:00
Jeppe BandJeppe Bundgaard aa5e0a5507 Add EAN to customer registration flows (#147)
* Add EAN to economic customer registration

* Focus EAN PR Playwright mapping

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 18:12:05 +02:00
Jeppe Bundgaard 6c1c1def78 Add limited backoffice employee QR login links 2026-07-06 17:32:58 +02:00
Jeppe Bundgaard 1a232183a8 Merge master into fixed price override branch 2026-07-06 17:28:58 +02:00
Jeppe Bundgaard cfa7d5be69 Merge remote-tracking branch 'origin/master' into codex/superuser-user-overview-hub
# Conflicts:
#	.github/workflows/tests.yml
#	scripts/playwright-pr-mapping.mjs
#	src/views/dashboards/userDashboard/wash/MyWashStart.vue
#	tests/unit/playwright-pr-mapping.spec.js
2026-07-06 17:12:45 +02:00
Jeppe B bb375eb149 Merge pull request #144 from copenhagentruckwash/codex/fix-invoicing-period-selfwash
Fix invoicing period Selvvask counts
2026-07-06 17:08:06 +02:00
Jeppe B 7e5e6ce680 Enforce only tankcleaning POS product rules (#151) 2026-07-06 17:07:03 +02:00
Jeppe Bundgaard 1032f8b4f1 Show limited backoffice employees in backoffice 2026-07-06 16:59:46 +02:00
Jeppe B 744a56c8b2 Merge PR #157: Cleanup superuser dashboard tabs
Cleanup superuser dashboard tabs
2026-07-06 16:59:27 +02:00
Jeppe Bundgaard e58372a190 Merge remote-tracking branch 'origin/master' into codex/pr157-merge-resolve-692380
# Conflicts:
#	scripts/playwright-pr-mapping.mjs
#	src/i18n/generated/da-v2.json
#	src/i18n/generated/de-v2.json
#	src/i18n/generated/en-v2.json
#	src/i18n/generated/no-v2.json
#	src/i18n/generated/sv-v2.json
#	src/i18n/source/da/phrases/index.json
#	src/i18n/source/de/phrases/index.json
#	src/i18n/source/en/phrases/index.json
#	src/i18n/source/no/phrases/index.json
#	src/i18n/source/sv/phrases/index.json
#	src/views/dashboards/userDashboard/wash/MyWashStart.vue
#	tests/unit/playwright-pr-mapping.spec.js
2026-07-06 16:59:00 +02:00
Jeppe Bundgaard 19d44b8666 Add limited backoffice user ID translations 2026-07-06 16:53:41 +02:00
Jeppe Bundgaard 92a52d6194 Gate subuser customer navigation and actions 2026-07-06 16:48:24 +02:00
Jeppe BandJeppe Bundgaard db1c0e25c0 Move limited backoffice shortcut into header (#140)
* Move limited backoffice shortcut into header

* Use shared Playwright port locks in CI

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 16:47:33 +02:00
Jeppe Bundgaard 04860a7304 Deduplicate limited backoffice price products 2026-07-06 16:34:10 +02:00
Jeppe B d8d67301ed Add autosave for limited backoffice prices (#143) 2026-07-06 16:26:46 +02:00
Jeppe Bundgaard fa1e915b9c Merge origin/master into Selvvask period fix 2026-07-06 16:07:05 +02:00
Jeppe BandJeppe Bundgaard da86b69b2c Warn before invoicing multiple months together (#155)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 16:02:10 +02:00
Jeppe B 44e3167278 Build superuser department overview page (#156)
Build the superuser department overview page and align PR changed-E2E selection with the PR head ref.
2026-07-06 16:01:26 +02:00
Jeppe B 5cc22f14bc Fix date-only selection handling
Use local date-only parsing and formatting across date inputs and filters so clicked dates persist exactly.\n\nVerified with local format, lint, i18n, build, full unit tests, PR E2E, focused date E2E, broad smoke reruns, and GitHub PR checks.
2026-07-06 15:52:19 +02:00
Jeppe B 8519af5cbb Merge pull request #141 from copenhagentruckwash/codex/custom-pricing-only-departments
Add custom-only department pricing toggle
2026-07-06 15:31:41 +02:00
Jeppe BandJeppe Bundgaard 3fc4be8335 Update limited backoffice employee form (#146)
* Update limited backoffice employee form

* Map limited backoffice changes to focused E2E

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 15:31:16 +02:00
Jeppe Bundgaard 5a5cc0bed3 Guard MyWashStart timers after teardown 2026-07-06 15:02:57 +02:00
Jeppe Bundgaard 1a9e7503ef Stabilize PR Playwright runner load 2026-07-06 15:01:01 +02:00
Jeppe Bundgaard d84e9e3406 Map superuser dashboard changes to E2E 2026-07-06 14:56:18 +02:00
Jeppe Bundgaard c41954993c Fix PR E2E changed diff head 2026-07-06 14:54:41 +02:00
Jeppe Bundgaard a7c8fc3bff Reduce PR E2E runner pressure 2026-07-06 14:52:30 +02:00
Jeppe Bundgaard 36c1f10de8 Reduce PR E2E runner pressure 2026-07-06 14:47:03 +02:00
Jeppe Bundgaard 6002f97fb6 Finish superuser user overview hub 2026-07-06 14:42:23 +02:00
Jeppe Bundgaard 4378dad2b9 Stabilize PR E2E runner checks 2026-07-06 14:39:06 +02:00
Jeppe Bundgaard b8959cb7bb Cleanup superuser dashboard tabs 2026-07-06 14:07:36 +02:00
Jeppe Bundgaard ee41366c08 Bound changed PR E2E selection 2026-07-06 13:57:06 +02:00
Jeppe Bundgaard 3f42b77d13 Stabilize invoice distribution smoke session setup 2026-07-06 13:48:48 +02:00
Jeppe Bundgaard e22f78a449 Stabilize unit timer cleanup 2026-07-06 13:06:41 +02:00
Jeppe Bundgaard f2b66ff19f Stabilize recent wash refresh timers in tests 2026-07-06 13:05:01 +02:00
Jeppe Bundgaard 2fd0893773 Add customer product fixed price UI 2026-07-06 12:52:31 +02:00
Jeppe Bundgaard 0e9292d6d9 Merge remote-tracking branch 'origin/master' into codex/custom-pricing-only-departments
# Conflicts:
#	scripts/playwright-pr-mapping.mjs
2026-07-06 12:48:54 +02:00
Jeppe Bundgaard 69250ada66 Stabilize self-serve restored lane smoke test 2026-07-06 12:44:26 +02:00
Jeppe Bundgaard ce844137a0 Harden POS order detail E2E wait 2026-07-06 12:18:20 +02:00
Jeppe Bundgaard 7f3a8c07e2 Stabilize edge gateway installer smoke tests 2026-07-06 11:47:58 +02:00
Jeppe Bundgaard bd14d58f08 Stabilize self-serve wash smoke assertion 2026-07-06 11:03:22 +02:00
Jeppe Bundgaard b8494cd1d9 Merge remote-tracking branch 'origin/master' into codex/custom-pricing-only-departments
# Conflicts:
#	tests/unit/playwright-pr-mapping.spec.js
2026-07-06 10:36:57 +02:00
Jeppe Bundgaard 1a1c3db8e5 Fix invoicing period selfwash counts 2026-07-06 10:33:48 +02:00
Jeppe Bundgaard 49772c1334 Trigger frontend PR checks 2026-07-06 10:30:43 +02:00
Jeppe Bundgaard 02ddb99000 Add custom-only department pricing toggle 2026-07-06 10:21:06 +02:00
212 changed files with 12545 additions and 1409 deletions
+260 -17
View File
@@ -6,6 +6,39 @@ on:
branches:
- master
workflow_dispatch:
inputs:
mode:
description: "What to run for a manual dispatch."
required: true
type: choice
default: full
options:
- full
- targeted
- targeted-then-full
target_specs:
description: "Comma- or newline-separated Playwright spec paths under tests/e2e."
required: false
type: string
default: "tests/e2e/superuser-department-overview.spec.js"
target_projects:
description: "JSON array of Playwright projects for targeted mode."
required: false
type: string
default: '["chromium-desktop","chromium-mobile","chromium-tablet","webkit-mobile","webkit-desktop"]'
target_grep:
description: "Optional Playwright grep pattern for targeted mode."
required: false
type: string
default: ""
runner:
description: "Runner pool for this manually dispatched test run"
required: false
default: "self-hosted"
type: choice
options:
- self-hosted
- github-hosted
schedule:
- cron: "0 2 * * *"
@@ -13,16 +46,22 @@ permissions:
contents: read
concurrency:
group: frontend-tests-${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}
group: frontend-tests-${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.head_ref || github.ref_name }}
cancel-in-progress: true
# Repository variables used as CI runner and credit controls:
# - FRONTEND_CI_STANDARD_RUNNER: JSON runs-on value for format/build/unit jobs.
# - FRONTEND_CI_E2E_RUNNER: JSON runs-on value for Playwright jobs.
# - FRONTEND_CI_PR_E2E_MAX_PARALLEL: numeric Playwright PR job parallelism.
# - FRONTEND_CI_FULL_E2E_MAX_PARALLEL: numeric full-suite job parallelism.
# GitHub-hosted example: ["ubuntu-22.04"], with PR parallelism 2 and full parallelism 1.
jobs:
format-tests:
# CI runs on the repository's self-hosted runner pool.
runs-on: [self-hosted, Linux, X64, pleno, frontend]
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
timeout-minutes: 15
steps:
- name: Repair self-hosted workspace permissions
if: ${{ contains(vars.FRONTEND_CI_STANDARD_RUNNER || 'self-hosted', 'self-hosted') }}
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
@@ -55,10 +94,11 @@ jobs:
build-and-unit:
needs: format-tests
runs-on: [self-hosted, Linux, X64, pleno, frontend]
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
timeout-minutes: 30
steps:
- name: Repair self-hosted workspace permissions
if: ${{ contains(vars.FRONTEND_CI_STANDARD_RUNNER || 'self-hosted', 'self-hosted') }}
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
@@ -97,23 +137,206 @@ jobs:
env:
VITEST_BATCH_SIZE: 5
e2e-pr:
if: github.event_name != 'schedule'
e2e-targeted:
if: >
github.event_name == 'workflow_dispatch' &&
(inputs.mode == 'targeted' || inputs.mode == 'targeted-then-full')
needs: build-and-unit
name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }}
runs-on: [self-hosted, Linux, X64, pleno, frontend, docker]
timeout-minutes: 30
name: E2E-targeted-${{ matrix.project }}
# Use GitHub-hosted runners to avoid self-hosted desktop contention and sleep/power events.
runs-on: ubuntu-24.04
timeout-minutes: 35
strategy:
fail-fast: false
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:
suite: [core, changed]
project: [chromium-desktop, chromium-mobile]
env:
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-pr-${{ matrix.suite }}-${{ matrix.project }}
PLAYWRIGHT_REPORTER_MODE: line-html
PLAYWRIGHT_WORKERS: 1
PLAYWRIGHT_VIDEO_MODE: on-first-retry
steps:
- name: Repair self-hosted workspace permissions
if: ${{ contains(vars.FRONTEND_CI_E2E_RUNNER || 'self-hosted', 'self-hosted') }}
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
@@ -140,20 +363,29 @@ jobs:
EVENT_NAME: ${{ github.event_name }}
HEAD_SHA: ${{ github.sha }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
zero_sha="0000000000000000000000000000000000000000"
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_BASE_SHA" ]]; then
base_ref="$PR_BASE_SHA"
head_ref="$PR_HEAD_SHA"
elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then
git fetch --no-tags --prune origin "$DEFAULT_BRANCH"
base_ref="origin/$DEFAULT_BRANCH"
head_ref="$HEAD_SHA"
else
base_ref="$PUSH_BEFORE_SHA"
head_ref="$HEAD_SHA"
fi
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_HEAD_SHA" ]]; then
head_ref="$PR_HEAD_SHA"
else
head_ref="$HEAD_SHA"
fi
echo "base=$base_ref" >> "$GITHUB_OUTPUT"
echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT"
echo "head=$head_ref" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
uses: actions/setup-node@v5
@@ -181,8 +413,9 @@ jobs:
*) echo "Unsupported Playwright PR project: $MATRIX_PROJECT" >&2; exit 1 ;;
esac
port_seed=$((20000 + (RUN_ID % 20000) + suite_offset + project_offset))
lock_root="${RUNNER_TEMP:-/tmp}/pleno-playwright-port-locks"
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=""
@@ -223,6 +456,8 @@ jobs:
--env CI="${CI:-}" \
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
--env MATRIX_SUITE="$MATRIX_SUITE" \
--env MATRIX_PROJECT="$MATRIX_PROJECT" \
@@ -277,15 +512,21 @@ jobs:
if: >
always() &&
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch) &&
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted') &&
needs.build-and-unit.result == 'success' &&
(github.event_name == 'schedule' || needs.e2e-pr.result == 'success')
needs: [build-and-unit, e2e-pr]
(github.event_name == 'schedule' || needs.e2e-pr.result == 'success') &&
(
github.event_name != 'workflow_dispatch' ||
inputs.mode == 'full' ||
needs.e2e-targeted.result == 'success'
)
needs: [build-and-unit, e2e-pr, e2e-targeted]
name: E2E-full-${{ matrix.browser_label }}-${{ matrix.device }}-${{ matrix.role }}
runs-on: [self-hosted, Linux, X64, pleno, frontend, docker]
runs-on: ${{ fromJSON(vars.FRONTEND_CI_E2E_RUNNER || '["self-hosted","Linux","X64","pleno","frontend","docker"]') }}
timeout-minutes: 60
strategy:
fail-fast: false
max-parallel: 2
max-parallel: ${{ fromJSON(vars.FRONTEND_CI_FULL_E2E_MAX_PARALLEL || '1') }}
matrix:
browser: [chromium, webkit, firefox]
device: [mobile, desktop, tablet]
@@ -307,6 +548,7 @@ jobs:
PLAYWRIGHT_VIDEO_MODE: off
steps:
- name: Repair self-hosted workspace permissions
if: ${{ contains(vars.FRONTEND_CI_E2E_RUNNER || 'self-hosted', 'self-hosted') }}
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
@@ -357,8 +599,9 @@ jobs:
*) echo "Unsupported Playwright device: $MATRIX_DEVICE" >&2; exit 1 ;;
esac
port_seed=$((20000 + (RUN_ID % 20000) + role_offset + browser_offset + device_offset))
lock_root="${RUNNER_TEMP:-/tmp}/pleno-playwright-port-locks"
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=""
+12
View File
@@ -2415,6 +2415,12 @@ paths:
type: string
description: Contact person name
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:
type: string
description: reCAPTCHA verification token
@@ -8189,6 +8195,12 @@ paths:
email: {type: string}
phone: {type: integer}
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:
'200':
description: Success
+85 -4
View File
@@ -7,14 +7,18 @@ export const fallbackChangePatterns = [
/^vite\.config\.js$/u,
/^playwright(?:\..+)?\.config\.(?:js|ts)$/u,
/^playwright\.global-(?:setup|teardown)\.mjs$/u,
/^scripts\/run-playwright-(?:pr|ci-parallel|batched-chromium)\.mjs$/u,
/^scripts\/run-playwright-(?:ci-parallel|batched-chromium)\.mjs$/u,
/^tests\/e2e\/(?:support|fixtures)\//u,
];
export const sourceMappings = [
{
name: "auth",
patterns: [/^src\/(?:views|components|middleware)\/.*auth/iu, /^src\/views\/auth\//u, /^src\/components\/session\//u],
patterns: [
/^src\/(?:views|components|middleware)\/.*auth/iu,
/^src\/views\/auth\//u,
/^src\/components\/session\/(?!token\/SessionUser\/Objects\/)/u,
],
specs: ["tests/e2e/auth.smoke.spec.js", "tests/e2e/userAuth.spec.ts"],
projects: chromiumProjects,
},
@@ -24,6 +28,12 @@ export const sourceMappings = [
specs: ["tests/e2e/navigation.smoke.spec.js"],
projects: chromiumProjects,
},
{
name: "limited-backoffice",
patterns: [/^src\/views\/backoffice\//u, /^src\/services\/limitedBackoffice\.js$/u],
specs: ["tests/e2e/limited-backoffice.spec.ts"],
projects: chromiumProjects,
},
{
name: "superuser-roles-permissions",
patterns: [
@@ -33,6 +43,40 @@ export const sourceMappings = [
specs: ["tests/e2e/superuser-roles-permissions.spec.ts"],
projects: chromiumProjects,
},
{
name: "superuser-users",
patterns: [/^src\/views\/dashboards\/superUserDashboard\/user\//u],
specs: ["tests/e2e/superuser-users.spec.ts"],
projects: chromiumProjects,
},
{
name: "superuser-dashboard",
patterns: [
/^src\/views\/dashboards\/superUserDashboard\/SuperUserDashboard(?:Navigation)?\.vue$/u,
/^src\/components\/displays\/superuser\/system\//u,
],
specs: ["tests/e2e/superuser-system-status.smoke.spec.js"],
projects: chromiumProjects,
},
{
name: "superuser-department-overview",
patterns: [
/^src\/services\/superuserDepartmentOverview\.js$/u,
/^src\/views\/dashboards\/superUserDashboard\/department\/Department\.vue$/u,
/^src\/views\/dashboards\/superUserDashboard\/department\/SuperUserDashboardDepartmentNavigation\.vue$/u,
],
specs: ["tests/e2e/superuser-department-overview.spec.js"],
projects: chromiumProjects,
},
{
name: "header-navigation",
patterns: [
/^src\/components\/viewport\/page\/headers\//u,
/^src\/components\/models\/navigation\/items\/NavigationMenuItemsGlobal\.vue$/u,
],
specs: ["tests/e2e/navigation.smoke.spec.js", "tests/e2e/limited-backoffice.spec.ts"],
projects: chromiumProjects,
},
{
name: "booking",
patterns: [/bookings?/iu, /time-bookings/iu, /^src\/views\/guest\/book\//u],
@@ -51,8 +95,20 @@ export const sourceMappings = [
},
{
name: "pos",
patterns: [/\/pos[/-]/iu, /POS/iu, /^src\/assets\/pos\.css$/u],
specs: ["tests/e2e/pos-flow.spec.js", "tests/e2e/pos-mobile-order-flow.spec.js", "tests/e2e/admin-pos-orders.spec.ts"],
patterns: [
/(?:^|[/_.-])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,
},
{
@@ -64,12 +120,28 @@ export const sourceMappings = [
{
name: "admin-department-notifications",
patterns: [
/^src\/views\/dashboards\/departmentDashboard\/modules\/notifications\/DepartmentNotifications\.vue$/u,
/^src\/components\/displays\/department\/notifications\//u,
/^src\/components\/displays\/pagination\/models\/DepartmentPos\/NotificationsPhonePagination\.vue$/u,
],
specs: ["tests/e2e/admin-department-notifications.spec.ts"],
projects: chromiumProjects,
},
{
name: "admin-daily-report",
patterns: [
/^src\/views\/dashboards\/departmentDashboard\/modules\/daily-report\//u,
/^src\/components\/session\/token\/SessionUser\/Objects\/DepartmentDailyReports\.vue$/u,
],
specs: ["tests/e2e/admin-daily-report.spec.ts"],
projects: chromiumProjects,
},
{
name: "limited-backoffice",
patterns: [/^src\/views\/backoffice\/LimitedBackoffice/u],
specs: ["tests/e2e/limited-backoffice.spec.ts"],
projects: chromiumProjects,
},
{
name: "invoicing",
patterns: [/invoic/iu, /economic[-/]?queue/iu, /collected-order/iu],
@@ -92,6 +164,15 @@ export const sourceMappings = [
specs: ["tests/e2e/superuser-system-status.smoke.spec.js"],
projects: chromiumProjects,
},
{
name: "superuser-department-pricing",
patterns: [
/^src\/views\/dashboards\/superUserDashboard\/department\/(?:DepartmentPricing|SuperUserSelectedDepartmentObject)\.vue$/u,
/^src\/components\/session\/token\/SessionUser\/Objects\/Departments\.vue$/u,
],
specs: ["tests/e2e/superuser-department-pricing-custom-only.spec.ts"],
projects: ["chromium-desktop"],
},
{
name: "self-serve",
patterns: [/self[-/]?serve/iu, /selfserve/iu, /wash\/MyWash/iu],
+2
View File
@@ -101,8 +101,10 @@ export const ownedFilesByRole = {
"superuser-customer-complaints.spec.ts",
"superuser-customers-mass-import.spec.ts",
"superuser-department-branding.spec.js",
"superuser-department-overview.spec.js",
"superuser-department-gates.spec.ts",
"superuser-department-lanes.spec.ts",
"superuser-department-pricing-custom-only.spec.ts",
"superuser-departments-archive.spec.ts",
"superuser-drafts.spec.ts",
"superuser-products-layout.spec.ts",
+24 -4
View File
@@ -261,6 +261,8 @@ function selectChangedTests(changedFiles) {
specProjects: new Map(),
mappedFiles: [],
unmappedFiles: [],
directSpecFiles: [],
skippedDirectSpecFiles: [],
fallback: false,
};
@@ -270,8 +272,7 @@ function selectChangedTests(changedFiles) {
const file = normalizePath(rawFile);
if (isE2eSpec(file)) {
addSpec(selection, file, selectedProjects);
selection.mappedFiles.push(file);
selection.directSpecFiles.push(file);
continue;
}
@@ -295,13 +296,26 @@ function selectChangedTests(changedFiles) {
selection.mappedFiles.push(file);
for (const mapping of matches) {
const projects = mapping.projects.filter((project) => selectedProjects.includes(project));
const mappedProjects = mapping.projects.length > 0 ? mapping.projects : selectedProjects;
const projects = mappedProjects.filter((project) => selectedProjects.includes(project));
if (projects.length === 0) {
continue;
}
for (const spec of mapping.specs) {
addSpec(selection, spec, projects.length > 0 ? projects : selectedProjects);
addSpec(selection, spec, projects);
}
}
}
if (selection.specProjects.size === 0 && !selection.fallback) {
for (const file of selection.directSpecFiles) {
addSpec(selection, file, selectedProjects);
selection.mappedFiles.push(file);
}
} else {
selection.skippedDirectSpecFiles.push(...selection.directSpecFiles);
}
return selection;
}
@@ -370,6 +384,12 @@ async function runChangedSelection(selection) {
return 0;
}
if (selection.skippedDirectSpecFiles.length > 0) {
console.log(
`[playwright-pr] Source mappings selected changed-area specs; direct E2E file edits are covered by mapped/core gates: ${selection.skippedDirectSpecFiles.join(", ")}`
);
}
for (const [index, group] of groups.entries()) {
for (const project of group.projects) {
const code = await runPlaywright({
+11 -45
View File
@@ -6,6 +6,7 @@ import Swal from "sweetalert2";
import CustomerProductDiscountDisplay
from "@/components/displays/department/pos/displays/CustomerProductDiscountDisplay.vue";
import { useI18n } from 'vue-i18n';
import { isProductRestrictedForCustomer } from "@/features/customer/customerProductRules.js";
const { t } = useI18n();
const expandIcon = ref(null);
@@ -83,23 +84,6 @@ const hasRestrictAdditionalServices = () => {
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
* This is used to check if adding a product has any requirements.
@@ -162,37 +146,19 @@ const doesAddonHaveNoteRequirement = (addon) => {
};
const isAddonRestricted = (addon) => {
const addonName = getAddonName(addon).toLowerCase();
// Check if the addon has "Spot Free" in the name
if (hasRestrictSpotFree() && addonName.includes('spot free')) {
return true;
}
// Check if the addon has "Indvendig vask" in the name
if (hasRestrictInteriorCleaning() && addonName.includes('indvendig vask')) {
return true;
}
const addonProduct = getAddonProduct(addon);
return isProductRestrictedForCustomer({
...addonProduct,
name: getAddonName(addon) || addonProduct.name,
category: addonProduct.category ?? addon?.category,
}, customerAttributesSafe.value);
};
const isProductRestricted = () => {
const productName = String(props.name || '').toLowerCase();
// Check if the product has "Spot Free" in the name
if (hasRestrictSpotFree() && productName.includes('spot free')) {
return true;
}
// 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;
}
}
return isProductRestrictedForCustomer({
...props.product,
name: props.name || props.product?.name,
}, customerAttributesSafe.value);
};
const orderByOrderPriority = (addons) => {
@@ -126,6 +126,8 @@ const dropdownRoot = ref(null);
const dropdownContent = ref(null);
const isDropdownOpen = ref(false);
const dropdownInstanceId = Math.random().toString(36).substring(2, 15);
const dropdownPlacement = ref("bottom");
const isDropdownPlacementLocked = ref(false);
const shouldOpenDropdownUp = ref(false);
const dropdownMaxHeight = ref(null);
const isDesktopFlyoutLayout = ref(false);
@@ -140,6 +142,7 @@ const previewRequestsInFlight = new Set();
const generatedObjectUrls = new Set();
let previewStateGeneration = 0;
let contentResizeObserver = null;
let dropdownLayoutUpdateId = 0;
const desktopFlyoutMinViewportWidth = 1400;
const desktopFlyoutRootPanelWidthRem = 15;
const desktopFlyoutSubmenuWidthRem = 17;
@@ -230,6 +233,8 @@ const dropdownContentStyle = computed(() => {
});
const resetDropdownLayout = () => {
dropdownPlacement.value = "bottom";
isDropdownPlacementLocked.value = false;
shouldOpenDropdownUp.value = false;
dropdownMaxHeight.value = null;
isFixedPosition.value = false;
@@ -291,7 +296,35 @@ const getViewportInsets = () => {
};
};
const getDropdownPlacementSpaces = (triggerRect, viewportInsets = getViewportInsets()) => ({
bottom: Math.max(window.innerHeight - triggerRect.bottom - viewportInsets.bottom, 0),
top: Math.max(triggerRect.top - viewportInsets.top, 0),
});
const resolveDropdownPlacement = (triggerRect, menuHeight, viewportInsets = getViewportInsets()) => {
const spaces = getDropdownPlacementSpaces(triggerRect, viewportInsets);
return menuHeight > spaces.bottom && spaces.top > spaces.bottom ? "top" : "bottom";
};
const getDropdownAvailableHeight = (triggerRect, viewportInsets = getViewportInsets()) => {
const spaces = getDropdownPlacementSpaces(triggerRect, viewportInsets);
return dropdownPlacement.value === "top" ? spaces.top : spaces.bottom;
};
const lockDropdownPlacement = (triggerRect, menuHeight, viewportInsets = getViewportInsets()) => {
if (!isDropdownPlacementLocked.value) {
dropdownPlacement.value = resolveDropdownPlacement(triggerRect, menuHeight, viewportInsets);
isDropdownPlacementLocked.value = true;
}
shouldOpenDropdownUp.value = dropdownPlacement.value === "top";
};
const updateDropdownLayout = async () => {
const updateId = ++dropdownLayoutUpdateId;
if (!isDropdownOpen.value || !dropdownRoot.value || !dropdownContent.value) {
resetDropdownLayout();
return;
@@ -299,35 +332,41 @@ const updateDropdownLayout = async () => {
await nextTick();
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownRoot.value || !dropdownContent.value) {
return;
}
const triggerElement = dropdownRoot.value.querySelector(".dropdown-trigger");
const triggerRect = (triggerElement ?? dropdownRoot.value).getBoundingClientRect();
syncDesktopFlyoutState(triggerRect);
syncDesktopFlyoutPosition();
await nextTick();
if (!dropdownContent.value) {
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownContent.value) {
return;
}
const viewportInsets = getViewportInsets();
const viewportTop = viewportInsets.top;
const viewportBottom = window.innerHeight - viewportInsets.bottom;
const spaceBelow = Math.max(window.innerHeight - triggerRect.bottom - viewportInsets.bottom, 0);
const spaceAbove = Math.max(triggerRect.top - viewportInsets.top, 0);
const menuHeight = Math.ceil(dropdownContent.value.scrollHeight);
const openUpward = menuHeight > spaceBelow && spaceAbove > spaceBelow;
const availableHeight = openUpward ? spaceAbove : spaceBelow;
lockDropdownPlacement(triggerRect, menuHeight, viewportInsets);
const availableHeight = getDropdownAvailableHeight(triggerRect, viewportInsets);
const nextMaxHeight = availableHeight > 0 && menuHeight > availableHeight ? Math.floor(availableHeight) : null;
shouldOpenDropdownUp.value = openUpward;
dropdownMaxHeight.value = nextMaxHeight;
await nextTick();
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownContent.value) {
return;
}
syncDesktopFlyoutPosition();
await nextTick();
if (!dropdownContent.value) {
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value || !dropdownContent.value) {
return;
}
@@ -342,6 +381,12 @@ const updateDropdownLayout = async () => {
const boundedHeight = Math.floor(renderedMenuRect.height - overflowAbove - overflowBelow);
dropdownMaxHeight.value = boundedHeight > 0 ? boundedHeight : 1;
await nextTick();
if (updateId !== dropdownLayoutUpdateId || !isDropdownOpen.value) {
return;
}
syncDesktopFlyoutPosition();
};
@@ -1465,7 +1510,7 @@ onMounted(() => {
contentResizeObserver = new ResizeObserver(() => {
if (isDropdownOpen.value) {
syncDesktopFlyoutPosition();
void updateDropdownLayout();
}
});
@@ -2399,7 +2444,7 @@ watch(
() => {
if (isDropdownOpen.value) {
void nextTick(() => {
syncDesktopFlyoutPosition();
void updateDropdownLayout();
});
}
}
@@ -2507,7 +2552,6 @@ const syncDesktopFlyoutPosition = () => {
const triggerRect = triggerEl.getBoundingClientRect();
const contentHeight = dropdownContentEl.scrollHeight;
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
const contentRect = dropdownContentEl.getBoundingClientRect();
@@ -2537,12 +2581,13 @@ const syncDesktopFlyoutPosition = () => {
const viewportInsets = getViewportInsets();
const viewportTop = viewportInsets.top;
const viewportBottom = viewportHeight - viewportInsets.bottom;
let top = triggerRect.bottom;
if (top + contentHeight > viewportBottom) {
top = Math.max(viewportTop, triggerRect.top - contentHeight);
}
const effectiveContentHeight = dropdownMaxHeight.value
? Math.min(contentHeight, dropdownMaxHeight.value)
: contentHeight;
const top =
dropdownPlacement.value === "top"
? Math.max(viewportTop, triggerRect.top - effectiveContentHeight)
: triggerRect.bottom;
const nextFixedStyles = {
top: `${top}px`,
@@ -2558,18 +2603,8 @@ const syncDesktopFlyoutPosition = () => {
fixedPositionStyles.value = nextFixedStyles;
}
} else {
const contentRect = dropdownContentEl.getBoundingClientRect();
const contentBottom = contentRect.top + contentRect.height;
if (contentBottom > viewportHeight) {
const nextTop = `${triggerRect.top - contentRect.height}px`;
if (dropdownContentEl.style.top !== nextTop) {
dropdownContentEl.style.top = nextTop;
}
} else {
if (dropdownContentEl.style.top !== "") {
dropdownContentEl.style.top = "";
}
if (dropdownContentEl.style.top !== "") {
dropdownContentEl.style.top = "";
}
}
};
@@ -24,8 +24,24 @@ const onCustomerChange = () => {
isCustomerSelected.value = false;
}
const parseValue = (value) => {
let tmp_value = parseFloat(value).toFixed(2);
const parseFixedPriceValue = (value) => {
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
return `${tmp_value}%`;
}
@@ -78,7 +94,7 @@ watch(customer_id, onCustomerChange, { immediate: true });
<!--<span class="icon is-small mr-1">
<i class="fas fa-percent" aria-hidden="true"></i>
</span> -->
<span>{{ parseValue(discount.percentage) }}</span>
<span>{{ parseValue(discount) }}</span>
</div>
</div>
</div>
@@ -86,4 +102,4 @@ watch(customer_id, onCustomerChange, { immediate: true });
<style scoped>
</style>
</style>
@@ -29,6 +29,8 @@ const discount_product = ref(null);
const discount_category = ref(null);
// The global discount (if any)
const discount_global = ref(null);
// The fixed product price (if any)
const fixed_product_price = ref(null);
// Show the discounts dropdown
const showDiscountsDropdown = ref(false);
@@ -49,6 +51,19 @@ const hasGlobalDiscount = () => {
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
const setProductDiscount = (percentage) => {
// Check if the percentage is a number
@@ -83,6 +98,16 @@ const setGlobalDiscount = (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
const getDiscountDebug = () => {
@@ -101,9 +126,15 @@ const getDiscountDebug = () => {
// Parse the customer discounts
const parseCustomerDiscounts = () => {
const tmp_discount_product = props.customer_discounts.find((discount) => discount.product_or_category_id === props.product.id && !discount.is_category)
const tmp_discount_category = props.customer_discounts.find((discount) => discount.product_or_category_id === props.product.category && discount.is_category);
const tmp_discount_global = props.customer_discounts.find((discount) => discount.id === 999999 && discount.is_category);
discount_product.value = null;
discount_category.value = null;
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("Category discount: ", tmp_discount_category);
@@ -112,6 +143,7 @@ const parseCustomerDiscounts = () => {
// Set the product discount
if (tmp_discount_product) {
setProductDiscount(tmp_discount_product.percentage);
setFixedProductPrice(tmp_discount_product.fixed_price);
}
// Set the category discount
if (tmp_discount_category) {
@@ -125,6 +157,10 @@ const parseCustomerDiscounts = () => {
// Get the best discount for the customer
const getBestDiscount = () => {
if (hasFixedProductPrice()) {
highestEligibleDiscount.value = 0;
return 0;
}
// Set the best discount to 0
let tmp_best_discount = 0;
// Check if the product discount is higher than the current best discount
@@ -182,13 +218,32 @@ watch(() => props.customer_discounts, () => {
<span
aria-haspopup="true"
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"
> -{{ highestEligibleDiscount }}%</span>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content py-0">
<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 -->
<a class="list-item">
<div class="list-item-content">
@@ -239,6 +294,7 @@ watch(() => props.customer_discounts, () => {
</div>
</div>
</a>
</template>
</div>
</div>
</div>
@@ -254,4 +310,4 @@ watch(() => props.customer_discounts, () => {
.text-can-not-select {
user-select: none;
}
</style>
</style>
@@ -817,6 +817,10 @@ const isDraftAssignmentModalOpen = computed(() => {
});
const canEditTransaction = (order) => {
if (props.isCustomerView) {
return SessionUser.canAccessCustomerFeature("orders", "edit");
}
return SessionUser.canAccessAdmin() || SessionUser.canAccessDepartment(order.department_id);
};
@@ -1259,7 +1263,7 @@ const formatCashierName = (order) => {
:hover-text="order.reference"
:parse-function="(value) => truncateOrderField(value, 10)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => true"
:permission-check-function="() => canEditTransaction(order)"
/>
<EditableTableColumn
:object="order"
@@ -1278,7 +1282,7 @@ const formatCashierName = (order) => {
:hover-text="order.po"
:parse-function="(value) => truncateOrderField(value, 10)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => canSeePoField() || SessionUser.canAccessAdmin()"
:permission-check-function="() => canEditTransaction(order)"
/>
<td>{{ order.created_at }}</td>
<td>{{ SessionUser.functions.currency.toLocal(order.total_net_amount) }}</td>
@@ -1774,7 +1778,7 @@ const formatCashierName = (order) => {
:hover-text="order.reference"
:parse-function="(value) => truncateOrderField(value, 20)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => true"
:permission-check-function="() => canEditTransaction(order)"
/>
</small>
</div>
@@ -1818,7 +1822,7 @@ const formatCashierName = (order) => {
:hover-text="order.po"
:parse-function="(value) => truncateOrderField(value, 20)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => canSeePoField() || SessionUser.canAccessAdmin()"
:permission-check-function="() => canEditTransaction(order)"
/>
</small>
</div>
@@ -1863,7 +1867,7 @@ const formatCashierName = (order) => {
:hover-text="order.reg_1"
:parse-function="(value) => truncateOrderField(value, 20)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => true"
:permission-check-function="() => canEditTransaction(order)"
/>
</small>
</div>
@@ -1895,7 +1899,7 @@ const formatCashierName = (order) => {
:hover-text="order.reg_2"
:parse-function="(value) => truncateOrderField(value, 20)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => true"
:permission-check-function="() => canEditTransaction(order)"
/>
</small>
</div>
@@ -1927,7 +1931,7 @@ const formatCashierName = (order) => {
:hover-text="order.reg_3"
:parse-function="(value) => truncateOrderField(value, 20)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => true"
:permission-check-function="() => canEditTransaction(order)"
/>
</small>
</div>
@@ -62,6 +62,7 @@ watch(
form.invoiceEmail,
form.contactEmail,
form.contactPhone,
form.ean,
canCreateCustomer.value,
searchResult.value,
errorMessage.value,
@@ -124,6 +125,24 @@ watch(
/>
</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">
<label class="label"
><small>{{ $t("admin.pos.invoice_email") }}</small></label
@@ -3,6 +3,7 @@ export const POS_ADD_CUSTOMER_AUTOFILL_FIELDS = [
"invoiceEmail",
"contactEmail",
"contactPhone",
"ean",
];
export function normalizePosAddCustomerValue(value) {
@@ -20,6 +21,7 @@ export function createPosAddCustomerFields(overrides = {}) {
invoiceEmail: "",
contactEmail: "",
contactPhone: "",
ean: "",
...overrides,
};
}
@@ -30,6 +32,7 @@ export function createPosAddCustomerManualOverrides(overrides = {}) {
invoiceEmail: false,
contactEmail: false,
contactPhone: false,
ean: false,
...overrides,
};
}
@@ -93,6 +96,7 @@ export function buildPosAddCustomerAutofillValues(searchResult) {
invoiceEmail: normalizePosAddCustomerValue(searchResult?.email),
contactEmail: normalizePosAddCustomerValue(searchResult?.email),
contactPhone: normalizePosAddCustomerValue(searchResult?.phone),
ean: normalizePosAddCustomerValue(searchResult?.ean),
};
}
@@ -22,7 +22,7 @@ const props = defineProps({
import {computed, ref, watch} from 'vue';
import { departments, getDepartments, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {orderBy, orderDirection, setOrder, loadList} from "@/components/pagination/paginatedList.vue";
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
import Swal from "sweetalert2";
import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue";
@@ -38,6 +38,8 @@ import {
setCachedXlvaskUsageAmount
} from "@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js";
const { orderBy, orderDirection, setOrder, loadList } = usePaginatedListInstance();
const redirectDepartmentOrderPage = (orderId, departmentId) => {
// Send the user to the order page (In a new tab)
// `/admin/${departmentId}/modules/pos/orders/${orderId}`
@@ -0,0 +1 @@
export const SELFWASH_PERIOD_ALL_LIMIT = 10000;
@@ -38,7 +38,8 @@ if (router.currentRoute.value.params.departmentId) {
<TableLabeledPagination :label="SessionUser.objects.department_notification_sms.meta.title">
<template #buttons="{ loadList }">
<button
class="button is-info is-small"
class="button is-primary is-small"
data-testid="department-notification-sms-add-button"
@click="SessionUser.objects.department_notification_sms.showCreateObjectForm(() => loadList(), {department: parseInt(router.currentRoute.value.params.departmentId)})"
>
<span class="icon is-small">
@@ -21,9 +21,32 @@ const props = defineProps({
type: Number, default: 0
}
})
import {computed, ref, watch } from "vue";
import {computed, provide, ref, watch } from "vue";
import { useRouter } from "vue-router";
import {
PaginatedListKey,
usePaginatedList,
} from "@/components/pagination/paginatedList.vue";
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
import PaginationDisplayTemplateDates
from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
import ShowErrorField from "@/components/global/ShowErrorField.vue";
import { useI18n } from 'vue-i18n'
import { BSwitch } from "buefy";
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
import { SELFWASH_PERIOD_ALL_LIMIT } from "@/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n()
const router = useRouter();
const paginatedList = usePaginatedList();
provide(PaginatedListKey, paginatedList);
const {
isLoading,
list,
loadList,
@@ -38,23 +61,8 @@ import {
setOrder,
hideSearchField,
setHideSearchField,
} from "@/components/pagination/paginatedList.vue";
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
} = paginatedList;
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
import PaginationDisplayTemplateDates
from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
import ShowErrorField from "@/components/global/ShowErrorField.vue";
import { useI18n } from 'vue-i18n'
import { BSwitch } from "buefy";
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
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);
if (props.loadAllAtOnce) {
setMetaItemsPerPage(SELFWASH_PERIOD_ALL_LIMIT, false);
@@ -104,13 +112,24 @@ if (router.currentRoute.value.params.departmentId) {
}
const isImportLoading = ref(false);
const buildImportUsageParams = () => {
if (!props.inheritPeriodFilters) {
return {};
}
return {
...(props.initialDateFrom ? { dateFrom: props.initialDateFrom } : {}),
...(props.initialDateTo ? { dateTo: props.initialDateTo } : {}),
};
};
const importUsageButton = async () => {
isImportLoading.value = true;
try {
await SessionUser.request(
'/modules/xlvask/tasks/import-usage',
'GET',
{},
buildImportUsageParams(),
function (error) {
// This function is called when the request fails
console.error("Failed to import orders.", error);
@@ -10,11 +10,21 @@ const props = defineProps(["setCustomerFilter", "hideSearch", "autoLoad"]);
const paginatedList = usePaginatedList();
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();
setEndpoint("/users", false);
setFilter("customer_number", 0, false);
setAdditionalQueryParameters({ include_limited_backoffice_employees: "true" });
setOrder("created_at", "desc");
// Hide the search field, if the hideSearch prop is set
@@ -52,7 +62,7 @@ if (props.autoLoad) {
</div>
</div>
</template>
<UsersTable :objects="list" />
<UsersTable :objects="list" @migrated="loadList" />
</TableLabeledPagination>
</template>
@@ -24,7 +24,7 @@ import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue"
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
import BookingsTable from "@/components/displays/user/bookings/bookingsTable.vue";
import {departments} from "@/components/pagination/departmentTabs.vue";
import {ref, watch} from "vue";
import {computed, ref, watch} from "vue";
import { Colors } from "@/ThemeConfig.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useI18n } from 'vue-i18n'
@@ -32,6 +32,8 @@ import { todayLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n()
const router = useRouter();
const isUserRoute = computed(() => router.currentRoute.value.path.startsWith("/user"));
const canCreateBooking = computed(() => SessionUser.canAccessCustomerFeature("bookings", "add"));
// Set the selected status to all
const selectedStatus = ref("*");
@@ -148,7 +150,7 @@ const showNewOrderBookingsPortal = () => {
</div>
<div class="column is-auto-fill my-3" v-if="!isSmall"/>
<!-- Only show the bookings for today (Switch, if the route is /user) -->
<div class="column is-narrow my-3" v-if="router.currentRoute.value.path.startsWith('/user')" :class="{ 'has-text-right': !isSmall }">
<div class="column is-narrow my-3" v-if="isUserRoute" :class="{ 'has-text-right': !isSmall }">
<label class="label">{{ isSmall ? t('common.today') : t('pagination.show_only_today') }}</label>
<div class="control">
<div class="field">
@@ -158,7 +160,7 @@ const showNewOrderBookingsPortal = () => {
</div>
</div>
<!-- Create a new booking, if the route is /user -->
<div class="column is-narrow my-3" v-if="router.currentRoute.value.path.startsWith('/user')" :class="{ 'has-text-right': !isSmall }">
<div class="column is-narrow my-3" v-if="isUserRoute && canCreateBooking" :class="{ 'has-text-right': !isSmall }">
<button
class="button is-link"
@click="SessionUser.functions.redirectTo.external('/user/bookings/new', true)"
@@ -17,6 +17,7 @@ const { t } = useI18n();
const router = useRouter();
const isUserRoute = computed(() => router.currentRoute.value.path.startsWith("/user"));
const isAdminRoute = computed(() => router.currentRoute.value.path.startsWith("/admin"));
const canCreateBooking = computed(() => SessionUser.canAccessCustomerFeature("bookings", "add"));
/**
* Props
*/
@@ -202,7 +203,7 @@ onMounted(() => {
<label for="today"></label>
</div>
</div>
<div class="order-bookings-pagination__new-booking-action">
<div class="order-bookings-pagination__new-booking-action" v-if="canCreateBooking">
<label class="label is-small order-bookings-pagination__desktop-spacer">&nbsp;</label>
<button
class="button is-link button-same-width"
@@ -8,6 +8,7 @@ import {
import {SessionUser} from "@/components/session/token/SessionUser.vue";
import { useRouter } from "vue-router";
import VehiclesTable from "@/components/displays/user/vehicles/vehiclesTable.vue";
import { computed } from "vue";
import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue";
import {Colors} from "@/ThemeConfig.vue";
@@ -20,6 +21,8 @@ const { t } = useI18n();
* Router
*/
const router = useRouter();
const isUserRoute = computed(() => router.currentRoute.value.path.startsWith("/user"));
const canAddVehicle = computed(() => SessionUser.canAccessCustomerFeature("vehicles", "add"));
const props = defineProps({
customer_id: {
@@ -58,7 +61,7 @@ loadList();
</div>
<!-- Create a new vehicle, if the route is /user -->
<div class="vehicles-pagination__add-action"
v-if="router.currentRoute.value.path.startsWith('/user')">
v-if="isUserRoute && canAddVehicle">
<label class="label is-small">&nbsp;</label>
<button
class="button is-link button-same-width vehicles-pagination__add-button"
@@ -1,5 +1,5 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { RouterLink } from "vue-router";
import { useI18n } from "vue-i18n";
import {
@@ -16,6 +16,7 @@ import {
const { t, te, locale } = useI18n();
const MAX_GATEWAY_CARDS = 8;
const DEFAULT_DASHBOARD_TAB = "infrastructure";
const gatewayStatusPriority = Object.freeze({
OFFLINE: 0,
DEGRADED: 1,
@@ -77,7 +78,14 @@ const gatewayFleetUsage = ref(createEmptyGatewayFleetUsage());
const gatewaySectionSuppressed = ref(false);
const gatewayDepartments = ref({});
const gatewayDepartmentsLoaded = ref(false);
const activeDashboardTab = ref(DEFAULT_DASHBOARD_TAB);
const showGatewaySection = computed(() => canViewGateways.value && !gatewaySectionSuppressed.value);
const dashboardTabKeys = computed(() => [
DEFAULT_DASHBOARD_TAB,
...(showGatewaySection.value ? ["gateways"] : []),
"modules",
"sessions",
]);
const gatewaySummaryCards = computed(() => [
{
id: "total",
@@ -187,6 +195,16 @@ const isStale = computed(() => {
return nowTick.value - lastLoadedAt.value.getTime() > (refreshAfterSeconds.value * 2000);
});
watch(
() => [activeDashboardTab.value, dashboardTabKeys.value.join("|")],
() => {
if (!dashboardTabKeys.value.includes(activeDashboardTab.value)) {
activeDashboardTab.value = DEFAULT_DASHBOARD_TAB;
}
},
{ immediate: true }
);
const loadStatus = async ({ force = false } = {}) => {
const [snapshotValue] = await Promise.all([
getSuperuserSystemStatus({ force }),
@@ -707,203 +725,250 @@ function modulePath(key) {
</ul>
</div>
<section class="system-status-section">
<div class="section-heading">
<h3>{{ $t("system_status.sections.infrastructure") }}</h3>
<RouterLink class="section-link" to="/superuser/system/replication">
{{ $t("system_status.actions.open_replication") }}
</RouterLink>
</div>
<div class="status-card-grid">
<article
v-for="card in statusCards"
:key="card.id"
class="status-card"
:class="statusClass(card.status)"
:data-testid="`status-card-${card.id}`"
>
<div class="status-card__top">
<p class="status-card__title">{{ card.title }}</p>
<span class="status-pill" :class="statusClass(card.status)">{{ statusLabel(card.status) }}</span>
</div>
<strong class="status-card__primary">{{ card.primary }}</strong>
<p class="status-card__secondary">{{ card.secondary }}</p>
<small class="status-card__detail">{{ card.detail }}</small>
<small
v-if="card.replicationLabel"
class="status-card__detail status-card__replication"
:data-testid="`status-card-${card.id}-replication`"
>
{{ card.replicationLabel }}
</small>
</article>
</div>
</section>
<section
v-if="showGatewaySection"
class="system-status-section"
data-testid="system-status-gateways"
<b-tabs
v-model="activeDashboardTab"
expanded
type="is-boxed"
class="system-status-tabs"
data-testid="system-status-dashboard-tabs"
>
<div class="section-heading">
<div>
<h3>{{ $t("system_status.sections.gateways") }}</h3>
<p class="section-subtitle">{{ $t("system_status.gateways.description") }}</p>
</div>
<RouterLink class="section-link" to="/superuser/selfserve/edge-agents">
{{ $t("system_status.gateways.actions.open_fleet") }}
</RouterLink>
</div>
<b-tab-item value="infrastructure">
<template #header>
<b-icon pack="fas" icon="server" />
<span data-testid="system-status-tab-infrastructure">
{{ $t("system_status.sections.infrastructure") }}
</span>
</template>
<div class="gateway-summary-grid">
<article
v-for="card in gatewaySummaryCards"
:key="card.id"
class="summary-card gateway-summary-card"
:class="card.toneClass"
:data-testid="`gateway-summary-card-${card.id}`"
<section class="system-status-section" data-testid="system-status-panel-infrastructure">
<div class="section-heading">
<h3>{{ $t("system_status.sections.infrastructure") }}</h3>
<RouterLink class="section-link" to="/superuser/system/replication">
{{ $t("system_status.actions.open_replication") }}
</RouterLink>
</div>
<div class="status-card-grid">
<article
v-for="card in statusCards"
:key="card.id"
class="status-card"
:class="statusClass(card.status)"
:data-testid="`status-card-${card.id}`"
>
<div class="status-card__top">
<p class="status-card__title">{{ card.title }}</p>
<span class="status-pill" :class="statusClass(card.status)">{{ statusLabel(card.status) }}</span>
</div>
<strong class="status-card__primary">{{ card.primary }}</strong>
<p class="status-card__secondary">{{ card.secondary }}</p>
<small class="status-card__detail">{{ card.detail }}</small>
<small
v-if="card.replicationLabel"
class="status-card__detail status-card__replication"
:data-testid="`status-card-${card.id}-replication`"
>
{{ card.replicationLabel }}
</small>
</article>
</div>
</section>
</b-tab-item>
<b-tab-item v-if="showGatewaySection" value="gateways">
<template #header>
<b-icon pack="fas" icon="network-wired" />
<span data-testid="system-status-tab-gateways">
{{ $t("system_status.sections.gateways") }}
</span>
</template>
<section
class="system-status-section"
data-testid="system-status-gateways"
>
<p class="summary-label">{{ card.title }}</p>
<strong>{{ card.value }}</strong>
</article>
</div>
<div
v-if="gatewayError"
class="notification is-warning is-light gateway-notification"
data-testid="gateway-section-error"
>
{{ $t("system_status.gateways.error") }}
</div>
<div
v-if="gatewayLoading && !gatewayCards.length"
class="notification is-light gateway-notification"
>
{{ $t("system_status.gateways.loading") }}
</div>
<div v-if="gatewayCards.length" class="gateway-grid">
<article
v-for="gateway in gatewayCards"
:key="gateway.id"
class="gateway-card"
:class="gateway.toneClass"
:data-testid="`gateway-card-${gateway.id}`"
>
<div class="gateway-card__top">
<div class="section-heading">
<div>
<strong class="gateway-card__title">{{ gateway.displayLabel }}</strong>
<p class="gateway-card__subtitle">{{ gateway.departmentName }}</p>
<h3>{{ $t("system_status.sections.gateways") }}</h3>
<p class="section-subtitle">{{ $t("system_status.gateways.description") }}</p>
</div>
<span class="status-pill" :class="gateway.toneClass">{{ gateway.statusLabel }}</span>
<RouterLink class="section-link" to="/superuser/selfserve/edge-agents">
{{ $t("system_status.gateways.actions.open_fleet") }}
</RouterLink>
</div>
<div class="gateway-card__meta">
<span>
{{ $t("system_status.gateways.labels.discovery") }}: {{ gateway.discoveryLabel }}
</span>
<span>
{{ $t("system_status.gateways.labels.last_heartbeat") }}: {{ gateway.lastHeartbeatLabel }}
</span>
<span v-if="gateway.activeOperationLabel">
{{ $t("system_status.gateways.labels.active_operation") }}: {{ gateway.activeOperationLabel }}
</span>
<div class="gateway-summary-grid">
<article
v-for="card in gatewaySummaryCards"
:key="card.id"
class="summary-card gateway-summary-card"
:class="card.toneClass"
:data-testid="`gateway-summary-card-${card.id}`"
>
<p class="summary-label">{{ card.title }}</p>
<strong>{{ card.value }}</strong>
</article>
</div>
<p v-if="gateway.message" class="gateway-card__message">
{{ gateway.message }}
</p>
<RouterLink :to="gateway.link" class="module-card__link">
{{ $t("system_status.gateways.actions.open_gateway") }}
</RouterLink>
</article>
</div>
<div
v-else-if="!gatewayLoading && !gatewayError"
class="gateway-empty"
data-testid="gateway-empty-state"
>
{{ $t("system_status.gateways.empty") }}
</div>
</section>
<section class="system-status-section">
<div class="section-heading">
<h3>{{ $t("system_status.sections.modules") }}</h3>
</div>
<div class="module-grid">
<article
v-for="module in modules"
:key="module.key"
class="module-card"
:class="statusClass(module.status)"
:data-testid="`module-card-${module.key}`"
>
<div class="module-card__top">
<p class="module-card__title">{{ moduleLabel(module.key) }}</p>
<span class="status-pill" :class="statusClass(module.status)">{{ statusLabel(module.status) }}</span>
<div
v-if="gatewayError"
class="notification is-warning is-light gateway-notification"
data-testid="gateway-section-error"
>
{{ $t("system_status.gateways.error") }}
</div>
<p class="module-card__reason" :data-testid="`module-reason-${module.key}`">
{{ moduleReasonText(module) }}
</p>
<div class="module-card__meta">
<span>{{ $t("system_status.labels.enabled") }}: {{ module.enabled ? $t("system_status.status.yes") : $t("system_status.status.no") }}</span>
<span>{{ $t("system_status.labels.configured") }}: {{ module.configured ? $t("system_status.status.yes") : $t("system_status.status.no") }}</span>
<span>{{ $t("system_status.labels.checked_at") }}: {{ formatDate(module.checked_at) }}</span>
<div
v-if="gatewayLoading && !gatewayCards.length"
class="notification is-light gateway-notification"
>
{{ $t("system_status.gateways.loading") }}
</div>
<RouterLink v-if="modulePath(module.key)" :to="modulePath(module.key)" class="module-card__link">
{{ $t("system_status.actions.open_config") }}
</RouterLink>
</article>
</div>
</section>
<section class="system-status-section">
<div class="section-heading">
<h3>{{ $t("system_status.sections.sessions") }}</h3>
<p>{{ $t("system_status.labels.activity_window", { minutes: sessions.active_window_minutes }) }}</p>
</div>
<div v-if="gatewayCards.length" class="gateway-grid">
<article
v-for="gateway in gatewayCards"
:key="gateway.id"
class="gateway-card"
:class="gateway.toneClass"
:data-testid="`gateway-card-${gateway.id}`"
>
<div class="gateway-card__top">
<div>
<strong class="gateway-card__title">{{ gateway.displayLabel }}</strong>
<p class="gateway-card__subtitle">{{ gateway.departmentName }}</p>
</div>
<span class="status-pill" :class="gateway.toneClass">{{ gateway.statusLabel }}</span>
</div>
<div class="table-container sessions-table">
<table class="table is-fullwidth is-hoverable">
<thead>
<tr>
<th>{{ $t("system_status.table.active") }}</th>
<th>{{ $t("system_status.table.name") }}</th>
<th>{{ $t("system_status.table.type") }}</th>
<th>{{ $t("system_status.table.device") }}</th>
<th>{{ $t("system_status.table.route") }}</th>
<th>{{ $t("system_status.table.first_seen") }}</th>
<th>{{ $t("system_status.table.last_seen") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="session in sessions.recent_sessions" :key="`${session.session_kind}-${session.principal_id}-${session.last_seen_at}`">
<td>
<span class="status-pill" :class="session.active ? 'is-ok' : 'is-down'">
{{ session.active ? $t("system_status.status.active") : $t("system_status.status.inactive") }}
<div class="gateway-card__meta">
<span>
{{ $t("system_status.gateways.labels.discovery") }}: {{ gateway.discoveryLabel }}
</span>
</td>
<td>
<strong>{{ sessionDisplayName(session) }}</strong>
<div class="session-context">{{ sessionContextLabel(session) }}</div>
</td>
<td>{{ formatSessionKind(session.session_kind) }}</td>
<td>{{ formatDeviceType(session.device_type) }}</td>
<td class="session-route">{{ session.last_route || "--" }}</td>
<td>{{ formatDate(session.first_seen_at) }}</td>
<td>{{ formatDate(session.last_seen_at) }}</td>
</tr>
<tr v-if="!sessions.recent_sessions?.length">
<td colspan="7">{{ $t("system_status.states.no_sessions") }}</td>
</tr>
</tbody>
</table>
</div>
</section>
<span>
{{ $t("system_status.gateways.labels.last_heartbeat") }}: {{ gateway.lastHeartbeatLabel }}
</span>
<span v-if="gateway.activeOperationLabel">
{{ $t("system_status.gateways.labels.active_operation") }}: {{ gateway.activeOperationLabel }}
</span>
</div>
<p v-if="gateway.message" class="gateway-card__message">
{{ gateway.message }}
</p>
<RouterLink :to="gateway.link" class="module-card__link">
{{ $t("system_status.gateways.actions.open_gateway") }}
</RouterLink>
</article>
</div>
<div
v-else-if="!gatewayLoading && !gatewayError"
class="gateway-empty"
data-testid="gateway-empty-state"
>
{{ $t("system_status.gateways.empty") }}
</div>
</section>
</b-tab-item>
<b-tab-item value="modules">
<template #header>
<b-icon pack="fas" icon="puzzle-piece" />
<span data-testid="system-status-tab-modules">
{{ $t("system_status.sections.modules") }}
</span>
</template>
<section class="system-status-section" data-testid="system-status-panel-modules">
<div class="section-heading">
<h3>{{ $t("system_status.sections.modules") }}</h3>
</div>
<div class="module-grid">
<article
v-for="module in modules"
:key="module.key"
class="module-card"
:class="statusClass(module.status)"
:data-testid="`module-card-${module.key}`"
>
<div class="module-card__top">
<p class="module-card__title">{{ moduleLabel(module.key) }}</p>
<span class="status-pill" :class="statusClass(module.status)">{{ statusLabel(module.status) }}</span>
</div>
<p class="module-card__reason" :data-testid="`module-reason-${module.key}`">
{{ moduleReasonText(module) }}
</p>
<div class="module-card__meta">
<span>{{ $t("system_status.labels.enabled") }}: {{ module.enabled ? $t("system_status.status.yes") : $t("system_status.status.no") }}</span>
<span>{{ $t("system_status.labels.configured") }}: {{ module.configured ? $t("system_status.status.yes") : $t("system_status.status.no") }}</span>
<span>{{ $t("system_status.labels.checked_at") }}: {{ formatDate(module.checked_at) }}</span>
</div>
<RouterLink v-if="modulePath(module.key)" :to="modulePath(module.key)" class="module-card__link">
{{ $t("system_status.actions.open_config") }}
</RouterLink>
</article>
</div>
</section>
</b-tab-item>
<b-tab-item value="sessions">
<template #header>
<b-icon pack="fas" icon="users" />
<span data-testid="system-status-tab-sessions">
{{ $t("system_status.sections.sessions") }}
</span>
</template>
<section class="system-status-section" data-testid="system-status-panel-sessions">
<div class="section-heading">
<h3>{{ $t("system_status.sections.sessions") }}</h3>
<p>{{ $t("system_status.labels.activity_window", { minutes: sessions.active_window_minutes }) }}</p>
</div>
<div class="table-container sessions-table">
<table class="table is-fullwidth is-hoverable">
<thead>
<tr>
<th>{{ $t("system_status.table.active") }}</th>
<th>{{ $t("system_status.table.name") }}</th>
<th>{{ $t("system_status.table.type") }}</th>
<th>{{ $t("system_status.table.device") }}</th>
<th>{{ $t("system_status.table.route") }}</th>
<th>{{ $t("system_status.table.first_seen") }}</th>
<th>{{ $t("system_status.table.last_seen") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="session in sessions.recent_sessions" :key="`${session.session_kind}-${session.principal_id}-${session.last_seen_at}`">
<td>
<span class="status-pill" :class="session.active ? 'is-ok' : 'is-down'">
{{ session.active ? $t("system_status.status.active") : $t("system_status.status.inactive") }}
</span>
</td>
<td>
<strong>{{ sessionDisplayName(session) }}</strong>
<div class="session-context">{{ sessionContextLabel(session) }}</div>
</td>
<td>{{ formatSessionKind(session.session_kind) }}</td>
<td>{{ formatDeviceType(session.device_type) }}</td>
<td class="session-route">{{ session.last_route || "--" }}</td>
<td>{{ formatDate(session.first_seen_at) }}</td>
<td>{{ formatDate(session.last_seen_at) }}</td>
</tr>
<tr v-if="!sessions.recent_sessions?.length">
<td colspan="7">{{ $t("system_status.states.no_sessions") }}</td>
</tr>
</tbody>
</table>
</div>
</section>
</b-tab-item>
</b-tabs>
</div>
</template>
@@ -924,10 +989,10 @@ function modulePath(key) {
.module-card,
.gateway-card {
border: 1px solid #d7dde7;
border-radius: 18px;
border-radius: 8px;
padding: 1rem 1.1rem;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.06);
background: #ffffff;
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.05);
}
.summary-card--overall {
@@ -991,6 +1056,23 @@ function modulePath(key) {
color: #0f4c81;
}
.system-status-tabs {
min-width: 0;
}
.system-status-tabs :deep(.tab-content) {
padding: 1rem 0 0;
}
.system-status-tabs :deep(.tabs ul) {
align-items: stretch;
}
.system-status-tabs :deep(.tabs a) {
min-height: 2.75rem;
gap: 0.35rem;
}
.status-card__top,
.module-card__top {
display: flex;
@@ -1107,7 +1189,7 @@ function modulePath(key) {
.gateway-empty {
border: 1px dashed #cbd5e1;
border-radius: 18px;
border-radius: 8px;
padding: 1rem 1.1rem;
color: #475569;
background: rgba(248, 250, 252, 0.8);
@@ -12,6 +12,11 @@ import UserOtherVaskeabonnement
import Swal from "sweetalert2";
import OrderItemsTable from "@/components/displays/department/pos/order/orderItemsTable.vue";
import OrderContentTable from "@/components/displays/superuser/tables/OrderContentTable.vue";
import {
buildMultiMonthInvoiceContext,
MULTI_MONTH_INVOICE_ACTION,
promptMultiMonthInvoiceWarning,
} from "@/services/invoiceMonthSplitWarning.js";
const props = defineProps({
orders: {
@@ -126,32 +131,34 @@ const isAnyOrderSelected = () => {
return selectedInvoiceCollections.value.length > 0;
}
const getSelectedOrders = () => {
return props.orders.filter((order) => selectedInvoiceCollections.value.includes(order.invoice_collection_id));
}
/** Invoice collections */
const onInvoiceCollections = async () => {
// Check if any orders are selected
if (selectedInvoiceCollections.value.length === 0) {
return;
}
/** Create the invoice */
const onCreateInvoiceDraft = async () => {
// Create the invoice
console.log('Create invoice');
await SessionUser.objects.collectedOrderInvoices.functions.economic.invoice(parseInt(props.collectedOrderInvoice.id)).then((response) => {
console.log('Invoice created successfully', response);
Swal.fire({
title: 'Fakturaen er oprettet',
text: 'Fakturaen er oprettet i E-conomic',
icon: 'success',
showConfirmButton: false,
timer: 2000
}).then(() => {
location.reload();
});
}).catch((error) => {
console.log('Error creating invoice', error);
}
)
const selectedOrders = getSelectedOrders();
const invoiceWarningContext = buildMultiMonthInvoiceContext(selectedOrders, {
getDate: (order) => order?.created_at ?? order?.date,
getInvoiceCollectionId: (order) => order?.invoice_collection_id,
});
const invoiceWarningAction = await promptMultiMonthInvoiceWarning({
context: invoiceWarningContext,
splitByMonth: SessionUser.objects.collectedOrderInvoices.functions.split_by_month,
parseErrorMessage: SessionUser.functions.parseErrorMessage,
});
if (invoiceWarningAction === MULTI_MONTH_INVOICE_ACTION.SPLIT) {
location.reload();
return;
}
if (invoiceWarningAction === MULTI_MONTH_INVOICE_ACTION.CANCEL) {
return;
}
for (let i = 0; i < selectedInvoiceCollections.value.length; i++) {
const selectedInvoiceCollectionId = selectedInvoiceCollections.value[i];
// Check if the invoice collection is already booked
@@ -210,6 +217,7 @@ const isOrderContentVisible = (order) => {
class="button is-small"
@click="onInvoiceCollections()"
:disabled="!isAnyOrderSelected()"
data-testid="invoice-order-table-invoice-button"
>
{{ $t('global.invoice_now') }}
</button>
@@ -274,4 +282,4 @@ const isOrderContentVisible = (order) => {
<style scoped>
</style>
</style>
@@ -1,21 +1,136 @@
<script setup>
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
import { useI18n } from 'vue-i18n';
import Swal from "sweetalert2";
const { t } = useI18n();
defineProps(['objects']);
import { ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
const { loadList } = usePaginatedListInstance();
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue";
const redirect = (path) => {
window.location = path;
}
const limitedBackofficeTemplates = ref([]);
const templatesLoading = ref(false);
const templatesLoadError = ref(false);
const applyingTemplateKey = ref("");
const canApplyLimitedBackofficeTemplates = computed(() => SessionUser.hasPermission("add_role_permission"));
const loadLimitedBackofficeTemplates = async () => {
if (!canApplyLimitedBackofficeTemplates.value || templatesLoading.value) {
return;
}
templatesLoading.value = true;
templatesLoadError.value = false;
try {
const templates = await SessionUser.objects.roles.functions.getLimitedBackofficePermissionTemplates();
limitedBackofficeTemplates.value = Array.isArray(templates) ? templates : [];
} catch (error) {
console.error("Failed to load limited backoffice permission templates", error);
templatesLoadError.value = true;
} finally {
templatesLoading.value = false;
}
};
const templateTranslation = (template, field) => {
const key = `templates.limited_backoffice.roles.${template?.key}.${field}`;
const translated = t(key);
return translated === key ? template?.[field] || template?.key || "" : translated;
};
const templateLabel = (template) => templateTranslation(template, "label");
const rolePermissions = (role) => (Array.isArray(role?.permissions) ? role.permissions : []);
const templatePermissions = (template) => (Array.isArray(template?.permissions) ? template.permissions : []);
const missingTemplatePermissions = (role, template) => {
const currentPermissions = new Set(rolePermissions(role));
return templatePermissions(template).filter((permission) => permission && !currentPermissions.has(permission));
};
const roleTemplateActionKey = (role, template) => `${role?.id}:${template?.key}`;
const isApplyingTemplate = (role, template) => applyingTemplateKey.value === roleTemplateActionKey(role, template);
const isTemplateActionDisabled = (role, template) =>
templatesLoading.value || Boolean(applyingTemplateKey.value) || missingTemplatePermissions(role, template).length === 0;
const applyLimitedBackofficeTemplate = async (role, template) => {
const missingPermissions = missingTemplatePermissions(role, template);
const label = templateLabel(template);
if (missingPermissions.length === 0) {
await Swal.fire({
icon: "success",
title: t("roles.templates.up_to_date_title"),
text: t("roles.templates.up_to_date_text", { role: role.name, template: label }),
});
return;
}
const confirmation = await Swal.fire({
icon: "question",
title: t("roles.templates.confirm_title"),
text: t("roles.templates.confirm_text", {
count: missingPermissions.length,
role: role.name,
template: label,
}),
showCancelButton: true,
confirmButtonText: t("roles.templates.confirm_button"),
cancelButtonText: t("common.cancel"),
});
if (!confirmation.isConfirmed) {
return;
}
applyingTemplateKey.value = roleTemplateActionKey(role, template);
try {
const results = await Promise.allSettled(
missingPermissions.map((permission) =>
SessionUser.objects.roles.functions.permissions.add(role.id, permission)
)
);
const failed = results.filter((result) => result.status === "rejected");
if (failed.length > 0) {
throw failed[0].reason;
}
await Swal.fire({
icon: "success",
title: t("roles.templates.success_title"),
text: t("roles.templates.success_text", {
count: missingPermissions.length,
role: role.name,
template: label,
}),
});
} catch (error) {
console.error("Failed to apply limited backoffice permission template", error);
await Swal.fire({
icon: "error",
title: t("roles.templates.error_title"),
text: t("roles.templates.error_text", { role: role.name, template: label }),
});
} finally {
applyingTemplateKey.value = "";
await loadList();
}
};
onMounted(loadLimitedBackofficeTemplates);
</script>
<template>
@@ -66,6 +181,46 @@ const redirect = (path) => {
icon="fas fa-clone"
:click-action="() => SessionUser.objects.roles.functions.cloneObject(object.id).then(() => loadList())"
></ActionSettingsWheelItem>
<div
v-if="canApplyLimitedBackofficeTemplates"
class="role-limited-backoffice-template-actions"
:data-testid="`role-limited-backoffice-template-actions-${object.id}`"
>
<ActionSettingsWheelItemLabel
:label="$t('roles.templates.section')"
icon="fas fa-id-badge"
></ActionSettingsWheelItemLabel>
<ActionSettingsWheelItem
v-if="templatesLoading"
:label="$t('roles.templates.loading')"
icon="fas fa-spinner"
disabled
></ActionSettingsWheelItem>
<ActionSettingsWheelItem
v-else-if="templatesLoadError"
:label="$t('roles.templates.load_error')"
icon="fas fa-sync-alt"
:click-action="loadLimitedBackofficeTemplates"
></ActionSettingsWheelItem>
<ActionSettingsWheelItem
v-else-if="limitedBackofficeTemplates.length === 0"
:label="$t('roles.templates.unavailable')"
icon="fas fa-ban"
disabled
></ActionSettingsWheelItem>
<template v-else>
<ActionSettingsWheelItem
v-for="template in limitedBackofficeTemplates"
:key="template.key"
:label="$t('roles.templates.apply', { template: templateLabel(template) })"
:icon="isApplyingTemplate(object, template) ? 'fas fa-spinner' : 'fas fa-id-badge'"
:disabled="isTemplateActionDisabled(object, template)"
:test-id="`role-template-${object.id}-${template.key}`"
:click-action="() => applyLimitedBackofficeTemplate(object, template)"
></ActionSettingsWheelItem>
</template>
</div>
</template>
</ActionSettingsWheelButton>
</div>
@@ -85,4 +240,4 @@ const redirect = (path) => {
<style scoped>
</style>
</style>
@@ -2,6 +2,14 @@
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
import { showEditUserForm } from "@/components/forms/superUser/editUserForm.vue";
import {
getLimitedBackofficeDepartments,
getLimitedBackofficeRoles,
limitedBackofficeErrorMessage,
migrateLimitedBackofficeEmployee,
unwrapLimitedBackofficeResponse,
} from "@/services/limitedBackoffice.js";
import Swal from "sweetalert2";
defineProps({
objects: {
@@ -10,8 +18,109 @@ defineProps({
},
});
const emit = defineEmits(["migrated"]);
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),
});
};
const escapeHtml = (value) =>
String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
const canMigrateToLimitedBackoffice = (user) =>
Number(user?.customer_number) === 0 && !Boolean(user?.limited_backoffice_managed);
const migrateUser = async (user) => {
try {
const [departmentsResponse, rolesResponse] = await Promise.all([
getLimitedBackofficeDepartments(),
getLimitedBackofficeRoles(),
]);
const departments = unwrapLimitedBackofficeResponse(departmentsResponse) || [];
const roles = unwrapLimitedBackofficeResponse(rolesResponse) || [];
if (!Array.isArray(departments) || departments.length === 0 || !Array.isArray(roles) || roles.length === 0) {
throw new Error("Limited backoffice roles or departments are unavailable.");
}
const roleOptions = roles
.filter((role) => role?.key && role?.key !== "superuser")
.map((role) => {
const selected = role.key === "cashier" ? " selected" : "";
return `<option value="${escapeHtml(role.key)}"${selected}>${escapeHtml(role.label || role.key)}</option>`;
})
.join("");
const departmentOptions = departments
.map(
(department) => `
<label class="checkbox is-block has-text-left mb-2">
<input type="checkbox" class="limited-migration-department" value="${escapeHtml(department.id)}" />
${escapeHtml(department.name || `Department ${department.id}`)}
</label>
`
)
.join("");
const result = await Swal.fire({
title: "Migrate employee",
html: `
<div class="field has-text-left">
<label class="label">Employee</label>
<p>${escapeHtml(user.display_name || `#${user.id}`)}</p>
</div>
<div class="field has-text-left">
<label class="label" for="limited-migration-role">Role</label>
<div class="control">
<div class="select is-fullwidth">
<select id="limited-migration-role">${roleOptions}</select>
</div>
</div>
</div>
<div class="field has-text-left">
<label class="label">Departments</label>
<div class="control">${departmentOptions}</div>
</div>
`,
showCancelButton: true,
confirmButtonText: "Migrate",
preConfirm: async () => {
const role = document.getElementById("limited-migration-role")?.value;
const departmentIds = Array.from(document.querySelectorAll(".limited-migration-department:checked")).map(
(checkbox) => Number(checkbox.value)
);
if (!role || departmentIds.length === 0) {
Swal.showValidationMessage("Select a role and at least one department.");
return false;
}
try {
await migrateLimitedBackofficeEmployee(user.id, {
role_key: role,
department_ids: departmentIds,
});
return true;
} catch (error) {
Swal.showValidationMessage(limitedBackofficeErrorMessage(error, "Could not migrate employee."));
return false;
}
},
});
if (result.isConfirmed) {
await Swal.fire("Employee migrated");
emit("migrated");
}
} catch (error) {
await Swal.fire("Error", limitedBackofficeErrorMessage(error, "Could not migrate employee."), "error");
}
};
</script>
@@ -45,6 +154,13 @@ const editUser = (user) => {
:label="$t('global.edit')"
:test-id="`superuser-user-edit-${user.id}`"
/>
<ActionSettingsWheelItem
v-if="canMigrateToLimitedBackoffice(user)"
:click-action="() => migrateUser(user)"
icon="fas fa-random"
:label="$t('superuser.pages.employees.migrate_to_limited_backoffice')"
:test-id="`superuser-user-migrate-limited-${user.id}`"
/>
</template>
</ActionSettingsWheelButton>
</div>
@@ -45,6 +45,11 @@ const reload = () => {
}
};
const canViewVehicle = () => SessionUser.canAccessCustomerFeature("vehicles", "list");
const canEditVehicle = () => SessionUser.canAccessCustomerFeature("vehicles", "edit");
const canDeleteVehicle = () => SessionUser.canAccessCustomerFeature("vehicles", "delete");
const canShowVehicleRowActions = () => canViewVehicle() || canDeleteVehicle();
const redirectUserVehiclePage = (vehicleId) => {
// Send the user to the vehicle page
window.location.href = `/user/vehicles/${vehicleId}`;
@@ -146,6 +151,7 @@ const getProductOptionsLabel = (vehicle) => {
:object="object"
:loadList="reload"
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
:permission-check-function="canEditVehicle"
column="reg"
/>
<!-- Type -->
@@ -153,6 +159,7 @@ const getProductOptionsLabel = (vehicle) => {
:object="object"
:loadList="reload"
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
:permission-check-function="canEditVehicle"
column="type"
:parse-function="
(value) => {
@@ -165,6 +172,7 @@ const getProductOptionsLabel = (vehicle) => {
:object="object"
:loadList="reload"
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
:permission-check-function="canEditVehicle"
column="wash_subscription"
:parse-function="
(value) => {
@@ -174,7 +182,7 @@ const getProductOptionsLabel = (vehicle) => {
/>
<!-- Product Options, if the wash subscription is set to true -->
<td v-if="!props.compact">
<template v-if="object.wash_subscription">
<template v-if="object.wash_subscription && canEditVehicle()">
<!-- Enabled subscription -->
<ActionSettingsWheelButton
:label="getProductOptionsLabel(object)"
@@ -218,13 +226,14 @@ const getProductOptionsLabel = (vehicle) => {
:object="object"
:loadList="reload"
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
:permission-check-function="canEditVehicle"
column="reference"
/>
<!-- Actions -->
<td>
<td v-if="!props.compact">
<!-- Actions stay grouped behind the wheel menu. -->
<ActionSettingsWheelButton
v-if="!props.compact"
v-if="canShowVehicleRowActions()"
:user_id="object.user_id"
:reg_1="object.reg"
:displayActionsDirectly="false"
@@ -232,12 +241,14 @@ const getProductOptionsLabel = (vehicle) => {
<template #actions>
<!-- View (Redirect to the vehicle page) -->
<ActionSettingsWheelItem
v-if="canViewVehicle()"
:label="$t('global.manage') + ' ' + $t('objects.vehicles.single')"
icon="fas fa-eye"
:click-action="() => redirectUserVehiclePage(object.id)"
/>
<!-- Delete -->
<ActionSettingsWheelItem
v-if="canDeleteVehicle()"
:label="$t('global.delete') + ' ' + $t('objects.vehicles.single')"
icon="fas fa-trash"
:template="'danger'"
@@ -4,11 +4,13 @@ import axios from "axios";
import { API_URL } from "@/config.js";
import { getError, parseError, removeError } from "@/components/request/HandleGlobalError.vue";
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import { economicEanPayloadValue, normalizeEconomicEan } from "@/services/economicCustomerIdentifiers.js";
const customerFormData = reactive({
cvr: null,
contactEmail: null,
contactPhone: null
contactPhone: null,
ean: null
});
const deliverSuccess = ref(false);
@@ -17,17 +19,24 @@ let deliverTimeoutId;
const submitCustomer = async () => {
removeError("authRegisterCvr");
try {
await axios.post(API_URL + "/auth/register/cvr", {
const payload = {
cvr: customerFormData.cvr,
invoiceEmail: customerFormData.contactEmail,
contactEmail: customerFormData.contactEmail,
contactPhone: 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.contactEmail = null;
customerFormData.cvr = null;
customerFormData.ean = null;
deliverSuccess.value = true;
deliverTimeoutId = setTimeout(() => {
@@ -59,6 +68,20 @@ onBeforeUnmount(() => {
>
</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">
<label for="email">{{ $t("customer_creation.customer.email_label") }}</label>
<input id="email" v-model="customerFormData.contactEmail" type="email" name="email" autocomplete="email">
@@ -5,15 +5,17 @@ import {
getProductsCategory,
loadOrderItems,
productCategoryAllowed,
isProductRestricted,
setProductsCategory,
getUserProductDiscount,
getUserProductPrice,
showFakeCreateOrderItem,
department_id,
user_discounts,
getUserGlobalDiscount,
getUserDiscounts,
customer_id,
customer_attributes
customer_attributes,
loadCustomerAttributes
} from "@/components/shop/POSDepartmentProcess.vue";
import {getProductCategory, getProducts} from "@/components/shop/Products.vue";
import {createOrderItem} from "@/components/shop/OrdersItems.vue";
@@ -151,6 +153,10 @@ const fetchProductsForCategory = async ({
products.value = [];
try {
if (customerNumber) {
await loadCustomerAttributes(customerNumber);
}
const response = normalizedCategory === null
? await getProducts(departmentId, props.finalPrice, customerNumber)
: await getProductCategory(normalizedCategory, departmentId, props.finalPrice, customerNumber);
@@ -549,10 +555,9 @@ const addPreviousOrderToCurrent = async (previousOrder) => {
continue;
}
const previousOrderProduct = getPreviousOrderProduct(orderItem);
const discount = getUserProductDiscount(previousOrderProduct || {});
const previousOrderProduct = getPreviousOrderProduct(orderItem) || { id: productId, price: 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);
await createOrderItem(orderId, productId, quantity);
@@ -586,14 +591,12 @@ const getRecommendedProductPrice = (productId) => {
return '0';
}
const discount = getUserProductDiscount(product);
const price = Number(product.price ?? 0);
return (price - (price * (discount / 100))).toFixed(0);
return String(getUserProductPrice(product));
};
const isRecommendedProductCategoryAllowed = (productId) => {
const product = getProductById(productId);
return !!product && productCategoryAllowed(product.category);
return !!product && !isProductRestricted(product);
};
const addRecommendedProductToOrder = async (productId) => {
@@ -405,6 +405,24 @@ watch(activeQuickAction, (nextMode, previousMode) => {
/>
</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">
<label class="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({
title: 'Edit User',
html: `
@@ -13,7 +15,7 @@ export const showEditUserForm = async (id, customer_number, display_name, role)
<div class="field">
<label class="label">Customer Number</label>
<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 class="field">
@@ -25,7 +27,7 @@ export const showEditUserForm = async (id, customer_number, display_name, role)
<div class="field">
<label class="label">Role</label>
<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 class="field">
@@ -50,4 +52,4 @@ export const showEditUserForm = async (id, customer_number, display_name, role)
});
}
</script>
</script>
@@ -75,19 +75,11 @@ const items = computed<NavigationItemProps[]>(() => [
* ]
* }
*/
{
label: t('templates.limited_backoffice.title'),
to: `/backoffice`,
type: 'link',
permissions: ['limited_backoffice_access'],
hidden: hasPermission('superuser'),
},
{
label: t('common.settings'),
to: `/user/profile`,
type: 'link',
permissions: ['user'],
hidden: !window.location.pathname.startsWith('/user')
hidden: !window.location.pathname.startsWith('/user') || !SessionUser.authenticated.value
},
{
label: t('common.logout'),
@@ -51,7 +51,7 @@ const items = computed<NavigationItemProps[]>(() => [
to: `/user`,
type: 'link',
// Dashboard visible to regular users and all subusers (subusers always have at least one grant)
permissions: ['user', 'BOOKINGS_LIST', 'ORDERS_LIST', 'VEHICLES_LIST', 'SELFSERVE_LIST', 'SUBUSERS_LIST'],
permissions: ['user', 'BOOKINGS_LIST', 'BOOKINGS_ADD', 'ORDERS_LIST', 'VEHICLES_LIST', 'VEHICLES_ADD', 'SELFSERVE_LIST', 'SUBUSERS_LIST'],
},
{
label: t('common.vehicles'),
@@ -1,17 +1,25 @@
<script setup>
const props = defineProps(['invoice_id'])
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import { handleEconomicError } from "@/components/request/HandleEconomicError.vue";
import { ref } from "vue";
import { computed } from "vue";
const props = defineProps(["invoice_id"]);
const hasInvoice = computed(() => {
return props.invoice_id !== null && props.invoice_id !== undefined && String(props.invoice_id).trim() !== "";
});
const getOrderPDF = async () => {
await authenticatedRequest('/invoices/pdf?id=' + props.invoice_id,
'GET',{
}) .then((response) => {
if (!hasInvoice.value) {
return;
}
await authenticatedRequest("/invoices/pdf", "GET", {
id: props.invoice_id,
}).then((response) => {
// Open the PDF in a new tab
window.open(response.data.data.url, '_blank');
window.open(response.data.data.url, "_blank");
}).catch((error) => {
handleEconomicError(error);
});
@@ -19,9 +27,17 @@ const getOrderPDF = async () => {
</script>
<template>
<LoadButtonWhileAwait class="is-dark" :loadFunction="getOrderPDF" icon="fas fa-file-pdf"> Hent faktura PDF </LoadButtonWhileAwait>
<LoadButtonWhileAwait
class="is-dark"
:loadFunction="getOrderPDF"
:disabled="!hasInvoice"
actionKey="economic-invoice-pdf-download"
icon="fas fa-file-pdf"
>
Hent faktura PDF
</LoadButtonWhileAwait>
</template>
<style scoped>
</style>
</style>
@@ -89,6 +89,66 @@ const SUBUSER_MANAGEMENT_PERMISSIONS = {
},
};
const CUSTOMER_FEATURE_PERMISSIONS = {
vehicles: {
list: {
customer: ["user", "list_own_vehicles"],
subuser: ["VEHICLES_LIST"],
},
add: {
customer: ["user", "add_vehicle"],
subuser: ["VEHICLES_ADD"],
},
edit: {
customer: ["edit_vehicle", "add_vehicle"],
subuser: ["VEHICLES_EDIT"],
},
delete: {
customer: ["delete_vehicle"],
subuser: ["VEHICLES_DELETE"],
},
},
bookings: {
list: {
customer: ["user", "list_own_bookings"],
subuser: ["BOOKINGS_LIST"],
},
add: {
customer: ["user"],
subuser: ["BOOKINGS_ADD"],
},
edit: {
customer: ["edit_own_bookings"],
subuser: ["BOOKINGS_EDIT"],
},
delete: {
customer: ["delete_own_bookings"],
subuser: ["BOOKINGS_DELETE"],
},
},
orders: {
list: {
customer: ["user", "list_own_orders"],
subuser: ["ORDERS_LIST"],
},
edit: {
customer: ["edit_own_orders"],
subuser: ["ORDERS_EDIT"],
},
},
selfserve: {
list: {
customer: ["user", "list_own_department_selfserve_vehicle_conditions"],
subuser: ["SELFSERVE_LIST"],
},
add: {
customer: ["add_own_department_selfserve_vehicle_conditions"],
subuser: ["SELFSERVE_ADD"],
},
},
subusers: SUBUSER_MANAGEMENT_PERMISSIONS,
};
const getStoredSessionSnapshot = () => {
if (typeof window === "undefined") {
return null;
@@ -858,6 +918,13 @@ export const SessionUser = {
}
return SessionUser.permissions.value.includes(permission) || SessionUser.permissions.value.includes("superuser");
},
hasAnyPermission: (permissions = []) => {
const permissionList = Array.isArray(permissions) ? permissions : [permissions].filter(Boolean);
if (permissionList.length === 0) {
return true;
}
return permissionList.some((permission) => SessionUser.hasPermission(permission));
},
/** Can access shortcuts */
canAccessAdmin: () => {
return SessionUser.hasPermission("admin") || SessionUser.hasPermission("superuser");
@@ -877,6 +944,15 @@ export const SessionUser = {
? SessionUser.hasPermission(permissions.subuser)
: SessionUser.hasPermission(permissions.customer);
},
canAccessCustomerFeature: (feature, action = "list") => {
const permissions = CUSTOMER_FEATURE_PERMISSIONS[feature]?.[action];
if (!permissions) {
return false;
}
const permissionList = SessionUser.isSubuser.value ? permissions.subuser : permissions.customer;
return SessionUser.hasAnyPermission(permissionList);
},
canAccessDepartment: (id = 0) => {
return SessionUser.hasPermission("department_access_" + parseInt(id)) || SessionUser.canAccessSuperUser();
},
@@ -444,10 +444,14 @@ export const CollectedOrderInvoices = {
});
},
split_by_month: async (dateFrom, dateTo, options = {}) => {
const invoiceCollectionIds = Array.isArray(options.invoiceCollectionIds)
? options.invoiceCollectionIds
: options.invoice_collection_ids;
return authenticatedRequest('/collected-invoices/split-by-month', 'POST', {
dateFrom,
dateTo,
...(options.preview !== undefined ? { preview: !!options.preview } : {}),
...(Array.isArray(invoiceCollectionIds) ? { invoice_collection_ids: invoiceCollectionIds } : {}),
}).then((response) => {
console.log(response);
return response;
@@ -164,6 +164,21 @@ const t = (key) => i18n.global.t(key);
}
);
},
setProductTarget: async ({ department_id, product_id, target_percentage }) => {
const normalizedTarget = target_percentage === null || target_percentage === undefined || target_percentage === ""
? null
: Number(target_percentage);
return authenticatedRequest(
"/departments/daily-reports/product-targets",
"PUT",
{
department_id: parseInt(department_id, 10),
product_id: parseInt(product_id, 10),
target_percentage: normalizedTarget,
}
);
},
createComplaint: async ({
department_id,
customer_number = null,
@@ -108,6 +108,14 @@ export const Departments = {
required: false,
},
},
custom_pricing_only: {
label: t("objects.departments.columns.custom_pricing_only"),
type: "boolean",
sortable: true,
creation: {
required: false,
},
},
longitude: {
label: t("objects.departments.columns.longitude"),
type: "number",
@@ -164,6 +172,14 @@ export const Departments = {
archived: async (id, archived) => {
return ObjectsGlobal.set.column(Departments.meta.endpoint, id, "archived", ObjectsGlobal.parse.boolean(archived));
},
custom_pricing_only: async (id, custom_pricing_only) => {
return ObjectsGlobal.set.column(
Departments.meta.endpoint,
id,
"custom_pricing_only",
ObjectsGlobal.parse.boolean(custom_pricing_only)
);
},
longitude: async (id, longitude) => {
return ObjectsGlobal.set.column(Departments.meta.endpoint, id, "longitude", parseFloat(longitude));
},
@@ -32,6 +32,23 @@ const normalizeDateTimeLocalValue = (value) => {
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.
*/
@@ -431,20 +448,15 @@ export const ObjectsGlobal = {
* @returns {Promise} The promise
*/
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)) {
return ObjectsGlobal.cache.get(cacheKey);
}
let requestEndpoint = endpoint;
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}`;
}
const requestEndpoint = `${endpoint}${buildQueryString(requestParams)}`;
let options = ObjectsGlobal.requestOptions.applyOptions(optionsObject);
let promise;
if (options.authenticated) {
@@ -499,23 +511,24 @@ export const ObjectsGlobal = {
* @returns {Promise} The promise
*/
objects: async (endpoint, data = {}) => {
const cacheKey = JSON.stringify({endpoint, data});
if (ObjectsGlobal.cache.has(cacheKey)) {
return ObjectsGlobal.cache.get(cacheKey);
}
if (data.filters && typeof data.filters === 'object') {
data.filters = Object.entries(data.filters).flatMap(([key, value]) => {
const requestData = sanitizeQueryParams(data);
if (requestData.filters && typeof requestData.filters === 'object') {
requestData.filters = Object.entries(requestData.filters).flatMap(([key, value]) => {
if (Array.isArray(value)) {
return value.map(v => `${key}:${v}`);
}
return `${key}:${value}`;
}).join(',');
}
const cacheKey = JSON.stringify({endpoint, data: requestData});
if (ObjectsGlobal.cache.has(cacheKey)) {
return ObjectsGlobal.cache.get(cacheKey);
}
const promise = authenticatedRequest(
endpoint,
"GET",
data
requestData
).then((response) => {
return response.data.data;
}).catch((error) => {
@@ -8,6 +8,17 @@ import i18n from '@/i18n';
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
* @type {ref<null>}
@@ -269,19 +280,19 @@ export const Products = {
},
},
get: {
all: async (options = {department_id: null, customer_id: null, category_id: null, final_price: false}) => {
return ObjectsGlobal.get.objects(Products.meta.endpoint, {...options});
all: async (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}) => {
return ObjectsGlobal.get.object(Products.meta.endpoint, {id: id, ...options});
single: async (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) => {
return ObjectsGlobal.get.object(Products.meta.endpoint, {
return ObjectsGlobal.get.object(Products.meta.endpoint, normalizeProductQueryOptions({
category: category_id,
department_id: department_id,
...(customer_id !== null ? {customer_id: customer_id} : {}),
...{final_price: final_price}
});
}));
},
},
delete: async (id) => {
@@ -324,4 +335,4 @@ export const Products = {
);
},
};
</script>
</script>
@@ -1,5 +1,4 @@
<script>
import Swal from "sweetalert2";
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
@@ -123,6 +122,10 @@ export const Roles = {
id: id
});
},
getLimitedBackofficePermissionTemplates: async () => {
const response = await authenticatedRequest("/roles/limited-backoffice-permission-templates", "GET");
return response?.data?.data ?? response?.data ?? [];
},
permissions: {
add: async (roleId, permissionId) => {
return authenticatedRequest("/roles/permissions", "POST", {
@@ -161,4 +164,4 @@ export const Roles = {
);
}
};
</script>
</script>
+59 -54
View File
@@ -10,6 +10,10 @@ import { getAttributes } from "@/components/shop/CustomerAttributes.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import { doesOrderContainWashCertificateProduct } from "@/components/displays/department/pos/utils/washCertificate.js";
import {
isProductCategoryRestrictedForCustomer,
isProductRestrictedForCustomer,
} from "@/features/customer/customerProductRules.js";
import Swal from "sweetalert2";
/**
@@ -153,6 +157,8 @@ export const nextStep = async (options = { isMobile: false, orderCreation: true
clearErrors();
// If the current step is 1, create the order
if (step.value === 1) {
await loadCustomerAttributes(customer_id.value);
if (normalizedOptions.orderCreation === false) {
step.value++;
return;
@@ -1613,24 +1619,7 @@ export const customerUsesPONumbers = () => {
/** Check if the product category is allowed */
export const productCategoryAllowed = (category) => {
// Check if the product category is the addons category
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;
return !isProductCategoryRestrictedForCustomer(category, customer_attributes.value);
};
/** 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 */
export const isProductRestricted = (product) => {
if (!product) return false;
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;
return isProductRestrictedForCustomer(product, customer_attributes.value);
};
/** Check if an addon is restricted based on customer attributes */
export const isAddonRestricted = (addon) => {
if (!addon) return false;
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;
return isProductRestrictedForCustomer(addon, customer_attributes.value);
};
export function loadOrderDetails(onAfterSuccess = null, options = {}) {
@@ -1844,13 +1802,30 @@ export const getUserDiscounts = async () => {
});
};
/** Get user product discount */
export const getUserProductDiscount = (product, allowCategory = null, onlyCategory = null) => {
// Check if the user discounts are loaded
const ensureUserDiscountsLoaded = () => {
if (!has_user_discounts_loaded.value) {
has_user_discounts_loaded.value = true;
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
const productId = product.id;
@@ -1948,6 +1923,36 @@ export const getUserProductDiscount = (product, allowCategory = null, onlyCatego
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) => {
order_items.value.push({
id: Math.floor(Math.random() * 1000),
@@ -6,9 +6,8 @@ import CollectedInvoiceQueueMonitor from "@/components/viewport/page/headers/men
import SubuserGrantSelector from "@/components/session/subuser/SubuserGrantSelector.vue";
import { IS_DEV } from '@/config.js';
import SessionUser from "@/components/session/token/SessionUser.vue";
import { useRoute } from "vue-router";
import { computed } from "vue";
const route = useRoute();
import HeaderAccessShortcuts from "@/components/viewport/page/headers/HeaderAccessShortcuts.vue";
const isAuthenticatedSessionReady = computed(() => SessionUser.isInitiated() && SessionUser.authenticated.value);
</script>
@@ -41,20 +40,7 @@ const isAuthenticatedSessionReady = computed(() => SessionUser.isInitiated() &&
<!-- User account name and avatar (Desktop) -->
<div class="navbar-item mr-1">
<div class="buttons">
<!-- Back-Office button -->
<router-link class="button is-white" to="/superuser" v-if="SessionUser.canAccessSuperUser() && !route.path.includes('/superuser')">
<span class="icon">
<i class="fas fa-tools" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
</span>
<span>{{ $t('header.backoffice') }}</span>
</router-link>
<!-- Department button -->
<router-link class="button is-white" to="/admin" v-if="SessionUser.canAccessAdmin() && !route.path.includes('/admin')">
<span class="icon">
<i class="fas fa-building" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
</span>
<span>{{ $t('common.departments') }}</span>
</router-link>
<HeaderAccessShortcuts />
<!-- Return to own account if impersonating -->
<template v-if="SessionUser.hasSuperUserToken()">
<router-link class="button is-white" to="/auth/return">
@@ -63,11 +63,7 @@ const navigationItems = computed<NavigationMenuItem[]>(() => {
});
const hasPermission = (item: NavigationMenuItem) => {
if (!item.permissions?.length) {
return true;
}
return item.permissions.some((permission) => SessionUser.hasPermission(permission));
return SessionUser.hasAnyPermission(item.permissions ?? []);
};
const isItemVisible = (item: NavigationMenuItem) => !item.hidden && hasPermission(item);
@@ -0,0 +1,99 @@
<script setup>
import { computed } from "vue";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { IS_DEV } from "@/config.js";
import SessionUser from "@/components/session/token/SessionUser.vue";
import {
getAdminDepartmentIdFromRoute,
getAdminDepartmentRoute,
getLimitedBackofficeDepartmentIdFromRoute,
getLimitedBackofficeDepartmentRoute,
} from "@/components/viewport/page/headers/headerShortcuts.js";
const route = useRoute();
const { t } = useI18n({ useScope: "global" });
const adminDepartmentId = computed(() => getAdminDepartmentIdFromRoute(route));
const backofficeDepartmentId = computed(() => getLimitedBackofficeDepartmentIdFromRoute(route));
const canAccessDepartment = (departmentId) =>
Number.isInteger(departmentId) && departmentId > 0 && SessionUser.canAccessDepartment(departmentId);
const canShowSuperuserBackoffice = computed(
() => SessionUser.canAccessSuperUser() && !route.path.includes("/superuser")
);
const canShowLimitedBackoffice = computed(() => {
const departmentId = adminDepartmentId.value;
return (
canAccessDepartment(departmentId) &&
SessionUser.hasPermission("limited_backoffice_access") &&
!SessionUser.canAccessSuperUser()
);
});
const limitedBackofficeTarget = computed(() => getLimitedBackofficeDepartmentRoute(adminDepartmentId.value));
const adminShortcutTarget = computed(() =>
backofficeDepartmentId.value ? getAdminDepartmentRoute(backofficeDepartmentId.value) : "/admin"
);
const canShowAdminShortcut = computed(() => {
if (!SessionUser.canAccessAdmin() || route.path.includes("/admin")) {
return false;
}
const departmentId = backofficeDepartmentId.value;
return departmentId === null || canAccessDepartment(departmentId);
});
</script>
<template>
<router-link
v-if="canShowSuperuserBackoffice"
class="button is-white"
to="/superuser"
data-testid="superuser-backoffice-header-button"
>
<span class="icon">
<i class="fas fa-tools" :class="{ 'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV }"></i>
</span>
<span>{{ t("header.backoffice") }}</span>
</router-link>
<router-link
v-if="canShowLimitedBackoffice"
class="button is-white"
:to="limitedBackofficeTarget"
data-testid="limited-backoffice-header-button"
>
<span class="icon">
<i class="fas fa-tools" :class="{ 'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV }"></i>
</span>
<span>{{ t("templates.limited_backoffice.title") }}</span>
</router-link>
<router-link
v-if="canShowAdminShortcut"
class="button is-white"
:to="adminShortcutTarget"
data-testid="admin-header-button"
>
<span class="icon">
<i class="fas fa-building" :class="{ 'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV }"></i>
</span>
<span>{{ t("common.departments") }}</span>
</router-link>
</template>
<style scoped>
.transform-color-black {
filter: invert(100%) grayscale(100%) brightness(0) contrast(100%);
}
.transform-color-red {
filter: invert(27%) sepia(96%) saturate(7493%) hue-rotate(357deg) brightness(103%) contrast(101%);
}
</style>
@@ -5,9 +5,8 @@ import NavigationMenuGlobalSearch from "@/components/viewport/page/headers/menu/
import CollectedInvoiceQueueMonitor from "@/components/viewport/page/headers/menu/CollectedInvoiceQueueMonitor.vue";
import { IS_DEV } from '@/config.js';
import SessionUser from "@/components/session/token/SessionUser.vue";
import { useRoute } from "vue-router";
import { computed } from "vue";
const route = useRoute();
import HeaderAccessShortcuts from "@/components/viewport/page/headers/HeaderAccessShortcuts.vue";
const isAuthenticatedSessionReady = computed(() => SessionUser.isInitiated() && SessionUser.authenticated.value);
</script>
@@ -41,20 +40,7 @@ const isAuthenticatedSessionReady = computed(() => SessionUser.isInitiated() &&
<!-- User account name and avatar (Desktop) -->
<div class="navbar-item mr-1">
<div class="buttons">
<!-- Back-Office button -->
<router-link class="button is-white" to="/superuser" v-if="SessionUser.canAccessSuperUser() && !route.path.includes('/superuser')">
<span class="icon">
<i class="fas fa-tools" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
</span>
<span>{{ $t('header.backoffice') }}</span>
</router-link>
<!-- Department button -->
<router-link class="button is-white" to="/admin" v-if="SessionUser.canAccessAdmin() && !route.path.includes('/admin')">
<span class="icon">
<i class="fas fa-building" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
</span>
<span>{{ $t('common.departments') }}</span>
</router-link>
<HeaderAccessShortcuts />
<!-- Return to own account if impersonating -->
<template v-if="SessionUser.hasSuperUserToken()">
<router-link class="button is-white" to="/auth/return">
@@ -0,0 +1,31 @@
const parsePositiveInteger = (value) => {
const parsedValue = Number.parseInt(String(value ?? ""), 10);
return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null;
};
const getPath = (route) => String(route?.path ?? "");
export const getAdminDepartmentIdFromRoute = (route) => {
const path = getPath(route);
const fromPath = parsePositiveInteger(path.match(/^\/admin\/(\d+)(?:\/|$)/)?.[1]);
if (fromPath !== null) {
return fromPath;
}
return path.startsWith("/admin") ? parsePositiveInteger(route?.params?.departmentId) : null;
};
export const getLimitedBackofficeDepartmentIdFromRoute = (route) => {
const path = getPath(route);
const fromPath = parsePositiveInteger(path.match(/^\/backoffice\/departments\/(\d+)(?:\/|$)/)?.[1]);
if (fromPath !== null) {
return fromPath;
}
return path.startsWith("/backoffice/departments") ? parsePositiveInteger(route?.params?.departmentId) : null;
};
export const getLimitedBackofficeDepartmentRoute = (departmentId) =>
`/backoffice/departments/${encodeURIComponent(String(departmentId))}/prices`;
export const getAdminDepartmentRoute = (departmentId) => `/admin/${encodeURIComponent(String(departmentId))}`;
@@ -43,23 +43,24 @@ const redirect = async (to: string) => {
}
};
// Permissions and visibility logic
const hasPermission = (permission: string) => {
return SessionUser.hasPermission(permission);
}
const hasDepartmentIdInPath = (path: string) => {
const departmentRegex = /\/admin\/\d+/;
return departmentRegex.test(path);
}
const hasItemPermissions = (item: any) => {
return SessionUser.hasAnyPermission(item?.permissions ?? []);
}
const hasPermissions = computed(() => {
if (!props.item.permissions || props.item.permissions.length === 0) {
return true; // No permissions required, item is visible
}
return props.item.permissions.some(hasPermission);
return hasItemPermissions(props.item);
});
const isVisible = () => {
if (props.item.hidden) {
return false;
}
switch (props.item.type) {
case 'category':
// Check if any child item is visible
@@ -96,10 +97,14 @@ const hasChildren = computed(() => {
});
const isChildVisible = (child: any) => {
if (child.type === 'department') {
return hasPermissions.value && hasDepartmentIdInPath(router.currentRoute.value.path);
if (child.hidden) {
return false;
}
return hasPermissions.value;
if (child.type === 'department') {
return hasItemPermissions(child) && hasDepartmentIdInPath(router.currentRoute.value.path);
}
return hasItemPermissions(child);
}
const hasVisibleChildren = computed(() => {
@@ -189,7 +189,7 @@ const descriptionKeyOverrides: Partial<Record<EntityType, string[]>> = {
xlvask_customers: ['email', 'city', 'address', 'vatnumber'],
motorapi_lookups: ['endpoint', 'license_plate', 'result'],
fxratesapi_conversion_rates: ['base', 'target', 'endpoint', 'result'],
customer_discounts: ['customer_number', 'percentage'],
customer_discounts: ['customer_number', 'fixed_price', 'percentage'],
module_config: ['description', 'value'],
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(
field('Discount', 'discount', 'percentage'),
field('Fixed price', 'fixed_price'),
field('Target', 'product_or_category_id', 'object_id'),
field('Category', 'is_category'),
field('Customer #', 'customer_number'),
+9 -2
View File
@@ -2,6 +2,7 @@ import { computed, onBeforeUnmount, reactive, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { searchAndSelectCustomer } from "@/components/shop/POSDepartmentProcess.vue";
import SessionUser from "@/components/session/token/SessionUser.vue";
import { economicEanPayloadValue } from "@/services/economicCustomerIdentifiers.js";
import {
applyPosAddCustomerSearchResult,
createPosAddCustomerFields,
@@ -152,14 +153,20 @@ export function usePosAddCustomerForm(options = {}) {
errorMessage.value = null;
try {
await SessionUser.request("/auth/register/cvr", "POST", {
const payload = {
cvr: form.cvr,
companyPhone: Number.parseInt(form.companyPhone, 10),
invoiceEmail: form.invoiceEmail,
contactEmail: form.contactEmail,
contactPhone: Number.parseInt(form.contactPhone, 10),
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 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;
};
+244 -7
View File
@@ -1176,6 +1176,9 @@
"title": "@:{'templates.generated.compat.limited_backoffice.prices.title'}",
"save": "@:{'templates.generated.compat.limited_backoffice.prices.save'}",
"saved": "@:{'templates.generated.compat.limited_backoffice.prices.saved'}",
"autosave_saving": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_saving'}",
"autosave_pending": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_pending'}",
"autosave_saved": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_saved'}",
"product": "@:{'templates.generated.compat.limited_backoffice.prices.product'}",
"price": "@:{'templates.generated.compat.limited_backoffice.prices.price'}",
"price_required": "@:{'templates.generated.compat.limited_backoffice.prices.price_required'}",
@@ -1190,6 +1193,15 @@
"edit_title": "@:{'templates.generated.compat.limited_backoffice.employees.edit_title'}",
"name": "@:{'templates.generated.compat.limited_backoffice.employees.name'}",
"email": "@:{'templates.generated.compat.limited_backoffice.employees.email'}",
"user_id": "@:{'templates.generated.compat.limited_backoffice.employees.user_id'}",
"phone": "@:{'templates.generated.compat.limited_backoffice.employees.phone'}",
"phone_validation": "@:{'templates.generated.compat.limited_backoffice.employees.phone_validation'}",
"country_codes": {
"denmark": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.denmark'}",
"sweden": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.sweden'}",
"norway": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.norway'}",
"finland": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.finland'}"
},
"password": "@:{'templates.generated.compat.limited_backoffice.employees.password'}",
"password_create_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_create_help'}",
"password_update_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_update_help'}",
@@ -1208,7 +1220,13 @@
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
"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": {
"viewer": {
@@ -1238,7 +1256,8 @@
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
"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": {
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
@@ -1251,6 +1270,11 @@
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"products": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.products'}",
"customers": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.customers'}",
"vehicles": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.vehicles'}",
"attachments": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.attachments'}",
"scanner": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.scanner'}",
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
@@ -1302,6 +1326,66 @@
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"complete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
},
"view_product_catalog": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
},
"view_product_recommendations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
},
"search_customers": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
},
"view_customer_details": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
},
"view_customer_notes": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
},
"add_customer_notes": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
},
"view_customer_flags": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
},
"search_vehicles": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
},
"view_vehicle_matches": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
},
"view_vehicle_history": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
},
"view_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
},
"add_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
},
"download_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
},
"view_plate_scans": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
},
"view_department_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
@@ -1498,6 +1582,14 @@
"subtitle_suffix": "@:{'words.generated.salg'} @:{'words.generated.af'} produktet",
"title": "Produktsalg"
},
"product_targets": {
"cancel_aria": "Annuller mål",
"edit_aria": "Rediger målprocent",
"input_label": "Målprocent",
"save_aria": "Gem mål",
"save_error": "Målet kunne ikke gemmes.",
"validation_range": "Målprocenten skal være mellem 0 og 100."
},
"reports": {
"bookings": {
"completed_count": "@.capitalize:{'words.generated.antal'} @:{'words.generated.fuldførte'}",
@@ -1692,6 +1784,7 @@
"company_phone": "Virksomhedstelefon",
"contact_email": "@.capitalize:{'words.generated.kontakt'}-@:{'words.generated.e'}-@:{'words.generated.mail'}",
"contact_phone": "Kontakttelefon",
"ean": "EAN",
"created_by": "@.capitalize:{'words.generated.oprettet'} @:{'words.generated.af'}",
"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'}.",
@@ -3240,6 +3333,7 @@
"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_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'}",
"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'}",
@@ -3455,6 +3549,13 @@
"opening_hours": {
"closed": "Stengt"
},
"pricing": {
"custom_pricing_disabled": "Fallback aktiv",
"custom_pricing_enabled": "Kun egne priser",
"custom_pricing_missing_price": "Manglende afdelingspriser bliver 999999.",
"custom_pricing_only": "Ingen fallback-priser",
"effective_department_price": "Effektiv afdelingspris"
},
"search_placeholder": "@.capitalize:{'words.generated.søg'} @:{'words.generated.efter'} @:{'words.generated.afdelingsnavn'}",
"select_department": "@.capitalize:{'words.generated.vælg'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.afdeling'}",
"subtitle": "@.capitalize:{'words.generated.administrer'} @:{'words.generated.afdelinger'}",
@@ -4028,6 +4129,15 @@
"preview_title": "@:{'words.generated.forhandsvis'} @:{'words.generated.manedsopdeling'}",
"success_text": "@:{'words.generated.behandlede'} {processed} @:{'words.generated.fakturasamlinger'}. Opdelte {changed} @:{'words.generated.og'} sprang {skipped} @:{'words.generated.over'}.",
"success_title": "@.capitalize:{'words.generated.manedsopdeling'} @:{'words.generated.fuldført'}"
},
"multi_month_invoice_warning": {
"invoice_together": "Fakturer sammen",
"split_by_month": "Opdel efter måned",
"split_error_title": "Månedsopdeling mislykkedes",
"split_success_text": "Behandlede {processed} fakturasamlinger. Opdelte {changed} og sprang {skipped} over.",
"split_success_title": "Månedsopdeling fuldført",
"text": "Du er ved at fakturere ordrer fra flere måneder sammen ({months}). Skal de i stedet opdeles efter måned?",
"title": "Ordrer fra flere måneder"
}
},
"invoicing": {
@@ -4285,6 +4395,7 @@
"departments": {
"columns": {
"archived": "Arkiveret",
"custom_pricing_only": "Ingen fallback-priser",
"dimension": "Dimension",
"economic_department": "@.upper:{'words.generated.e'}-@:{'words.generated.conomic'} @:{'words.generated.afdeling'}",
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
@@ -4937,6 +5048,22 @@
"title": "Tilladelser for rolle {id}",
"title_with_name": "Tilladelser for {name}"
},
"templates": {
"apply": "Anvend {template}-skabelon",
"confirm_button": "Anvend skabelon",
"confirm_text": "Tilfoej {count} manglende tilladelser fra {template} til {role}?",
"confirm_title": "Anvend tilladelsesskabelon?",
"error_text": "En eller flere tilladelser fra {template} kunne ikke tilfoejes til {role}. Rollelisten blev genindlaest.",
"error_title": "Kunne ikke anvende skabelon",
"load_error": "Kunne ikke indlaese skabeloner. Proev igen",
"loading": "Indlaeser skabeloner...",
"section": "Begraenset backoffice-skabeloner",
"success_text": "Tilfoejede {count} tilladelser fra {template} til {role}.",
"success_title": "Skabelon anvendt",
"unavailable": "Ingen skabeloner er tilgaengelige.",
"up_to_date_text": "{role} har allerede alle tilladelser i {template}.",
"up_to_date_title": "Rollen er opdateret"
},
"subtitle": "@.capitalize:{'words.generated.brugerroller'}"
},
"self_wash": {
@@ -5346,6 +5473,7 @@
},
"employees": {
"create": "@:{'words.generated.opret'} @:{'words.generated.ny'} @:{'words.generated.medarbejder'}",
"migrate_to_limited_backoffice": "Migrer til begrænset backoffice",
"subtitle": "@.capitalize:{'words.generated.her'} @:{'words.generated.kan'} @:{'words.generated.du'} @:{'words.generated.se'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.liste'} @:{'words.generated.over'} @:{'words.generated.alle'} @:{'words.generated.medarbejdere'} @:{'words.generated.i'} @:{'words.generated.systemet'}"
},
"orders": {
@@ -5726,6 +5854,31 @@
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
},
"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'}",
"user_data": "@.capitalize:{'words.generated.brugerdata'}",
"user_id": "@.capitalize:{'words.generated.bruger'} @.upper:{'words.generated.id'}",
@@ -6010,6 +6163,9 @@
"title": "@.capitalize:{'words.generated.afdelingspriser'}",
"save": "@:{'words.generated.gem'} @:{'words.generated.priser'}",
"saved": "Priserne @:{'words.generated.er'} @:{'words.generated.gemt'}.",
"autosave_saving": "Gemmer priser...",
"autosave_pending": "Prisændringer afventer.",
"autosave_saved": "Alle prisændringer er gemt.",
"product": "Produkt",
"price": "Afdelingspris",
"price_required": "@.capitalize:{'words.generated.alle'} @:{'words.generated.produkter'} @:{'words.generated.skal'} have @:{'words.replication.host_definite_suffix'} @:{'words.generated.ikke'}-negativ numerisk pris.",
@@ -6024,6 +6180,15 @@
"edit_title": "@:{'words.generated.rediger'} @:{'words.generated.medarbejder'}",
"name": "Navn",
"email": "Email",
"user_id": "Bruger-id",
"phone": "Telefon",
"phone_validation": "Telefonnummeret skal være 4-15 cifre.",
"country_codes": {
"denmark": "Danmark",
"sweden": "Sverige",
"norway": "Norge",
"finland": "Finland"
},
"password": "@.capitalize:{'words.generated.adgangskode'}",
"password_create_help": "@.capitalize:{'words.generated.pakrævet'} @:{'words.generated.for'} nye @:{'words.generated.medarbejdere'}. Brug @:{'words.generated.mindst'} 8 tegn.",
"password_update_help": "Lad feltet være tomt @:{'words.generated.for'} at beholde den nuværende @:{'words.generated.adgangskode'}.",
@@ -6042,16 +6207,22 @@
"active": "Aktiv",
"inactive": "Inaktiv",
"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": {
"viewer": {
"label": "Viser",
"description": "@.capitalize:{'words.generated.kan'} logge ind @:{'words.generated.og'} se data @:{'words.generated.for'} @:{'words.generated.tildelte'} @:{'words.generated.afdelinger'}."
"label": "Deaktiveret",
"description": "Holder medarbejderen registreret uden adgang til ordre-, booking- eller administrationsfunktioner."
},
"cashier": {
"label": "Kassemedarbejder",
"description": "@.capitalize:{'words.generated.kan'} arbejde med @:{'words.generated.ordrer'} @:{'words.generated.og'} ordrelinjer @:{'words.generated.for'} @:{'words.generated.tildelte'} @:{'words.generated.afdelinger'}."
"description": "@.capitalize:{'words.generated.kan'} arbejde med @:{'words.generated.ordrer'}, ordrelinjer @:{'words.generated.og'} @:{'words.generated.bookinger'} @:{'words.generated.for'} @:{'words.generated.tildelte'} @:{'words.generated.afdelinger'}."
},
"booking_coordinator": {
"label": "Bookingkoordinator",
@@ -6072,7 +6243,8 @@
"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'}.",
"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": {
"title": "Rolletilladelser",
@@ -6085,6 +6257,11 @@
"groups": {
"account": "Konto",
"orders": "Ordrer",
"products": "Produkter",
"customers": "Kunder",
"vehicles": "Køretøjer",
"attachments": "Vedhæftninger",
"scanner": "Scannere",
"bookings": "Bookinger",
"time_bookings": "Tidsbookinger",
"reports": "Rapporter",
@@ -6136,6 +6313,66 @@
"label": "Opkræv ordrer",
"description": "Kan tage betaling eller opkræve en betalingsreservation for en ordre."
},
"complete_orders": {
"label": "Afslut ordrer",
"description": "Kan markere ordrer som afsluttede."
},
"view_product_catalog": {
"label": "Se produktkatalog",
"description": "Kan indlæse POS-kategorier, produkter, priser og tilvalg."
},
"view_product_recommendations": {
"label": "Se produktforslag",
"description": "Kan se produktforslag baseret på ordrer og køretøjsdata."
},
"search_customers": {
"label": "Søg kunder",
"description": "Kan søge og vælge kunder til POS-ordrer."
},
"view_customer_details": {
"label": "Se kundeoplysninger",
"description": "Kan indlæse kundeoplysninger fra et kundenummer."
},
"view_customer_notes": {
"label": "Se kundebemærkninger",
"description": "Kan læse kundebemærkninger i POS."
},
"add_customer_notes": {
"label": "Tilføj kundebemærkninger",
"description": "Kan tilføje kundebemærkninger fra POS."
},
"view_customer_flags": {
"label": "Se kundemarkeringer",
"description": "Kan se kundemarkeringer, der påvirker ordrekrav og produktregler."
},
"search_vehicles": {
"label": "Søg køretøjer",
"description": "Kan søge nummerplader og se køretøjsstatus."
},
"view_vehicle_matches": {
"label": "Se køretøjsmatch",
"description": "Kan se kundematch for køretøjer og forslag til ukendte køretøjer."
},
"view_vehicle_history": {
"label": "Se køretøjshistorik",
"description": "Kan se seneste ordrehistorik for en nummerplade."
},
"view_order_attachments": {
"label": "Se ordrevedhæftninger",
"description": "Kan liste vedhæftninger på ordrer."
},
"add_order_attachments": {
"label": "Tilføj ordrevedhæftninger",
"description": "Kan uploade filer og vedhæfte vaskecertifikater til ordrer."
},
"download_order_attachments": {
"label": "Download ordrevedhæftninger",
"description": "Kan åbne og downloade ordrevedhæftninger."
},
"view_plate_scans": {
"label": "Se nummerpladescanninger",
"description": "Kan se seneste nummerpladescanninger og scannere for tildelte afdelinger."
},
"view_department_bookings": {
"label": "Se afdelingsbookinger",
"description": "Kan se bookinger for tildelte afdelinger."
+258 -7
View File
@@ -1287,6 +1287,9 @@
"title": "@:{'templates.generated.compat.limited_backoffice.prices.title'}",
"save": "@:{'templates.generated.compat.limited_backoffice.prices.save'}",
"saved": "@:{'templates.generated.compat.limited_backoffice.prices.saved'}",
"autosave_saving": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_saving'}",
"autosave_pending": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_pending'}",
"autosave_saved": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_saved'}",
"product": "@:{'templates.generated.compat.limited_backoffice.prices.product'}",
"price": "@:{'templates.generated.compat.limited_backoffice.prices.price'}",
"price_required": "@:{'templates.generated.compat.limited_backoffice.prices.price_required'}",
@@ -1301,6 +1304,15 @@
"edit_title": "@:{'templates.generated.compat.limited_backoffice.employees.edit_title'}",
"name": "@:{'templates.generated.compat.limited_backoffice.employees.name'}",
"email": "@:{'templates.generated.compat.limited_backoffice.employees.email'}",
"user_id": "@:{'templates.generated.compat.limited_backoffice.employees.user_id'}",
"phone": "@:{'templates.generated.compat.limited_backoffice.employees.phone'}",
"phone_validation": "@:{'templates.generated.compat.limited_backoffice.employees.phone_validation'}",
"country_codes": {
"denmark": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.denmark'}",
"sweden": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.sweden'}",
"norway": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.norway'}",
"finland": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.finland'}"
},
"password": "@:{'templates.generated.compat.limited_backoffice.employees.password'}",
"password_create_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_create_help'}",
"password_update_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_update_help'}",
@@ -1319,7 +1331,13 @@
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
"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": {
"viewer": {
@@ -1349,7 +1367,8 @@
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
"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": {
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
@@ -1362,6 +1381,11 @@
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"products": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.products'}",
"customers": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.customers'}",
"vehicles": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.vehicles'}",
"attachments": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.attachments'}",
"scanner": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.scanner'}",
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
@@ -1413,6 +1437,66 @@
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"complete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
},
"view_product_catalog": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
},
"view_product_recommendations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
},
"search_customers": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
},
"view_customer_details": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
},
"view_customer_notes": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
},
"add_customer_notes": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
},
"view_customer_flags": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
},
"search_vehicles": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
},
"view_vehicle_matches": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
},
"view_vehicle_history": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
},
"view_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
},
"add_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
},
"download_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
},
"view_plate_scans": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
},
"view_department_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
@@ -1609,6 +1693,14 @@
"subtitle_suffix": "Produktverk?@:{'words.generated.ufe'}",
"title": "@:{'templates.generated.compat.admin.daily_report.product_sales.subtitle_suffix'}"
},
"product_targets": {
"cancel_aria": "Ziel abbrechen",
"edit_aria": "Zielprozentsatz bearbeiten",
"input_label": "Zielprozentsatz",
"save_aria": "Ziel speichern",
"save_error": "Das Ziel konnte nicht gespeichert werden.",
"validation_range": "Der Zielprozentsatz muss zwischen 0 und 100 liegen."
},
"reports": {
"bookings": {
"completed_count": "@:{'words.generated.abgeschlossene'} @:{'words.generated.anzahl'}",
@@ -1803,6 +1895,7 @@
"company_phone": "Firmentelefon",
"contact_email": "@:{'words.generated.kontakt'}-@.upper:{'words.generated.e'}-@:{'words.generated.mail'}",
"contact_phone": "Kontakttelefonnummer",
"ean": "EAN",
"created_by": "@.capitalize:{'words.generated.erstellt'} @:{'words.generated.von'}",
"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'}.",
@@ -3351,6 +3444,7 @@
"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_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'}",
"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'}",
@@ -3566,6 +3660,13 @@
"opening_hours": {
"closed": "@:{'templates.generated.compat.global.closed'}"
},
"pricing": {
"custom_pricing_disabled": "Fallback aktiviert",
"custom_pricing_enabled": "Nur eigene Preise",
"custom_pricing_missing_price": "Fehlende Abteilungspreise werden zu 999999.",
"custom_pricing_only": "Keine Fallback-Preise",
"effective_department_price": "Effektiver Abteilungspreis"
},
"search_placeholder": "@.capitalize:{'words.generated.nach'} @:{'words.generated.abteilungsname'} @:{'words.generated.suchen'}",
"select_department": "@:{'templates.generated.compat.nav.select_department'}",
"subtitle": "@:{'words.generated.abteilungen'} @:{'words.generated.verwalten'}",
@@ -4139,6 +4240,15 @@
"preview_title": "Preview @:{'words.generated.monthly'} @:{'words.generated.split'}",
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
},
"multi_month_invoice_warning": {
"invoice_together": "Zusammen abrechnen",
"split_by_month": "Nach Monat aufteilen",
"split_error_title": "Monatsaufteilung fehlgeschlagen",
"split_success_text": "{processed} Rechnungssammlungen verarbeitet. {changed} aufgeteilt, {skipped} übersprungen.",
"split_success_title": "Monatsaufteilung abgeschlossen",
"text": "Sie sind dabei, Aufträge aus mehreren Monaten gemeinsam abzurechnen ({months}). Sollen sie stattdessen nach Monat aufgeteilt werden?",
"title": "Aufträge aus mehreren Monaten"
}
},
"invoicing": {
@@ -4396,6 +4506,7 @@
"departments": {
"columns": {
"archived": "Archiviert",
"custom_pricing_only": "@:{'templates.generated.compat.departments.pricing.custom_pricing_only'}",
"dimension": "Abmessung",
"economic_department": "@:{'templates.generated.compat.objects.economic_departments.single'}",
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
@@ -5048,6 +5159,22 @@
"title": "Berechtigungen fuer Rolle {id}",
"title_with_name": "Berechtigungen fuer {name}"
},
"templates": {
"apply": "{template}-Vorlage anwenden",
"confirm_button": "Vorlage anwenden",
"confirm_text": "{count} fehlende Berechtigungen aus {template} zu {role} hinzufuegen?",
"confirm_title": "Berechtigungsvorlage anwenden?",
"error_text": "Eine oder mehrere Berechtigungen aus {template} konnten {role} nicht hinzugefuegt werden. Die Rollenliste wurde neu geladen.",
"error_title": "Vorlage konnte nicht angewendet werden",
"load_error": "Vorlagen konnten nicht geladen werden. Erneut versuchen",
"loading": "Vorlagen werden geladen...",
"section": "Limited-Backoffice-Vorlagen",
"success_text": "{count} Berechtigungen aus {template} zu {role} hinzugefuegt.",
"success_title": "Vorlage angewendet",
"unavailable": "Keine Vorlagen verfuegbar.",
"up_to_date_text": "{role} hat bereits alle Berechtigungen in {template}.",
"up_to_date_title": "Rolle ist aktuell"
},
"subtitle": "Benutzerrollen"
},
"self_wash": {
@@ -5457,6 +5584,7 @@
},
"employees": {
"create": "@.capitalize:{'words.generated.neuen'} @:{'words.generated.mitarbeiter'} @:{'words.generated.erstellen'}",
"migrate_to_limited_backoffice": "Zu eingeschränktem Backoffice migrieren",
"subtitle": "@.capitalize:{'words.generated.hier'} @:{'words.generated.sehen'} @.capitalize:{'words.generated.sie'} @:{'words.generated.eine'} @:{'words.generated.liste'} @:{'words.generated.aller'} @:{'words.generated.mitarbeiter'} @:{'words.generated.im'} @.capitalize:{'words.generated.system'}"
},
"orders": {
@@ -5837,6 +5965,31 @@
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
},
"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",
"user_data": "Brugerdata",
"user_id": "@:{'words.generated.benutzer'}-@.upper:{'words.generated.id'}",
@@ -6043,6 +6196,20 @@
},
"vehicles": {
"add": "@:{'words.generated.fahrzeug'} @:{'words.generated.hinzuf'}?@:{'words.generated.gen'}",
"add_modal": {
"customer_label": "Kunde",
"customer_placeholder": "Nach Kundenname oder Kundennummer suchen",
"error": "Fahrzeug konnte nicht hinzugefuegt werden.",
"no_customer_results": "Keine Kunden gefunden",
"reference_placeholder": "Optionale Referenz",
"registration_placeholder": "Kennzeichen",
"selected_customer": "Ausgewaehlter Kunde",
"submit": "Fahrzeug hinzufuegen",
"title": "Fahrzeug hinzufuegen",
"type_load_error": "Fahrzeugtypen konnten nicht geladen werden.",
"type_placeholder": "Fahrzeugtyp auswaehlen",
"validation_error": "Kunde, Kennzeichen und Fahrzeugtyp auswaehlen."
},
"brand": "@:{'templates.generated.compat.admin.pos.make'}",
"color": "Farbe",
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
@@ -6107,6 +6274,9 @@
"title": "@.capitalize:{'words.generated.abteilungspreise'}",
"save": "@:{'words.generated.preise'} @:{'words.generated.speichern'}",
"saved": "@:{'words.generated.preise'} @:{'words.generated.gespeichert'}.",
"autosave_saving": "Preise werden gespeichert...",
"autosave_pending": "Preisaenderungen ausstehend.",
"autosave_saved": "Alle Preisaenderungen gespeichert.",
"product": "@.capitalize:{'words.generated.produkt'}",
"price": "Abteilungspreis",
"price_required": "Jedes @.capitalize:{'words.generated.produkt'} @:{'words.generated.muss'} einen @:{'words.generated.nicht'}-negativen numerischen Preis @:{'words.generated.haben'}.",
@@ -6121,6 +6291,15 @@
"edit_title": "@:{'words.generated.mitarbeiter'} @:{'words.generated.bearbeiten'}",
"name": "Name",
"email": "E-Mail",
"user_id": "Benutzer-ID",
"phone": "Telefon",
"phone_validation": "Die Telefonnummer muss 4-15 Ziffern enthalten.",
"country_codes": {
"denmark": "Dänemark",
"sweden": "Schweden",
"norway": "Norwegen",
"finland": "Finnland"
},
"password": "@:{'words.generated.passwort'}",
"password_create_help": "@.capitalize:{'words.generated.fur'} neue @:{'words.generated.mitarbeiter'} @:{'words.generated.erforderlich'}. Verwenden @.capitalize:{'words.generated.sie'} @:{'words.generated.mindestens'} 8 Zeichen.",
"password_update_help": "Leer lassen, um das vorhandene @:{'words.generated.passwort'} beizubehalten.",
@@ -6139,16 +6318,22 @@
"active": "Aktiv",
"inactive": "Inaktiv",
"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": {
"viewer": {
"label": "Betrachter",
"description": "@.capitalize:{'words.generated.kann'} sich anmelden @:{'words.generated.und'} Daten zugewiesener @:{'words.generated.abteilungen'} @:{'words.generated.anzeigen'}."
"label": "Deaktiviert",
"description": "Hält den Mitarbeiter registriert, ohne Zugriff auf Auftrags-, Buchungs- oder Verwaltungsfunktionen."
},
"cashier": {
"label": "Kassierer",
"description": "@.capitalize:{'words.generated.kann'} mit Aufträgen @:{'words.generated.und'} Auftragszeilen @:{'words.generated.fur'} @:{'words.generated.zugewiesene'} @:{'words.generated.abteilungen'} arbeiten."
"description": "@.capitalize:{'words.generated.kann'} mit Aufträgen, Auftragszeilen @:{'words.generated.und'} @:{'words.generated.buchungen'} @:{'words.generated.fur'} @:{'words.generated.zugewiesene'} @:{'words.generated.abteilungen'} arbeiten."
},
"booking_coordinator": {
"label": "Buchungskoordinator",
@@ -6169,7 +6354,8 @@
"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'}.",
"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": {
"title": "Rollenberechtigungen",
@@ -6182,6 +6368,11 @@
"groups": {
"account": "Konto",
"orders": "Aufträge",
"products": "Produkte",
"customers": "Kunden",
"vehicles": "Fahrzeuge",
"attachments": "Anhänge",
"scanner": "Scanner",
"bookings": "Buchungen",
"time_bookings": "Zeitbuchungen",
"reports": "Berichte",
@@ -6233,6 +6424,66 @@
"label": "Aufträge abrechnen",
"description": "Kann Zahlungen annehmen oder eine Zahlungsreservierung für einen Auftrag belasten."
},
"complete_orders": {
"label": "Aufträge abschließen",
"description": "Kann Aufträge als abgeschlossen markieren."
},
"view_product_catalog": {
"label": "Produktkatalog anzeigen",
"description": "Kann POS-Kategorien, Produkte, Preise und Zusatzoptionen laden."
},
"view_product_recommendations": {
"label": "Produktempfehlungen anzeigen",
"description": "Kann Produktempfehlungen auf Basis von Aufträgen und Fahrzeugdaten sehen."
},
"search_customers": {
"label": "Kunden suchen",
"description": "Kann Kunden für POS-Aufträge suchen und auswählen."
},
"view_customer_details": {
"label": "Kundendetails anzeigen",
"description": "Kann Kundendetails anhand einer Kundennummer laden."
},
"view_customer_notes": {
"label": "Kundennotizen anzeigen",
"description": "Kann Kundennotizen im POS lesen."
},
"add_customer_notes": {
"label": "Kundennotizen hinzufügen",
"description": "Kann Kundennotizen im POS hinzufügen."
},
"view_customer_flags": {
"label": "Kundenmarkierungen anzeigen",
"description": "Kann Kundenmarkierungen sehen, die Auftragsanforderungen und Produktregeln beeinflussen."
},
"search_vehicles": {
"label": "Fahrzeuge suchen",
"description": "Kann Kennzeichen suchen und den Fahrzeugstatus sehen."
},
"view_vehicle_matches": {
"label": "Fahrzeugtreffer anzeigen",
"description": "Kann Kundentreffer für Fahrzeuge und Vorschläge zu unbekannten Fahrzeugen sehen."
},
"view_vehicle_history": {
"label": "Fahrzeughistorie anzeigen",
"description": "Kann die letzte Auftragshistorie für ein Kennzeichen sehen."
},
"view_order_attachments": {
"label": "Auftragsanhänge anzeigen",
"description": "Kann Anhänge auf Aufträgen auflisten."
},
"add_order_attachments": {
"label": "Auftragsanhänge hinzufügen",
"description": "Kann Dateien hochladen und Waschzertifikate an Aufträge anhängen."
},
"download_order_attachments": {
"label": "Auftragsanhänge herunterladen",
"description": "Kann Auftragsanhänge öffnen und herunterladen."
},
"view_plate_scans": {
"label": "Kennzeichenscans anzeigen",
"description": "Kann aktuelle Kennzeichenscans und Scanner für zugewiesene Abteilungen sehen."
},
"view_department_bookings": {
"label": "Abteilungsbuchungen anzeigen",
"description": "Kann Buchungen für zugewiesene Abteilungen sehen."
+244 -7
View File
@@ -1008,6 +1008,9 @@
"title": "@:{'templates.generated.compat.limited_backoffice.prices.title'}",
"save": "@:{'templates.generated.compat.limited_backoffice.prices.save'}",
"saved": "@:{'templates.generated.compat.limited_backoffice.prices.saved'}",
"autosave_saving": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_saving'}",
"autosave_pending": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_pending'}",
"autosave_saved": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_saved'}",
"product": "@:{'templates.generated.compat.limited_backoffice.prices.product'}",
"price": "@:{'templates.generated.compat.limited_backoffice.prices.price'}",
"price_required": "@:{'templates.generated.compat.limited_backoffice.prices.price_required'}",
@@ -1022,6 +1025,15 @@
"edit_title": "@:{'templates.generated.compat.limited_backoffice.employees.edit_title'}",
"name": "@:{'templates.generated.compat.limited_backoffice.employees.name'}",
"email": "@:{'templates.generated.compat.limited_backoffice.employees.email'}",
"user_id": "@:{'templates.generated.compat.limited_backoffice.employees.user_id'}",
"phone": "@:{'templates.generated.compat.limited_backoffice.employees.phone'}",
"phone_validation": "@:{'templates.generated.compat.limited_backoffice.employees.phone_validation'}",
"country_codes": {
"denmark": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.denmark'}",
"sweden": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.sweden'}",
"norway": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.norway'}",
"finland": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.finland'}"
},
"password": "@:{'templates.generated.compat.limited_backoffice.employees.password'}",
"password_create_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_create_help'}",
"password_update_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_update_help'}",
@@ -1040,7 +1052,13 @@
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
"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": {
"viewer": {
@@ -1070,7 +1088,8 @@
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
"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": {
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
@@ -1083,6 +1102,11 @@
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"products": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.products'}",
"customers": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.customers'}",
"vehicles": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.vehicles'}",
"attachments": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.attachments'}",
"scanner": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.scanner'}",
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
@@ -1134,6 +1158,66 @@
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"complete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
},
"view_product_catalog": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
},
"view_product_recommendations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
},
"search_customers": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
},
"view_customer_details": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
},
"view_customer_notes": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
},
"add_customer_notes": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
},
"view_customer_flags": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
},
"search_vehicles": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
},
"view_vehicle_matches": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
},
"view_vehicle_history": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
},
"view_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
},
"add_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
},
"download_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
},
"view_plate_scans": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
},
"view_department_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
@@ -1330,6 +1414,14 @@
"subtitle_suffix": "@:{'words.generated.product'} @:{'words.generated.sales'}",
"title": "@.capitalize:{'words.generated.product'} @.capitalize:{'words.generated.sales'}"
},
"product_targets": {
"cancel_aria": "Cancel target",
"edit_aria": "Edit target percentage",
"input_label": "Target percentage",
"save_aria": "Save target",
"save_error": "The target could not be saved.",
"validation_range": "The target percentage must be between 0 and 100."
},
"reports": {
"bookings": {
"completed_count": "@.capitalize:{'words.generated.completed'} @:{'words.generated.count'}",
@@ -1524,6 +1616,7 @@
"company_phone": "@.capitalize:{'words.generated.company'} @:{'words.generated.phone'}",
"contact_email": "@.capitalize:{'words.generated.contact'} @:{'words.generated.email'}",
"contact_phone": "@.capitalize:{'words.generated.contact'} @:{'words.generated.phone'}",
"ean": "EAN",
"created_by": "@.capitalize:{'words.generated.created'} @:{'words.generated.by'}",
"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'}.",
@@ -3072,6 +3165,7 @@
"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_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'}",
"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'}",
@@ -3287,6 +3381,13 @@
"opening_hours": {
"closed": "@:{'templates.generated.compat.global.closed'}"
},
"pricing": {
"custom_pricing_disabled": "Fallback enabled",
"custom_pricing_enabled": "Custom only",
"custom_pricing_missing_price": "Missing department prices resolve to 999999.",
"custom_pricing_only": "No fallback pricing",
"effective_department_price": "Effective department price"
},
"search_placeholder": "@.capitalize:{'words.generated.search'} @:{'words.generated.by'} @:{'words.generated.department'} @:{'words.generated.name'}",
"select_department": "@.capitalize:{'words.generated.select'} @:{'words.generated.a'} @:{'words.generated.department'}",
"subtitle": "@.capitalize:{'words.generated.manage'} @:{'words.generated.departments'}",
@@ -3860,6 +3961,15 @@
"preview_title": "@.capitalize:{'words.generated.preview'} @:{'words.generated.monthly'} @:{'words.generated.split'}",
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
},
"multi_month_invoice_warning": {
"invoice_together": "Invoice together",
"split_by_month": "Split by month",
"split_error_title": "Monthly split failed",
"split_success_text": "Processed {processed} invoice collections. Split {changed}, skipped {skipped}.",
"split_success_title": "Monthly split completed",
"text": "You are about to invoice orders from multiple months together ({months}). Should they be split by month instead?",
"title": "Orders from multiple months"
}
},
"invoicing": {
@@ -4117,6 +4227,7 @@
"departments": {
"columns": {
"archived": "@:{'words.generated.archived'}",
"custom_pricing_only": "No fallback pricing",
"dimension": "Dimension",
"economic_department": "@.upper:{'words.generated.e'}-@:{'words.generated.conomic'} @.capitalize:{'words.generated.department'}",
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
@@ -4769,6 +4880,22 @@
"title": "Role {id} permissions",
"title_with_name": "{name} permissions"
},
"templates": {
"apply": "Apply {template} template",
"confirm_button": "Apply template",
"confirm_text": "Add {count} missing permissions from {template} to {role}?",
"confirm_title": "Apply permission template?",
"error_text": "One or more permissions from {template} could not be added to {role}. The role list was reloaded.",
"error_title": "Could not apply template",
"load_error": "Could not load templates. Try again",
"loading": "Loading templates...",
"section": "Limited backoffice templates",
"success_text": "Added {count} permissions from {template} to {role}.",
"success_title": "Template applied",
"unavailable": "No templates are available.",
"up_to_date_text": "{role} already has every permission in {template}.",
"up_to_date_title": "Role is up to date"
},
"subtitle": "@.capitalize:{'words.generated.user'} @:{'words.generated.roles'}"
},
"self_wash": {
@@ -5178,6 +5305,7 @@
},
"employees": {
"create": "@.capitalize:{'words.generated.create'} @:{'words.generated.new'} @:{'words.generated.employee'}",
"migrate_to_limited_backoffice": "Migrate to limited backoffice",
"subtitle": "@.capitalize:{'words.generated.here'} @:{'words.generated.you'} @:{'words.generated.can'} @:{'words.generated.see'} @:{'words.generated.a'} @:{'words.generated.list'} @:{'words.generated.of'} @:{'words.generated.all'} @:{'words.generated.employees'} @:{'words.generated.in'} @:{'words.replication.article.host_mention'} @:{'words.generated.system'}"
},
"orders": {
@@ -5558,6 +5686,31 @@
"transaction_history": "@.capitalize:{'words.generated.transaction'} @:{'words.generated.history'}"
},
"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'}",
"user_data": "@.capitalize:{'words.generated.user'} @:{'words.generated.data'}",
"user_id": "@.capitalize:{'words.generated.user'} @.upper:{'words.generated.id'}",
@@ -5842,6 +5995,9 @@
"title": "@.capitalize:{'words.generated.department'} @:{'words.generated.prices'}",
"save": "@.capitalize:{'words.generated.save'} @:{'words.generated.prices'}",
"saved": "@.capitalize:{'words.generated.prices'} @:{'words.generated.saved'}.",
"autosave_saving": "Saving prices...",
"autosave_pending": "Price changes pending.",
"autosave_saved": "All price changes saved.",
"product": "@.capitalize:{'words.generated.product'}",
"price": "@.capitalize:{'words.generated.department'} @:{'words.generated.price'}",
"price_required": "Every @:{'words.generated.product'} @:{'words.generated.must'} @:{'words.generated.have'} @:{'words.generated.a'} non-negative numeric @:{'words.generated.price'}.",
@@ -5856,6 +6012,15 @@
"edit_title": "@.capitalize:{'words.generated.edit'} @:{'words.generated.employee'}",
"name": "Name",
"email": "Email",
"user_id": "User ID",
"phone": "Phone",
"phone_validation": "Phone number must be 4-15 digits.",
"country_codes": {
"denmark": "Denmark",
"sweden": "Sweden",
"norway": "Norway",
"finland": "Finland"
},
"password": "@.capitalize:{'words.generated.password'}",
"password_create_help": "@.capitalize:{'words.generated.required'} @:{'words.generated.for'} new @:{'words.generated.employees'}. Use @:{'words.generated.at'} @:{'words.generated.least'} 8 characters.",
"password_update_help": "Leave blank @:{'words.generated.to'} keep @:{'words.replication.article.host_mention'} existing @:{'words.generated.password'}.",
@@ -5874,16 +6039,22 @@
"active": "Active",
"inactive": "@.capitalize:{'words.generated.inactive'}",
"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": {
"viewer": {
"label": "Viewer",
"description": "@.capitalize:{'words.generated.can'} sign @:{'words.generated.in'} @:{'words.generated.and'} view @:{'words.generated.assigned'} @:{'words.generated.department'} data."
"label": "Deactivated",
"description": "Keeps the employee registered without order, booking, or management permissions."
},
"cashier": {
"label": "Cashier",
"description": "@.capitalize:{'words.generated.can'} work with @:{'words.generated.orders'} @:{'words.generated.and'} order lines @:{'words.generated.for'} @:{'words.generated.assigned'} @:{'words.generated.departments'}."
"description": "@.capitalize:{'words.generated.can'} work with @:{'words.generated.orders'}, order lines @:{'words.generated.and'} @:{'words.generated.bookings'} @:{'words.generated.for'} @:{'words.generated.assigned'} @:{'words.generated.departments'}."
},
"booking_coordinator": {
"label": "Booking coordinator",
@@ -5904,7 +6075,8 @@
"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'}.",
"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": {
"title": "Role permissions",
@@ -5917,6 +6089,11 @@
"groups": {
"account": "Account",
"orders": "Orders",
"products": "Products",
"customers": "Customers",
"vehicles": "Vehicles",
"attachments": "Attachments",
"scanner": "Scanners",
"bookings": "Bookings",
"time_bookings": "Time bookings",
"reports": "Reports",
@@ -5968,6 +6145,66 @@
"label": "Charge orders",
"description": "Can take payment or charge a payment intent for an order."
},
"complete_orders": {
"label": "Complete orders",
"description": "Can mark orders as completed."
},
"view_product_catalog": {
"label": "View product catalog",
"description": "Can load POS categories, products, prices, and addons."
},
"view_product_recommendations": {
"label": "View product recommendations",
"description": "Can see product suggestions based on orders and vehicle data."
},
"search_customers": {
"label": "Search customers",
"description": "Can search and select customers for POS orders."
},
"view_customer_details": {
"label": "View customer details",
"description": "Can load customer details from a customer number."
},
"view_customer_notes": {
"label": "View customer notes",
"description": "Can read customer notes in POS."
},
"add_customer_notes": {
"label": "Add customer notes",
"description": "Can add customer notes from POS."
},
"view_customer_flags": {
"label": "View customer flags",
"description": "Can see customer flags that affect order requirements and product rules."
},
"search_vehicles": {
"label": "Search vehicles",
"description": "Can search license plates and see vehicle status."
},
"view_vehicle_matches": {
"label": "View vehicle matches",
"description": "Can see vehicle customer matches and unknown vehicle suggestions."
},
"view_vehicle_history": {
"label": "View vehicle history",
"description": "Can see recent order history for a license plate."
},
"view_order_attachments": {
"label": "View order attachments",
"description": "Can list attachments on orders."
},
"add_order_attachments": {
"label": "Add order attachments",
"description": "Can upload files and attach wash certificates to orders."
},
"download_order_attachments": {
"label": "Download order attachments",
"description": "Can open and download order attachments."
},
"view_plate_scans": {
"label": "View plate scans",
"description": "Can view recent license plate scans and scanners for assigned departments."
},
"view_department_bookings": {
"label": "View department bookings",
"description": "Can see bookings for assigned departments."
+144
View File
@@ -126,6 +126,14 @@
"subtitle_suffix": "@:{'templates.generated.compat.admin.daily_report.product_sales.subtitle_suffix'}",
"title": "@:{'templates.generated.compat.admin.daily_report.product_sales.title'}"
},
"product_targets": {
"cancel_aria": "@:{'templates.generated.compat.admin.daily_report.product_targets.cancel_aria'}",
"edit_aria": "@:{'templates.generated.compat.admin.daily_report.product_targets.edit_aria'}",
"input_label": "@:{'templates.generated.compat.admin.daily_report.product_targets.input_label'}",
"save_aria": "@:{'templates.generated.compat.admin.daily_report.product_targets.save_aria'}",
"save_error": "@:{'templates.generated.compat.admin.daily_report.product_targets.save_error'}",
"validation_range": "@:{'templates.generated.compat.admin.daily_report.product_targets.validation_range'}"
},
"reports": {
"avg_revenue": {
"label": "@:{'templates.generated.compat.department_reports.avg_revenue_per_wash'}",
@@ -385,6 +393,7 @@
"delete": "@:{'templates.generated.compat.global.delete'}",
"details": "@:{'templates.generated.compat.admin.pos.details'}",
"download": "@:{'templates.generated.compat.guest.pwa.title'}",
"ean": "@:{'templates.generated.compat.admin.pos.ean'}",
"drafts_assignment": {
"button": "@:{'templates.generated.compat.admin.pos.drafts_assignment.button'}",
"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_desc": "@:{'templates.generated.compat.tables.bookings.wash_desc'}",
"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'}",
"intro": "@:{'templates.generated.compat.customer_creation.customer.intro'}",
"phone_label": "@:{'templates.generated.compat.customer_creation.customer.phone_label'}",
@@ -2420,6 +2430,13 @@
"title": "@:{'templates.generated.compat.departments.tab.opening_hours'}"
},
"phone": "@:common.phone",
"pricing": {
"custom_pricing_disabled": "@:{'templates.generated.compat.departments.pricing.custom_pricing_disabled'}",
"custom_pricing_enabled": "@:{'templates.generated.compat.departments.pricing.custom_pricing_enabled'}",
"custom_pricing_missing_price": "@:{'templates.generated.compat.departments.pricing.custom_pricing_missing_price'}",
"custom_pricing_only": "@:{'templates.generated.compat.departments.pricing.custom_pricing_only'}",
"effective_department_price": "@:{'templates.generated.compat.departments.pricing.effective_department_price'}"
},
"products": {
"new_product": "@:products.new_product"
},
@@ -3149,6 +3166,15 @@
"preview_title": "@:{'templates.generated.compat.invoicing_period.monthly_split.preview_title'}",
"success_text": "@:{'templates.generated.compat.invoicing_period.monthly_split.success_text'}",
"success_title": "@:{'templates.generated.compat.invoicing_period.monthly_split.success_title'}"
},
"multi_month_invoice_warning": {
"invoice_together": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.invoice_together'}",
"split_by_month": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.split_by_month'}",
"split_error_title": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.split_error_title'}",
"split_success_text": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.split_success_text'}",
"split_success_title": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.split_success_title'}",
"text": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.text'}",
"title": "@:{'templates.generated.compat.invoicing_period.multi_month_invoice_warning.title'}"
}
},
"invoicing": {
@@ -3486,6 +3512,7 @@
"departments": {
"columns": {
"archived": "@:{'templates.generated.compat.objects.departments.columns.archived'}",
"custom_pricing_only": "@:{'templates.generated.compat.objects.departments.columns.custom_pricing_only'}",
"dimension": "@:{'templates.generated.compat.objects.departments.columns.dimension'}",
"economic_department": "@:{'templates.generated.compat.objects.departments.columns.economic_department'}",
"latitude": "@:{'templates.generated.compat.objects.departments.columns.latitude'}",
@@ -4346,6 +4373,22 @@
"title": "@:{'templates.generated.compat.roles.permissions.title'}",
"title_with_name": "@:{'templates.generated.compat.roles.permissions.title_with_name'}"
},
"templates": {
"apply": "@:{'templates.generated.compat.roles.templates.apply'}",
"confirm_button": "@:{'templates.generated.compat.roles.templates.confirm_button'}",
"confirm_text": "@:{'templates.generated.compat.roles.templates.confirm_text'}",
"confirm_title": "@:{'templates.generated.compat.roles.templates.confirm_title'}",
"error_text": "@:{'templates.generated.compat.roles.templates.error_text'}",
"error_title": "@:{'templates.generated.compat.roles.templates.error_title'}",
"load_error": "@:{'templates.generated.compat.roles.templates.load_error'}",
"loading": "@:{'templates.generated.compat.roles.templates.loading'}",
"section": "@:{'templates.generated.compat.roles.templates.section'}",
"success_text": "@:{'templates.generated.compat.roles.templates.success_text'}",
"success_title": "@:{'templates.generated.compat.roles.templates.success_title'}",
"unavailable": "@:{'templates.generated.compat.roles.templates.unavailable'}",
"up_to_date_text": "@:{'templates.generated.compat.roles.templates.up_to_date_text'}",
"up_to_date_title": "@:{'templates.generated.compat.roles.templates.up_to_date_title'}"
},
"subtitle": "@:{'templates.generated.compat.roles.subtitle'}",
"title": "@:{'templates.generated.compat.common.roles'}"
},
@@ -4559,6 +4602,82 @@
"subtitle": "@:{'templates.generated.compat.superuser_dashboard.departments.subtitle'}",
"title": "@:common.departments"
},
"department_navigation": {
"overview": "Overview",
"modules": "Modules",
"branding": "Profile & Branding",
"gateways": "@.capitalize:{'words.generated.gateways'}",
"stripe": "@:{'words.generated.stripe'}",
"pricing": "Pricing",
"categories": "Categories"
},
"department_overview": {
"title": "Department overview",
"subtitle": "Operational overview for the selected department",
"loading": "Loading department overview",
"date_from": "From",
"date_to": "To",
"range_label": "{from} to {to}",
"empty_value": "-",
"out_of": "of {total}",
"presets": {
"today": "Today",
"last_seven_days": "Last 7 days"
},
"metrics": {
"bookings": "Bookings",
"complaints": "Complaints",
"night_washes": "Night washes",
"overtime": "Overtime",
"products_sold": "Products sold",
"revenue": "Revenue",
"transactions": "Transactions",
"washes": "Washes",
"water_usage": "Water"
},
"units": {
"hours": "h",
"liters": "L"
},
"products": {
"title": "Product mix",
"subtitle": "Tracked wash products in the selected period",
"empty": "No product activity for the selected period"
},
"profile": {
"title": "Department profile",
"no_description": "No department description",
"department_id": "Department ID",
"economic_department_id": "Economic department",
"branding": "Branding",
"created_at": "@:common.created",
"updated_at": "Updated"
},
"hardware": {
"title": "Hardware readiness",
"subtitle": "Gateway-backed lane and relay state",
"gateways": "Gateways online",
"lanes": "Lanes",
"gates": "Gates",
"relays": "Relays",
"scanners": "Scanners",
"issues": "Issues"
},
"quick_links": {
"title": "Department tools",
"subtitle": "Open the focused setup areas for this department",
"modules": "@:superuser_dashboard.department_navigation.modules",
"branding": "@:superuser_dashboard.department_overview.profile.branding",
"gateways": "@:superuser_dashboard.department_navigation.gateways",
"stripe": "@:superuser_dashboard.department_navigation.stripe",
"pricing": "@:superuser_dashboard.department_navigation.pricing",
"categories": "@:superuser_dashboard.department_navigation.categories"
},
"errors": {
"invalid_department": "A valid department is required",
"load": "Unable to load the department overview"
}
},
"employees": {
"edit_employee_title": "@:employees.edit_employee",
"new_employee_subtitle": "@:{'templates.generated.compat.superuser_dashboard.employees.new_employee_subtitle'}",
@@ -5537,6 +5656,31 @@
"transaction_history": "@:{'templates.generated.compat.user_admin.orders.transaction_history'}"
},
"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'}",
"subtitle": "@:user_admin.user_data",
"title": "@:{'templates.generated.compat.common.user'}",
+258 -7
View File
@@ -1290,6 +1290,9 @@
"title": "@:{'templates.generated.compat.limited_backoffice.prices.title'}",
"save": "@:{'templates.generated.compat.limited_backoffice.prices.save'}",
"saved": "@:{'templates.generated.compat.limited_backoffice.prices.saved'}",
"autosave_saving": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_saving'}",
"autosave_pending": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_pending'}",
"autosave_saved": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_saved'}",
"product": "@:{'templates.generated.compat.limited_backoffice.prices.product'}",
"price": "@:{'templates.generated.compat.limited_backoffice.prices.price'}",
"price_required": "@:{'templates.generated.compat.limited_backoffice.prices.price_required'}",
@@ -1304,6 +1307,15 @@
"edit_title": "@:{'templates.generated.compat.limited_backoffice.employees.edit_title'}",
"name": "@:{'templates.generated.compat.limited_backoffice.employees.name'}",
"email": "@:{'templates.generated.compat.limited_backoffice.employees.email'}",
"user_id": "@:{'templates.generated.compat.limited_backoffice.employees.user_id'}",
"phone": "@:{'templates.generated.compat.limited_backoffice.employees.phone'}",
"phone_validation": "@:{'templates.generated.compat.limited_backoffice.employees.phone_validation'}",
"country_codes": {
"denmark": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.denmark'}",
"sweden": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.sweden'}",
"norway": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.norway'}",
"finland": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.finland'}"
},
"password": "@:{'templates.generated.compat.limited_backoffice.employees.password'}",
"password_create_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_create_help'}",
"password_update_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_update_help'}",
@@ -1322,7 +1334,13 @@
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
"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": {
"viewer": {
@@ -1352,7 +1370,8 @@
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
"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": {
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
@@ -1365,6 +1384,11 @@
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"products": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.products'}",
"customers": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.customers'}",
"vehicles": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.vehicles'}",
"attachments": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.attachments'}",
"scanner": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.scanner'}",
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
@@ -1416,6 +1440,66 @@
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"complete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
},
"view_product_catalog": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
},
"view_product_recommendations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
},
"search_customers": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
},
"view_customer_details": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
},
"view_customer_notes": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
},
"add_customer_notes": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
},
"view_customer_flags": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
},
"search_vehicles": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
},
"view_vehicle_matches": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
},
"view_vehicle_history": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
},
"view_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
},
"add_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
},
"download_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
},
"view_plate_scans": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
},
"view_department_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
@@ -1612,6 +1696,14 @@
"subtitle_suffix": "@:{'words.generated.produktsalg'}",
"title": "@.capitalize:{'words.generated.produktsalg'}"
},
"product_targets": {
"cancel_aria": "Avbryt mål",
"edit_aria": "Rediger målprosent",
"input_label": "Målprosent",
"save_aria": "Lagre mål",
"save_error": "Målet kunne ikke lagres.",
"validation_range": "Målprosenten må være mellom 0 og 100."
},
"reports": {
"bookings": {
"completed_count": "@.capitalize:{'words.generated.fullført'} @:{'words.generated.telling'}",
@@ -1806,6 +1898,7 @@
"company_phone": "Firmatelefon",
"contact_email": "@.capitalize:{'words.generated.kontakt'} @:{'words.generated.e'}-@:{'words.generated.post'}",
"contact_phone": "@.capitalize:{'words.generated.kontakt'} @:{'words.generated.telefon'}",
"ean": "EAN",
"created_by": "Laget @:{'words.generated.av'}",
"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'}.",
@@ -3354,6 +3447,7 @@
"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_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'}",
"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'}",
@@ -3569,6 +3663,13 @@
"opening_hours": {
"closed": "Stengt"
},
"pricing": {
"custom_pricing_disabled": "Fallback aktiv",
"custom_pricing_enabled": "Kun egendefinerte priser",
"custom_pricing_missing_price": "Manglende avdelingspriser blir 999999.",
"custom_pricing_only": "Ingen fallback-priser",
"effective_department_price": "Effektiv avdelingspris"
},
"search_placeholder": "@.capitalize:{'words.generated.søk'} @:{'words.generated.etter'} @:{'words.generated.avdelingsnavn'}",
"select_department": "@.capitalize:{'words.generated.velg'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.avdeling'}",
"subtitle": "@.capitalize:{'words.generated.administrer'} @:{'words.generated.avdelinger'}",
@@ -4142,6 +4243,15 @@
"preview_title": "Preview @:{'words.generated.monthly'} @:{'words.generated.split'}",
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
},
"multi_month_invoice_warning": {
"invoice_together": "Fakturer samlet",
"split_by_month": "Del opp etter måned",
"split_error_title": "Månedsdeling mislyktes",
"split_success_text": "Behandlet {processed} fakturasamlinger. Delte opp {changed}, hoppet over {skipped}.",
"split_success_title": "Månedsdeling fullført",
"text": "Du er i ferd med å fakturere ordrer fra flere måneder samlet ({months}). Skal de i stedet deles opp etter måned?",
"title": "Ordrer fra flere måneder"
}
},
"invoicing": {
@@ -4399,6 +4509,7 @@
"departments": {
"columns": {
"archived": "Arkivert",
"custom_pricing_only": "@:{'templates.generated.compat.departments.pricing.custom_pricing_only'}",
"dimension": "Dimensjon",
"economic_department": "@.upper:{'words.generated.e'}-@:{'words.generated.økonomisk'} @:{'words.generated.avdeling'}",
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
@@ -5051,6 +5162,22 @@
"title": "Tillatelser for rolle {id}",
"title_with_name": "Tillatelser for {name}"
},
"templates": {
"apply": "Bruk {template}-mal",
"confirm_button": "Bruk mal",
"confirm_text": "Legg til {count} manglende tillatelser fra {template} til {role}?",
"confirm_title": "Bruke tillatelsesmal?",
"error_text": "En eller flere tillatelser fra {template} kunne ikke legges til i {role}. Rollelisten ble lastet inn pa nytt.",
"error_title": "Kunne ikke bruke mal",
"load_error": "Kunne ikke laste maler. Prov igjen",
"loading": "Laster maler...",
"section": "Begrenset backoffice-maler",
"success_text": "La til {count} tillatelser fra {template} i {role}.",
"success_title": "Mal brukt",
"unavailable": "Ingen maler er tilgjengelige.",
"up_to_date_text": "{role} har allerede alle tillatelser i {template}.",
"up_to_date_title": "Rollen er oppdatert"
},
"subtitle": "@.capitalize:{'words.generated.user'} roles"
},
"self_wash": {
@@ -5460,6 +5587,7 @@
},
"employees": {
"create": "@:{'words.generated.opprett'} @:{'words.generated.ny'} medarbeider",
"migrate_to_limited_backoffice": "Migrer til begrenset backoffice",
"subtitle": "@.capitalize:{'words.generated.her'} @:{'words.generated.kan'} @:{'words.generated.du'} @:{'words.generated.se'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.liste'} @:{'words.generated.over'} @:{'words.generated.alle'} @:{'words.generated.ansatte'} @:{'words.generated.i'} @:{'words.generated.systemet'}"
},
"orders": {
@@ -5840,6 +5968,31 @@
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
},
"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'}",
"user_data": "@.capitalize:{'words.generated.brukerdata'}",
"user_id": "@.capitalize:{'words.generated.bruker'}-@.upper:{'words.generated.id'}",
@@ -6046,6 +6199,20 @@
},
"vehicles": {
"add": "@:{'words.generated.lagg'} @:{'words.generated.till'} @:{'words.generated.fordon'}",
"add_modal": {
"customer_label": "Kunde",
"customer_placeholder": "Søk etter kundenavn eller kundenummer",
"error": "Kunne ikke legge til kjøretøy.",
"no_customer_results": "Ingen kunder funnet",
"reference_placeholder": "Valgfri referanse",
"registration_placeholder": "Registreringsnummer",
"selected_customer": "Valgt kunde",
"submit": "Legg til kjøretøy",
"title": "Legg til kjøretøy",
"type_load_error": "Kunne ikke laste kjøretøytyper.",
"type_placeholder": "Velg kjøretøytype",
"validation_error": "Velg kunde, registreringsnummer og kjøretøytype."
},
"brand": "Märke",
"color": "Färg",
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
@@ -6110,6 +6277,9 @@
"title": "@.capitalize:{'words.generated.avdelingspriser'}",
"save": "@.capitalize:{'words.generated.lagre'} @:{'words.generated.priser'}",
"saved": "Prisene @:{'words.generated.er'} @:{'words.generated.lagret'}.",
"autosave_saving": "Lagrer priser...",
"autosave_pending": "Prisendringer venter.",
"autosave_saved": "Alle prisendringer er lagret.",
"product": "Produkt",
"price": "Avdelingspris",
"price_required": "@.capitalize:{'words.generated.alle'} @:{'words.generated.produkter'} @:{'words.generated.ma'} ha @:{'words.replication.host_definite_suffix'} @:{'words.generated.ikke'}-negativ numerisk pris.",
@@ -6124,6 +6294,15 @@
"edit_title": "@:{'words.generated.rediger'} @:{'words.generated.medarbeider'}",
"name": "Navn",
"email": "E-post",
"user_id": "Bruker-ID",
"phone": "Telefon",
"phone_validation": "Telefonnummeret må være 4-15 sifre.",
"country_codes": {
"denmark": "Danmark",
"sweden": "Sverige",
"norway": "Norge",
"finland": "Finland"
},
"password": "@.capitalize:{'words.generated.passord'}",
"password_create_help": "@.capitalize:{'words.generated.kreves'} @:{'words.generated.for'} nye @:{'words.generated.medarbeidere'}. Bruk @:{'words.generated.minst'} 8 tegn.",
"password_update_help": "La stå tomt @:{'words.generated.for'} å beholde nåværende @:{'words.generated.passord'}.",
@@ -6142,16 +6321,22 @@
"active": "Aktiv",
"inactive": "Inaktiv",
"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": {
"viewer": {
"label": "Viser",
"description": "@.capitalize:{'words.generated.kan'} logge inn @:{'words.generated.og'} se data @:{'words.generated.for'} @:{'words.generated.tildelte'} @:{'words.generated.avdelinger'}."
"label": "Deaktivert",
"description": "Holder den ansatte registrert uten tilgang til ordre-, booking- eller administrasjonsfunksjoner."
},
"cashier": {
"label": "Kasserer",
"description": "@.capitalize:{'words.generated.kan'} arbeide med @:{'words.generated.ordre'} @:{'words.generated.og'} ordrelinjer @:{'words.generated.for'} @:{'words.generated.tildelte'} @:{'words.generated.avdelinger'}."
"description": "@.capitalize:{'words.generated.kan'} arbeide med @:{'words.generated.ordre'}, ordrelinjer @:{'words.generated.og'} @:{'words.generated.bookinger'} @:{'words.generated.for'} @:{'words.generated.tildelte'} @:{'words.generated.avdelinger'}."
},
"booking_coordinator": {
"label": "Bookingkoordinator",
@@ -6172,7 +6357,8 @@
"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'}.",
"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": {
"title": "Rolletillatelser",
@@ -6185,6 +6371,11 @@
"groups": {
"account": "Konto",
"orders": "Ordrer",
"products": "Produkter",
"customers": "Kunder",
"vehicles": "Kjøretøy",
"attachments": "Vedlegg",
"scanner": "Skannere",
"bookings": "Bookinger",
"time_bookings": "Tidsbookinger",
"reports": "Rapporter",
@@ -6236,6 +6427,66 @@
"label": "Belast ordrer",
"description": "Kan ta betaling eller belaste en betalingsreservasjon for en ordre."
},
"complete_orders": {
"label": "Fullfør ordrer",
"description": "Kan markere ordrer som fullført."
},
"view_product_catalog": {
"label": "Se produktkatalog",
"description": "Kan laste POS-kategorier, produkter, priser og tillegg."
},
"view_product_recommendations": {
"label": "Se produktforslag",
"description": "Kan se produktforslag basert på ordrer og kjøretøydata."
},
"search_customers": {
"label": "Søk kunder",
"description": "Kan søke og velge kunder for POS-ordrer."
},
"view_customer_details": {
"label": "Se kundeopplysninger",
"description": "Kan laste kundeopplysninger fra et kundenummer."
},
"view_customer_notes": {
"label": "Se kundemerknader",
"description": "Kan lese kundemerknader i POS."
},
"add_customer_notes": {
"label": "Legg til kundemerknader",
"description": "Kan legge til kundemerknader fra POS."
},
"view_customer_flags": {
"label": "Se kundemarkeringer",
"description": "Kan se kundemarkeringer som påvirker ordrekrav og produktregler."
},
"search_vehicles": {
"label": "Søk kjøretøy",
"description": "Kan søke etter registreringsnummer og se kjøretøystatus."
},
"view_vehicle_matches": {
"label": "Se kjøretøytreff",
"description": "Kan se kundetreff for kjøretøy og forslag til ukjente kjøretøy."
},
"view_vehicle_history": {
"label": "Se kjøretøyhistorikk",
"description": "Kan se nylig ordrehistorikk for et registreringsnummer."
},
"view_order_attachments": {
"label": "Se ordrevedlegg",
"description": "Kan liste vedlegg på ordrer."
},
"add_order_attachments": {
"label": "Legg til ordrevedlegg",
"description": "Kan laste opp filer og legge ved vaskesertifikater på ordrer."
},
"download_order_attachments": {
"label": "Last ned ordrevedlegg",
"description": "Kan åpne og laste ned ordrevedlegg."
},
"view_plate_scans": {
"label": "Se nummerskiltskanninger",
"description": "Kan se nylige nummerskiltskanninger og skannere for tildelte avdelinger."
},
"view_department_bookings": {
"label": "Se avdelingsbookinger",
"description": "Kan se bookinger for tildelte avdelinger."
+258 -7
View File
@@ -1340,6 +1340,9 @@
"title": "@:{'templates.generated.compat.limited_backoffice.prices.title'}",
"save": "@:{'templates.generated.compat.limited_backoffice.prices.save'}",
"saved": "@:{'templates.generated.compat.limited_backoffice.prices.saved'}",
"autosave_saving": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_saving'}",
"autosave_pending": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_pending'}",
"autosave_saved": "@:{'templates.generated.compat.limited_backoffice.prices.autosave_saved'}",
"product": "@:{'templates.generated.compat.limited_backoffice.prices.product'}",
"price": "@:{'templates.generated.compat.limited_backoffice.prices.price'}",
"price_required": "@:{'templates.generated.compat.limited_backoffice.prices.price_required'}",
@@ -1354,6 +1357,15 @@
"edit_title": "@:{'templates.generated.compat.limited_backoffice.employees.edit_title'}",
"name": "@:{'templates.generated.compat.limited_backoffice.employees.name'}",
"email": "@:{'templates.generated.compat.limited_backoffice.employees.email'}",
"user_id": "@:{'templates.generated.compat.limited_backoffice.employees.user_id'}",
"phone": "@:{'templates.generated.compat.limited_backoffice.employees.phone'}",
"phone_validation": "@:{'templates.generated.compat.limited_backoffice.employees.phone_validation'}",
"country_codes": {
"denmark": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.denmark'}",
"sweden": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.sweden'}",
"norway": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.norway'}",
"finland": "@:{'templates.generated.compat.limited_backoffice.employees.country_codes.finland'}"
},
"password": "@:{'templates.generated.compat.limited_backoffice.employees.password'}",
"password_create_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_create_help'}",
"password_update_help": "@:{'templates.generated.compat.limited_backoffice.employees.password_update_help'}",
@@ -1372,7 +1384,13 @@
"active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}",
"inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}",
"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": {
"viewer": {
@@ -1402,7 +1420,8 @@
"save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}",
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
"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": {
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
@@ -1415,6 +1434,11 @@
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"products": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.products'}",
"customers": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.customers'}",
"vehicles": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.vehicles'}",
"attachments": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.attachments'}",
"scanner": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.scanner'}",
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
@@ -1466,6 +1490,66 @@
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"complete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
},
"view_product_catalog": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
},
"view_product_recommendations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
},
"search_customers": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
},
"view_customer_details": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
},
"view_customer_notes": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
},
"add_customer_notes": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
},
"view_customer_flags": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
},
"search_vehicles": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
},
"view_vehicle_matches": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
},
"view_vehicle_history": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
},
"view_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
},
"add_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
},
"download_order_attachments": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
},
"view_plate_scans": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
},
"view_department_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
@@ -1662,6 +1746,14 @@
"subtitle_suffix": "@:{'words.generated.produktforsaljning'}",
"title": "@.capitalize:{'words.generated.produktforsaljning'}"
},
"product_targets": {
"cancel_aria": "Avbryt mål",
"edit_aria": "Redigera målprocent",
"input_label": "Målprocent",
"save_aria": "Spara mål",
"save_error": "Målet kunde inte sparas.",
"validation_range": "Målprocenten måste vara mellan 0 och 100."
},
"reports": {
"bookings": {
"completed_count": "@.capitalize:{'words.generated.antal'} @:{'words.generated.slutforda'}",
@@ -1856,6 +1948,7 @@
"company_phone": "Färetagstelefon",
"contact_email": "@.capitalize:{'words.generated.kontakt'}-@:{'words.generated.e'}-@:{'words.generated.post'}",
"contact_phone": "Kontakttelefon",
"ean": "EAN",
"created_by": "@.capitalize:{'words.generated.skapad'} @:{'words.generated.av'}",
"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'}.",
@@ -3404,6 +3497,7 @@
"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_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'}",
"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'}",
@@ -3619,6 +3713,13 @@
"opening_hours": {
"closed": "Stengt"
},
"pricing": {
"custom_pricing_disabled": "Fallback aktiv",
"custom_pricing_enabled": "Endast egna priser",
"custom_pricing_missing_price": "Saknade avdelningspriser blir 999999.",
"custom_pricing_only": "Inga fallback-priser",
"effective_department_price": "Effektivt avdelningspris"
},
"search_placeholder": "@:{'words.generated.sok'} @:{'words.generated.efter'} @:{'words.generated.avdelningsnamn'}",
"select_department": "@:{'words.generated.velg'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.avdeling'}",
"subtitle": "@:{'words.generated.administrer'} @:{'words.generated.avdelinger'}",
@@ -4192,6 +4293,15 @@
"preview_title": "Preview @:{'words.generated.monthly'} @:{'words.generated.split'}",
"success_text": "@:{'words.generated.processed'} {processed} @:{'words.generated.invoice'} @:{'words.generated.collections'}. @.capitalize:{'words.generated.split'} {changed}, @:{'words.generated.skipped'} {skipped}.",
"success_title": "@.capitalize:{'words.generated.monthly'} @:{'words.generated.split'} @:{'words.generated.completed'}"
},
"multi_month_invoice_warning": {
"invoice_together": "Fakturera tillsammans",
"split_by_month": "Dela upp per månad",
"split_error_title": "Månadsuppdelning misslyckades",
"split_success_text": "Bearbetade {processed} fakturasamlingar. Delade upp {changed}, hoppade över {skipped}.",
"split_success_title": "Månadsuppdelning klar",
"text": "Du håller på att fakturera ordrar från flera månader tillsammans ({months}). Ska de delas upp per månad i stället?",
"title": "Ordrar från flera månader"
}
},
"invoicing": {
@@ -4449,6 +4559,7 @@
"departments": {
"columns": {
"archived": "Arkiverad",
"custom_pricing_only": "@:{'templates.generated.compat.departments.pricing.custom_pricing_only'}",
"dimension": "Dimension",
"economic_department": "@:{'templates.generated.compat.objects.economic_departments.single'}",
"latitude": "Latitude",
@@ -5101,6 +5212,22 @@
"title": "Behorigheter for roll {id}",
"title_with_name": "Behorigheter for {name}"
},
"templates": {
"apply": "Anvand mallen {template}",
"confirm_button": "Anvand mall",
"confirm_text": "Lagg till {count} saknade behorigheter fran {template} till {role}?",
"confirm_title": "Anvanda behorighetsmall?",
"error_text": "En eller flera behorigheter fran {template} kunde inte laggas till i {role}. Rolllistan laddades om.",
"error_title": "Kunde inte anvanda mall",
"load_error": "Kunde inte ladda mallar. Forsok igen",
"loading": "Laddar mallar...",
"section": "Begransade backoffice-mallar",
"success_text": "Lade till {count} behorigheter fran {template} i {role}.",
"success_title": "Mall anvand",
"unavailable": "Inga mallar ar tillgangliga.",
"up_to_date_text": "{role} har redan alla behorigheter i {template}.",
"up_to_date_title": "Rollen ar uppdaterad"
},
"subtitle": "@.capitalize:{'words.generated.user'} @:{'words.generated.roles'}"
},
"self_wash": {
@@ -5510,6 +5637,7 @@
},
"employees": {
"create": "@.capitalize:{'words.generated.skapa'} @:{'words.generated.ny'} @:{'words.generated.anstalld'}",
"migrate_to_limited_backoffice": "Migrera till begränsad backoffice",
"subtitle": "@.capitalize:{'words.generated.har_2'} @:{'words.generated.kan'} @:{'words.generated.du'} @:{'words.generated.se'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.lista'} @:{'words.generated.over'} @:{'words.generated.alla'} @:{'words.generated.anstallda'} @:{'words.generated.i'} @:{'words.generated.systemet'}"
},
"orders": {
@@ -5890,6 +6018,31 @@
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
},
"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",
"user_data": "Brugerdata",
"user_id": "Användar-@.upper:{'words.generated.id'}",
@@ -6096,6 +6249,20 @@
},
"vehicles": {
"add": "@:{'words.generated.lagg'} @:{'words.generated.till'} @:{'words.generated.fordon'}",
"add_modal": {
"customer_label": "Kund",
"customer_placeholder": "Sök efter kundnamn eller kundnummer",
"error": "Det gick inte att lägga till fordon.",
"no_customer_results": "Inga kunder hittades",
"reference_placeholder": "Valfri referens",
"registration_placeholder": "Registreringsnummer",
"selected_customer": "Vald kund",
"submit": "Lägg till fordon",
"title": "Lägg till fordon",
"type_load_error": "Det gick inte att läsa in fordonstyper.",
"type_placeholder": "Välj fordonstyp",
"validation_error": "Välj kund, registreringsnummer och fordonstyp."
},
"brand": "Märke",
"color": "Färg",
"delete_vehicle": "@.capitalize:{'words.generated.ta'} @:{'words.generated.bort'} @:{'words.generated.fordon'}",
@@ -6160,6 +6327,9 @@
"title": "@.capitalize:{'words.generated.avdelningspriser'}",
"save": "@.capitalize:{'words.generated.spara'} @:{'words.generated.priser'}",
"saved": "Priserna @:{'words.generated.har'} @:{'words.generated.sparats'}.",
"autosave_saving": "Sparar priser...",
"autosave_pending": "Prisandringar vantar.",
"autosave_saved": "Alla prisandringar har sparats.",
"product": "Produkt",
"price": "Avdelningspris",
"price_required": "@.capitalize:{'words.generated.alla'} @:{'words.generated.produkter'} @:{'words.generated.maste'} ha ett icke-negativt numeriskt pris.",
@@ -6174,6 +6344,15 @@
"edit_title": "@:{'words.generated.redigera'} @:{'words.generated.medarbetare'}",
"name": "Namn",
"email": "E-post",
"user_id": "Användar-ID",
"phone": "Telefon",
"phone_validation": "Telefonnumret måste vara 4-15 siffror.",
"country_codes": {
"denmark": "Danmark",
"sweden": "Sverige",
"norway": "Norge",
"finland": "Finland"
},
"password": "@.capitalize:{'words.generated.losenord'}",
"password_create_help": "@.capitalize:{'words.generated.kravs'} @:{'words.generated.for'} nya @:{'words.generated.medarbetare'}. Använd @:{'words.generated.minst'} 8 tecken.",
"password_update_help": "Lämna tomt @:{'words.generated.for'} att behålla nuvarande @:{'words.generated.losenord'}.",
@@ -6192,16 +6371,22 @@
"active": "Aktiv",
"inactive": "Inaktiv",
"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": {
"viewer": {
"label": "Visare",
"description": "@.capitalize:{'words.generated.kan'} logga in @:{'words.generated.och'} se data @:{'words.generated.for'} @:{'words.generated.tilldelade'} @:{'words.generated.avdelningar'}."
"label": "Inaktiverad",
"description": "Behåller medarbetaren registrerad utan åtkomst till order-, boknings- eller administrationsfunktioner."
},
"cashier": {
"label": "Kassör",
"description": "@.capitalize:{'words.generated.kan'} arbeta med @:{'words.generated.order'} @:{'words.generated.och'} orderrader @:{'words.generated.for'} @:{'words.generated.tilldelade'} @:{'words.generated.avdelningar'}."
"description": "@.capitalize:{'words.generated.kan'} arbeta med @:{'words.generated.order'}, orderrader @:{'words.generated.och'} @:{'words.generated.bokningar'} @:{'words.generated.for'} @:{'words.generated.tilldelade'} @:{'words.generated.avdelningar'}."
},
"booking_coordinator": {
"label": "Bokningskoordinator",
@@ -6222,7 +6407,8 @@
"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'}.",
"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": {
"title": "Rollbehörigheter",
@@ -6235,6 +6421,11 @@
"groups": {
"account": "Konto",
"orders": "Order",
"products": "Produkter",
"customers": "Kunder",
"vehicles": "Fordon",
"attachments": "Bilagor",
"scanner": "Skannrar",
"bookings": "Bokningar",
"time_bookings": "Tidsbokningar",
"reports": "Rapporter",
@@ -6286,6 +6477,66 @@
"label": "Debitera order",
"description": "Kan ta betalt eller debitera en betalningsreservation för en order."
},
"complete_orders": {
"label": "Slutför ordrar",
"description": "Kan markera ordrar som slutförda."
},
"view_product_catalog": {
"label": "Visa produktkatalog",
"description": "Kan läsa in POS-kategorier, produkter, priser och tillval."
},
"view_product_recommendations": {
"label": "Visa produktförslag",
"description": "Kan se produktförslag baserade på ordrar och fordonsdata."
},
"search_customers": {
"label": "Sök kunder",
"description": "Kan söka och välja kunder för POS-ordrar."
},
"view_customer_details": {
"label": "Visa kunduppgifter",
"description": "Kan läsa in kunduppgifter från ett kundnummer."
},
"view_customer_notes": {
"label": "Visa kundanteckningar",
"description": "Kan läsa kundanteckningar i POS."
},
"add_customer_notes": {
"label": "Lägg till kundanteckningar",
"description": "Kan lägga till kundanteckningar från POS."
},
"view_customer_flags": {
"label": "Visa kundmarkeringar",
"description": "Kan se kundmarkeringar som påverkar orderkrav och produktregler."
},
"search_vehicles": {
"label": "Sök fordon",
"description": "Kan söka registreringsnummer och se fordonsstatus."
},
"view_vehicle_matches": {
"label": "Visa fordonsträffar",
"description": "Kan se kundträffar för fordon och förslag på okända fordon."
},
"view_vehicle_history": {
"label": "Visa fordonshistorik",
"description": "Kan se senaste orderhistorik för ett registreringsnummer."
},
"view_order_attachments": {
"label": "Visa orderbilagor",
"description": "Kan lista bilagor på ordrar."
},
"add_order_attachments": {
"label": "Lägg till orderbilagor",
"description": "Kan ladda upp filer och bifoga tvättcertifikat till ordrar."
},
"download_order_attachments": {
"label": "Ladda ner orderbilagor",
"description": "Kan öppna och ladda ner orderbilagor."
},
"view_plate_scans": {
"label": "Visa nummerskyltsavläsningar",
"description": "Kan se senaste nummerskyltsavläsningar och skannrar för tilldelade avdelningar."
},
"view_department_bookings": {
"label": "Se avdelningsbokningar",
"description": "Kan se bokningar för tilldelade avdelningar."
@@ -27,6 +27,14 @@
"subtitle_suffix": "@:{'terms.glossary.salg'} @:{'terms.glossary.af'} produktet",
"title": "Produktsalg"
},
"product_targets": {
"cancel_aria": "Annuller mål",
"edit_aria": "Rediger målprocent",
"input_label": "Målprocent",
"save_aria": "Gem mål",
"save_error": "Målet kunne ikke gemmes.",
"validation_range": "Målprocenten skal være mellem 0 og 100."
},
"reports": {
"bookings": {
"completed_count": "@.capitalize:{'terms.glossary.antal'} @:{'terms.glossary.fuldførte'}",
@@ -221,6 +229,7 @@
"company_phone": "Virksomhedstelefon",
"contact_email": "@.capitalize:{'terms.glossary.kontakt'}-@:{'terms.glossary.e'}-@:{'terms.glossary.mail'}",
"contact_phone": "Kontakttelefon",
"ean": "EAN",
"created_by": "@.capitalize:{'terms.glossary.oprettet'} @:{'terms.glossary.af'}",
"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'}.",
@@ -7,6 +7,7 @@
"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_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'}",
"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'}",
@@ -21,6 +21,13 @@
"opening_hours": {
"closed": "Stengt"
},
"pricing": {
"custom_pricing_disabled": "Fallback aktiv",
"custom_pricing_enabled": "Kun egne priser",
"custom_pricing_missing_price": "Manglende afdelingspriser bliver 999999.",
"custom_pricing_only": "Ingen fallback-priser",
"effective_department_price": "Effektiv afdelingspris"
},
"search_placeholder": "@.capitalize:{'terms.glossary.søg'} @:{'terms.glossary.efter'} @:{'terms.glossary.afdelingsnavn'}",
"select_department": "@.capitalize:{'terms.glossary.vælg'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.afdeling'}",
"subtitle": "@.capitalize:{'terms.glossary.administrer'} @:{'terms.glossary.afdelinger'}",
@@ -0,0 +1,15 @@
{
"compat": {
"invoicing_period": {
"multi_month_invoice_warning": {
"invoice_together": "Fakturer sammen",
"split_by_month": "Opdel efter måned",
"split_error_title": "Månedsopdeling mislykkedes",
"split_success_text": "Behandlede {processed} fakturasamlinger. Opdelte {changed} og sprang {skipped} over.",
"split_success_title": "Månedsopdeling fuldført",
"text": "Du er ved at fakturere ordrer fra flere måneder sammen ({months}). Skal de i stedet opdeles efter måned?",
"title": "Ordrer fra flere måneder"
}
}
}
}
@@ -140,6 +140,7 @@
"departments": {
"columns": {
"archived": "Arkiveret",
"custom_pricing_only": "Ingen fallback-priser",
"dimension": "Dimension",
"economic_department": "@.upper:{'terms.glossary.e'}-@:{'terms.glossary.conomic'} @:{'terms.glossary.afdeling'}",
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
@@ -91,6 +91,22 @@
"subtitle": "Rolletilladelser",
"title": "Tilladelser for rolle {id}",
"title_with_name": "Tilladelser for {name}"
},
"templates": {
"apply": "Anvend {template}-skabelon",
"confirm_button": "Anvend skabelon",
"confirm_text": "Tilfoej {count} manglende tilladelser fra {template} til {role}?",
"confirm_title": "Anvend tilladelsesskabelon?",
"error_text": "En eller flere tilladelser fra {template} kunne ikke tilfoejes til {role}. Rollelisten blev genindlaest.",
"error_title": "Kunne ikke anvende skabelon",
"load_error": "Kunne ikke indlaese skabeloner. Proev igen",
"loading": "Indlaeser skabeloner...",
"section": "Begraenset backoffice-skabeloner",
"success_text": "Tilfoejede {count} tilladelser fra {template} til {role}.",
"success_title": "Skabelon anvendt",
"unavailable": "Ingen skabeloner er tilgaengelige.",
"up_to_date_text": "{role} har allerede alle tilladelser i {template}.",
"up_to_date_title": "Rollen er opdateret"
}
}
}
@@ -78,6 +78,7 @@
},
"employees": {
"create": "@:{'terms.glossary.opret'} @:{'terms.glossary.ny'} @:{'terms.glossary.medarbejder'}",
"migrate_to_limited_backoffice": "Migrer til begrænset backoffice",
"subtitle": "@.capitalize:{'terms.glossary.her'} @:{'terms.glossary.kan'} @:{'terms.glossary.du'} @:{'terms.glossary.se'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.liste'} @:{'terms.glossary.over'} @:{'terms.glossary.alle'} @:{'terms.glossary.medarbejdere'} @:{'terms.glossary.i'} @:{'terms.glossary.systemet'}"
},
"orders": {
@@ -14,6 +14,31 @@
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
},
"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'}",
"user_data": "@.capitalize:{'terms.glossary.brugerdata'}",
"user_id": "@.capitalize:{'terms.glossary.bruger'} @.upper:{'terms.glossary.id'}",
+175 -7
View File
@@ -32,6 +32,9 @@
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
@@ -46,6 +49,15 @@
"edit_title": "@:{'phrases.compat.limited_backoffice.employees.edit_title'}",
"name": "@:{'phrases.compat.limited_backoffice.employees.name'}",
"email": "@:{'phrases.compat.limited_backoffice.employees.email'}",
"user_id": "@:{'phrases.compat.limited_backoffice.employees.user_id'}",
"phone": "@:{'phrases.compat.limited_backoffice.employees.phone'}",
"phone_validation": "@:{'phrases.compat.limited_backoffice.employees.phone_validation'}",
"country_codes": {
"denmark": "@:{'phrases.compat.limited_backoffice.employees.country_codes.denmark'}",
"sweden": "@:{'phrases.compat.limited_backoffice.employees.country_codes.sweden'}",
"norway": "@:{'phrases.compat.limited_backoffice.employees.country_codes.norway'}",
"finland": "@:{'phrases.compat.limited_backoffice.employees.country_codes.finland'}"
},
"password": "@:{'phrases.compat.limited_backoffice.employees.password'}",
"password_create_help": "@:{'phrases.compat.limited_backoffice.employees.password_create_help'}",
"password_update_help": "@:{'phrases.compat.limited_backoffice.employees.password_update_help'}",
@@ -64,7 +76,13 @@
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
"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": {
"viewer": {
@@ -94,7 +112,8 @@
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
"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": {
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
@@ -107,6 +126,11 @@
"groups": {
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
"products": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.products'}",
"customers": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.customers'}",
"vehicles": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.vehicles'}",
"attachments": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.attachments'}",
"scanner": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.scanner'}",
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
@@ -158,6 +182,66 @@
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"complete_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
},
"view_product_catalog": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
},
"view_product_recommendations": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
},
"search_customers": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
},
"view_customer_details": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
},
"view_customer_notes": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
},
"add_customer_notes": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
},
"view_customer_flags": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
},
"search_vehicles": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
},
"view_vehicle_matches": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
},
"view_vehicle_history": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
},
"view_order_attachments": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
},
"add_order_attachments": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
},
"download_order_attachments": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
},
"view_plate_scans": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
},
"view_department_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
@@ -265,6 +349,9 @@
"title": "@.capitalize:{'terms.glossary.afdelingspriser'}",
"save": "@:{'terms.glossary.gem'} @:{'terms.glossary.priser'}",
"saved": "Priserne @:{'terms.glossary.er'} @:{'terms.glossary.gemt'}.",
"autosave_saving": "Gemmer priser...",
"autosave_pending": "Prisændringer afventer.",
"autosave_saved": "Alle prisændringer er gemt.",
"product": "Produkt",
"price": "Afdelingspris",
"price_required": "@.capitalize:{'terms.glossary.alle'} @:{'terms.glossary.produkter'} @:{'terms.glossary.skal'} have @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.ikke'}-negativ numerisk pris.",
@@ -279,6 +366,15 @@
"edit_title": "@:{'terms.glossary.rediger'} @:{'terms.glossary.medarbejder'}",
"name": "Navn",
"email": "Email",
"user_id": "Bruger-id",
"phone": "Telefon",
"phone_validation": "Telefonnummeret skal være 4-15 cifre.",
"country_codes": {
"denmark": "Danmark",
"sweden": "Sverige",
"norway": "Norge",
"finland": "Finland"
},
"password": "@.capitalize:{'terms.glossary.adgangskode'}",
"password_create_help": "@.capitalize:{'terms.glossary.pakrævet'} @:{'terms.glossary.for'} nye @:{'terms.glossary.medarbejdere'}. Brug @:{'terms.glossary.mindst'} 8 tegn.",
"password_update_help": "Lad feltet være tomt @:{'terms.glossary.for'} at beholde den nuværende @:{'terms.glossary.adgangskode'}.",
@@ -297,16 +393,22 @@
"active": "Aktiv",
"inactive": "Inaktiv",
"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": {
"viewer": {
"label": "Viser",
"description": "@.capitalize:{'terms.glossary.kan'} logge ind @:{'terms.glossary.og'} se data @:{'terms.glossary.for'} @:{'terms.glossary.tildelte'} @:{'terms.glossary.afdelinger'}."
"label": "Deaktiveret",
"description": "Holder medarbejderen registreret uden adgang til ordre-, booking- eller administrationsfunktioner."
},
"cashier": {
"label": "Kassemedarbejder",
"description": "@.capitalize:{'terms.glossary.kan'} arbejde med @:{'terms.glossary.ordrer'} @:{'terms.glossary.og'} ordrelinjer @:{'terms.glossary.for'} @:{'terms.glossary.tildelte'} @:{'terms.glossary.afdelinger'}."
"description": "@.capitalize:{'terms.glossary.kan'} arbejde med @:{'terms.glossary.ordrer'}, ordrelinjer @:{'terms.glossary.og'} @:{'terms.glossary.bookinger'} @:{'terms.glossary.for'} @:{'terms.glossary.tildelte'} @:{'terms.glossary.afdelinger'}."
},
"booking_coordinator": {
"label": "Bookingkoordinator",
@@ -327,7 +429,8 @@
"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'}.",
"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": {
"title": "Rolletilladelser",
@@ -340,6 +443,11 @@
"groups": {
"account": "Konto",
"orders": "Ordrer",
"products": "Produkter",
"customers": "Kunder",
"vehicles": "Køretøjer",
"attachments": "Vedhæftninger",
"scanner": "Scannere",
"bookings": "Bookinger",
"time_bookings": "Tidsbookinger",
"reports": "Rapporter",
@@ -391,6 +499,66 @@
"label": "Opkræv ordrer",
"description": "Kan tage betaling eller opkræve en betalingsreservation for en ordre."
},
"complete_orders": {
"label": "Afslut ordrer",
"description": "Kan markere ordrer som afsluttede."
},
"view_product_catalog": {
"label": "Se produktkatalog",
"description": "Kan indlæse POS-kategorier, produkter, priser og tilvalg."
},
"view_product_recommendations": {
"label": "Se produktforslag",
"description": "Kan se produktforslag baseret på ordrer og køretøjsdata."
},
"search_customers": {
"label": "Søg kunder",
"description": "Kan søge og vælge kunder til POS-ordrer."
},
"view_customer_details": {
"label": "Se kundeoplysninger",
"description": "Kan indlæse kundeoplysninger fra et kundenummer."
},
"view_customer_notes": {
"label": "Se kundebemærkninger",
"description": "Kan læse kundebemærkninger i POS."
},
"add_customer_notes": {
"label": "Tilføj kundebemærkninger",
"description": "Kan tilføje kundebemærkninger fra POS."
},
"view_customer_flags": {
"label": "Se kundemarkeringer",
"description": "Kan se kundemarkeringer, der påvirker ordrekrav og produktregler."
},
"search_vehicles": {
"label": "Søg køretøjer",
"description": "Kan søge nummerplader og se køretøjsstatus."
},
"view_vehicle_matches": {
"label": "Se køretøjsmatch",
"description": "Kan se kundematch for køretøjer og forslag til ukendte køretøjer."
},
"view_vehicle_history": {
"label": "Se køretøjshistorik",
"description": "Kan se seneste ordrehistorik for en nummerplade."
},
"view_order_attachments": {
"label": "Se ordrevedhæftninger",
"description": "Kan liste vedhæftninger på ordrer."
},
"add_order_attachments": {
"label": "Tilføj ordrevedhæftninger",
"description": "Kan uploade filer og vedhæfte vaskecertifikater til ordrer."
},
"download_order_attachments": {
"label": "Download ordrevedhæftninger",
"description": "Kan åbne og downloade ordrevedhæftninger."
},
"view_plate_scans": {
"label": "Se nummerpladescanninger",
"description": "Kan se seneste nummerpladescanninger og scannere for tildelte afdelinger."
},
"view_department_bookings": {
"label": "Se afdelingsbookinger",
"description": "Kan se bookinger for tildelte afdelinger."
@@ -27,6 +27,14 @@
"subtitle_suffix": "Produktverk?@:{'terms.glossary.ufe'}",
"title": "@:{'phrases.compat.admin.daily_report.product_sales.subtitle_suffix'}"
},
"product_targets": {
"cancel_aria": "Ziel abbrechen",
"edit_aria": "Zielprozentsatz bearbeiten",
"input_label": "Zielprozentsatz",
"save_aria": "Ziel speichern",
"save_error": "Das Ziel konnte nicht gespeichert werden.",
"validation_range": "Der Zielprozentsatz muss zwischen 0 und 100 liegen."
},
"reports": {
"bookings": {
"completed_count": "@:{'terms.glossary.abgeschlossene'} @:{'terms.glossary.anzahl'}",
@@ -221,6 +229,7 @@
"company_phone": "Firmentelefon",
"contact_email": "@:{'terms.glossary.kontakt'}-@.upper:{'terms.glossary.e'}-@:{'terms.glossary.mail'}",
"contact_phone": "Kontakttelefonnummer",
"ean": "EAN",
"created_by": "@.capitalize:{'terms.glossary.erstellt'} @:{'terms.glossary.von'}",
"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'}.",
@@ -7,6 +7,7 @@
"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_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'}",
"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'}",
@@ -21,6 +21,13 @@
"opening_hours": {
"closed": "@:{'phrases.compat.global.closed'}"
},
"pricing": {
"custom_pricing_disabled": "Fallback aktiviert",
"custom_pricing_enabled": "Nur eigene Preise",
"custom_pricing_missing_price": "Fehlende Abteilungspreise werden zu 999999.",
"custom_pricing_only": "Keine Fallback-Preise",
"effective_department_price": "Effektiver Abteilungspreis"
},
"search_placeholder": "@.capitalize:{'terms.glossary.nach'} @:{'terms.glossary.abteilungsname'} @:{'terms.glossary.suchen'}",
"select_department": "@:{'phrases.compat.nav.select_department'}",
"subtitle": "@:{'terms.glossary.abteilungen'} @:{'terms.glossary.verwalten'}",
@@ -0,0 +1,15 @@
{
"compat": {
"invoicing_period": {
"multi_month_invoice_warning": {
"invoice_together": "Zusammen abrechnen",
"split_by_month": "Nach Monat aufteilen",
"split_error_title": "Monatsaufteilung fehlgeschlagen",
"split_success_text": "{processed} Rechnungssammlungen verarbeitet. {changed} aufgeteilt, {skipped} übersprungen.",
"split_success_title": "Monatsaufteilung abgeschlossen",
"text": "Sie sind dabei, Aufträge aus mehreren Monaten gemeinsam abzurechnen ({months}). Sollen sie stattdessen nach Monat aufgeteilt werden?",
"title": "Aufträge aus mehreren Monaten"
}
}
}
}
@@ -140,6 +140,7 @@
"departments": {
"columns": {
"archived": "Archiviert",
"custom_pricing_only": "@:{'phrases.compat.departments.pricing.custom_pricing_only'}",
"dimension": "Abmessung",
"economic_department": "@:{'phrases.compat.objects.economic_departments.single'}",
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
@@ -91,6 +91,22 @@
"subtitle": "Rollenberechtigungen",
"title": "Berechtigungen fuer Rolle {id}",
"title_with_name": "Berechtigungen fuer {name}"
},
"templates": {
"apply": "{template}-Vorlage anwenden",
"confirm_button": "Vorlage anwenden",
"confirm_text": "{count} fehlende Berechtigungen aus {template} zu {role} hinzufuegen?",
"confirm_title": "Berechtigungsvorlage anwenden?",
"error_text": "Eine oder mehrere Berechtigungen aus {template} konnten {role} nicht hinzugefuegt werden. Die Rollenliste wurde neu geladen.",
"error_title": "Vorlage konnte nicht angewendet werden",
"load_error": "Vorlagen konnten nicht geladen werden. Erneut versuchen",
"loading": "Vorlagen werden geladen...",
"section": "Limited-Backoffice-Vorlagen",
"success_text": "{count} Berechtigungen aus {template} zu {role} hinzugefuegt.",
"success_title": "Vorlage angewendet",
"unavailable": "Keine Vorlagen verfuegbar.",
"up_to_date_text": "{role} hat bereits alle Berechtigungen in {template}.",
"up_to_date_title": "Rolle ist aktuell"
}
}
}
@@ -78,6 +78,7 @@
},
"employees": {
"create": "@.capitalize:{'terms.glossary.neuen'} @:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.erstellen'}",
"migrate_to_limited_backoffice": "Zu eingeschränktem Backoffice migrieren",
"subtitle": "@.capitalize:{'terms.glossary.hier'} @:{'terms.glossary.sehen'} @.capitalize:{'terms.glossary.sie'} @:{'terms.glossary.eine'} @:{'terms.glossary.liste'} @:{'terms.glossary.aller'} @:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.im'} @.capitalize:{'terms.glossary.system'}"
},
"orders": {
@@ -14,6 +14,31 @@
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
},
"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",
"user_data": "Brugerdata",
"user_id": "@:{'terms.glossary.benutzer'}-@.upper:{'terms.glossary.id'}",
@@ -2,6 +2,20 @@
"compat": {
"vehicles": {
"add": "@:{'terms.glossary.fahrzeug'} @:{'terms.glossary.hinzuf'}?@:{'terms.glossary.gen'}",
"add_modal": {
"customer_label": "Kunde",
"customer_placeholder": "Nach Kundenname oder Kundennummer suchen",
"error": "Fahrzeug konnte nicht hinzugefuegt werden.",
"no_customer_results": "Keine Kunden gefunden",
"reference_placeholder": "Optionale Referenz",
"registration_placeholder": "Kennzeichen",
"selected_customer": "Ausgewaehlter Kunde",
"submit": "Fahrzeug hinzufuegen",
"title": "Fahrzeug hinzufuegen",
"type_load_error": "Fahrzeugtypen konnten nicht geladen werden.",
"type_placeholder": "Fahrzeugtyp auswaehlen",
"validation_error": "Kunde, Kennzeichen und Fahrzeugtyp auswaehlen."
},
"brand": "@:{'phrases.compat.admin.pos.make'}",
"color": "Farbe",
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
+175 -7
View File
@@ -32,6 +32,9 @@
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
@@ -46,6 +49,15 @@
"edit_title": "@:{'phrases.compat.limited_backoffice.employees.edit_title'}",
"name": "@:{'phrases.compat.limited_backoffice.employees.name'}",
"email": "@:{'phrases.compat.limited_backoffice.employees.email'}",
"user_id": "@:{'phrases.compat.limited_backoffice.employees.user_id'}",
"phone": "@:{'phrases.compat.limited_backoffice.employees.phone'}",
"phone_validation": "@:{'phrases.compat.limited_backoffice.employees.phone_validation'}",
"country_codes": {
"denmark": "@:{'phrases.compat.limited_backoffice.employees.country_codes.denmark'}",
"sweden": "@:{'phrases.compat.limited_backoffice.employees.country_codes.sweden'}",
"norway": "@:{'phrases.compat.limited_backoffice.employees.country_codes.norway'}",
"finland": "@:{'phrases.compat.limited_backoffice.employees.country_codes.finland'}"
},
"password": "@:{'phrases.compat.limited_backoffice.employees.password'}",
"password_create_help": "@:{'phrases.compat.limited_backoffice.employees.password_create_help'}",
"password_update_help": "@:{'phrases.compat.limited_backoffice.employees.password_update_help'}",
@@ -64,7 +76,13 @@
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
"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": {
"viewer": {
@@ -94,7 +112,8 @@
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
"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": {
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
@@ -107,6 +126,11 @@
"groups": {
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
"products": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.products'}",
"customers": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.customers'}",
"vehicles": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.vehicles'}",
"attachments": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.attachments'}",
"scanner": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.scanner'}",
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
@@ -158,6 +182,66 @@
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"complete_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
},
"view_product_catalog": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
},
"view_product_recommendations": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
},
"search_customers": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
},
"view_customer_details": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
},
"view_customer_notes": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
},
"add_customer_notes": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
},
"view_customer_flags": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
},
"search_vehicles": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
},
"view_vehicle_matches": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
},
"view_vehicle_history": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
},
"view_order_attachments": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
},
"add_order_attachments": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
},
"download_order_attachments": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
},
"view_plate_scans": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
},
"view_department_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
@@ -265,6 +349,9 @@
"title": "@.capitalize:{'terms.glossary.abteilungspreise'}",
"save": "@:{'terms.glossary.preise'} @:{'terms.glossary.speichern'}",
"saved": "@:{'terms.glossary.preise'} @:{'terms.glossary.gespeichert'}.",
"autosave_saving": "Preise werden gespeichert...",
"autosave_pending": "Preisaenderungen ausstehend.",
"autosave_saved": "Alle Preisaenderungen gespeichert.",
"product": "@.capitalize:{'terms.glossary.produkt'}",
"price": "Abteilungspreis",
"price_required": "Jedes @.capitalize:{'terms.glossary.produkt'} @:{'terms.glossary.muss'} einen @:{'terms.glossary.nicht'}-negativen numerischen Preis @:{'terms.glossary.haben'}.",
@@ -279,6 +366,15 @@
"edit_title": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.bearbeiten'}",
"name": "Name",
"email": "E-Mail",
"user_id": "Benutzer-ID",
"phone": "Telefon",
"phone_validation": "Die Telefonnummer muss 4-15 Ziffern enthalten.",
"country_codes": {
"denmark": "Dänemark",
"sweden": "Schweden",
"norway": "Norwegen",
"finland": "Finnland"
},
"password": "@:{'terms.glossary.passwort'}",
"password_create_help": "@.capitalize:{'terms.glossary.fur'} neue @:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.erforderlich'}. Verwenden @.capitalize:{'terms.glossary.sie'} @:{'terms.glossary.mindestens'} 8 Zeichen.",
"password_update_help": "Leer lassen, um das vorhandene @:{'terms.glossary.passwort'} beizubehalten.",
@@ -297,16 +393,22 @@
"active": "Aktiv",
"inactive": "Inaktiv",
"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": {
"viewer": {
"label": "Betrachter",
"description": "@.capitalize:{'terms.glossary.kann'} sich anmelden @:{'terms.glossary.und'} Daten zugewiesener @:{'terms.glossary.abteilungen'} @:{'terms.glossary.anzeigen'}."
"label": "Deaktiviert",
"description": "Hält den Mitarbeiter registriert, ohne Zugriff auf Auftrags-, Buchungs- oder Verwaltungsfunktionen."
},
"cashier": {
"label": "Kassierer",
"description": "@.capitalize:{'terms.glossary.kann'} mit Aufträgen @:{'terms.glossary.und'} Auftragszeilen @:{'terms.glossary.fur'} @:{'terms.glossary.zugewiesene'} @:{'terms.glossary.abteilungen'} arbeiten."
"description": "@.capitalize:{'terms.glossary.kann'} mit Aufträgen, Auftragszeilen @:{'terms.glossary.und'} @:{'terms.glossary.buchungen'} @:{'terms.glossary.fur'} @:{'terms.glossary.zugewiesene'} @:{'terms.glossary.abteilungen'} arbeiten."
},
"booking_coordinator": {
"label": "Buchungskoordinator",
@@ -327,7 +429,8 @@
"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'}.",
"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": {
"title": "Rollenberechtigungen",
@@ -340,6 +443,11 @@
"groups": {
"account": "Konto",
"orders": "Aufträge",
"products": "Produkte",
"customers": "Kunden",
"vehicles": "Fahrzeuge",
"attachments": "Anhänge",
"scanner": "Scanner",
"bookings": "Buchungen",
"time_bookings": "Zeitbuchungen",
"reports": "Berichte",
@@ -391,6 +499,66 @@
"label": "Aufträge abrechnen",
"description": "Kann Zahlungen annehmen oder eine Zahlungsreservierung für einen Auftrag belasten."
},
"complete_orders": {
"label": "Aufträge abschließen",
"description": "Kann Aufträge als abgeschlossen markieren."
},
"view_product_catalog": {
"label": "Produktkatalog anzeigen",
"description": "Kann POS-Kategorien, Produkte, Preise und Zusatzoptionen laden."
},
"view_product_recommendations": {
"label": "Produktempfehlungen anzeigen",
"description": "Kann Produktempfehlungen auf Basis von Aufträgen und Fahrzeugdaten sehen."
},
"search_customers": {
"label": "Kunden suchen",
"description": "Kann Kunden für POS-Aufträge suchen und auswählen."
},
"view_customer_details": {
"label": "Kundendetails anzeigen",
"description": "Kann Kundendetails anhand einer Kundennummer laden."
},
"view_customer_notes": {
"label": "Kundennotizen anzeigen",
"description": "Kann Kundennotizen im POS lesen."
},
"add_customer_notes": {
"label": "Kundennotizen hinzufügen",
"description": "Kann Kundennotizen im POS hinzufügen."
},
"view_customer_flags": {
"label": "Kundenmarkierungen anzeigen",
"description": "Kann Kundenmarkierungen sehen, die Auftragsanforderungen und Produktregeln beeinflussen."
},
"search_vehicles": {
"label": "Fahrzeuge suchen",
"description": "Kann Kennzeichen suchen und den Fahrzeugstatus sehen."
},
"view_vehicle_matches": {
"label": "Fahrzeugtreffer anzeigen",
"description": "Kann Kundentreffer für Fahrzeuge und Vorschläge zu unbekannten Fahrzeugen sehen."
},
"view_vehicle_history": {
"label": "Fahrzeughistorie anzeigen",
"description": "Kann die letzte Auftragshistorie für ein Kennzeichen sehen."
},
"view_order_attachments": {
"label": "Auftragsanhänge anzeigen",
"description": "Kann Anhänge auf Aufträgen auflisten."
},
"add_order_attachments": {
"label": "Auftragsanhänge hinzufügen",
"description": "Kann Dateien hochladen und Waschzertifikate an Aufträge anhängen."
},
"download_order_attachments": {
"label": "Auftragsanhänge herunterladen",
"description": "Kann Auftragsanhänge öffnen und herunterladen."
},
"view_plate_scans": {
"label": "Kennzeichenscans anzeigen",
"description": "Kann aktuelle Kennzeichenscans und Scanner für zugewiesene Abteilungen sehen."
},
"view_department_bookings": {
"label": "Abteilungsbuchungen anzeigen",
"description": "Kann Buchungen für zugewiesene Abteilungen sehen."
@@ -27,6 +27,14 @@
"subtitle_suffix": "@:{'terms.glossary.product'} @:{'terms.glossary.sales'}",
"title": "@.capitalize:{'terms.glossary.product'} @.capitalize:{'terms.glossary.sales'}"
},
"product_targets": {
"cancel_aria": "Cancel target",
"edit_aria": "Edit target percentage",
"input_label": "Target percentage",
"save_aria": "Save target",
"save_error": "The target could not be saved.",
"validation_range": "The target percentage must be between 0 and 100."
},
"reports": {
"bookings": {
"completed_count": "@.capitalize:{'terms.glossary.completed'} @:{'terms.glossary.count'}",
@@ -221,6 +229,7 @@
"company_phone": "@.capitalize:{'terms.glossary.company'} @:{'terms.glossary.phone'}",
"contact_email": "@.capitalize:{'terms.glossary.contact'} @:{'terms.glossary.email'}",
"contact_phone": "@.capitalize:{'terms.glossary.contact'} @:{'terms.glossary.phone'}",
"ean": "EAN",
"created_by": "@.capitalize:{'terms.glossary.created'} @:{'terms.glossary.by'}",
"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'}.",
@@ -7,6 +7,7 @@
"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_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'}",
"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'}",
@@ -21,6 +21,13 @@
"opening_hours": {
"closed": "@:{'phrases.compat.global.closed'}"
},
"pricing": {
"custom_pricing_disabled": "Fallback enabled",
"custom_pricing_enabled": "Custom only",
"custom_pricing_missing_price": "Missing department prices resolve to 999999.",
"custom_pricing_only": "No fallback pricing",
"effective_department_price": "Effective department price"
},
"search_placeholder": "@.capitalize:{'terms.glossary.search'} @:{'terms.glossary.by'} @:{'terms.glossary.department'} @:{'terms.glossary.name'}",
"select_department": "@.capitalize:{'terms.glossary.select'} @:{'terms.glossary.a'} @:{'terms.glossary.department'}",
"subtitle": "@.capitalize:{'terms.glossary.manage'} @:{'terms.glossary.departments'}",
@@ -0,0 +1,15 @@
{
"compat": {
"invoicing_period": {
"multi_month_invoice_warning": {
"invoice_together": "Invoice together",
"split_by_month": "Split by month",
"split_error_title": "Monthly split failed",
"split_success_text": "Processed {processed} invoice collections. Split {changed}, skipped {skipped}.",
"split_success_title": "Monthly split completed",
"text": "You are about to invoice orders from multiple months together ({months}). Should they be split by month instead?",
"title": "Orders from multiple months"
}
}
}
}
@@ -140,6 +140,7 @@
"departments": {
"columns": {
"archived": "@:{'terms.glossary.archived'}",
"custom_pricing_only": "No fallback pricing",
"dimension": "Dimension",
"economic_department": "@.upper:{'terms.glossary.e'}-@:{'terms.glossary.conomic'} @.capitalize:{'terms.glossary.department'}",
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
@@ -91,6 +91,22 @@
"subtitle": "Role permissions",
"title": "Role {id} permissions",
"title_with_name": "{name} permissions"
},
"templates": {
"apply": "Apply {template} template",
"confirm_button": "Apply template",
"confirm_text": "Add {count} missing permissions from {template} to {role}?",
"confirm_title": "Apply permission template?",
"error_text": "One or more permissions from {template} could not be added to {role}. The role list was reloaded.",
"error_title": "Could not apply template",
"load_error": "Could not load templates. Try again",
"loading": "Loading templates...",
"section": "Limited backoffice templates",
"success_text": "Added {count} permissions from {template} to {role}.",
"success_title": "Template applied",
"unavailable": "No templates are available.",
"up_to_date_text": "{role} already has every permission in {template}.",
"up_to_date_title": "Role is up to date"
}
}
}
@@ -78,6 +78,7 @@
},
"employees": {
"create": "@.capitalize:{'terms.glossary.create'} @:{'terms.glossary.new'} @:{'terms.glossary.employee'}",
"migrate_to_limited_backoffice": "Migrate to limited backoffice",
"subtitle": "@.capitalize:{'terms.glossary.here'} @:{'terms.glossary.you'} @:{'terms.glossary.can'} @:{'terms.glossary.see'} @:{'terms.glossary.a'} @:{'terms.glossary.list'} @:{'terms.glossary.of'} @:{'terms.glossary.all'} @:{'terms.glossary.employees'} @:{'terms.glossary.in'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.system'}"
},
"orders": {
@@ -14,6 +14,31 @@
"transaction_history": "@.capitalize:{'terms.glossary.transaction'} @:{'terms.glossary.history'}"
},
"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'}",
"user_data": "@.capitalize:{'terms.glossary.user'} @:{'terms.glossary.data'}",
"user_id": "@.capitalize:{'terms.glossary.user'} @.upper:{'terms.glossary.id'}",
+175 -7
View File
@@ -32,6 +32,9 @@
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
@@ -46,6 +49,15 @@
"edit_title": "@:{'phrases.compat.limited_backoffice.employees.edit_title'}",
"name": "@:{'phrases.compat.limited_backoffice.employees.name'}",
"email": "@:{'phrases.compat.limited_backoffice.employees.email'}",
"user_id": "@:{'phrases.compat.limited_backoffice.employees.user_id'}",
"phone": "@:{'phrases.compat.limited_backoffice.employees.phone'}",
"phone_validation": "@:{'phrases.compat.limited_backoffice.employees.phone_validation'}",
"country_codes": {
"denmark": "@:{'phrases.compat.limited_backoffice.employees.country_codes.denmark'}",
"sweden": "@:{'phrases.compat.limited_backoffice.employees.country_codes.sweden'}",
"norway": "@:{'phrases.compat.limited_backoffice.employees.country_codes.norway'}",
"finland": "@:{'phrases.compat.limited_backoffice.employees.country_codes.finland'}"
},
"password": "@:{'phrases.compat.limited_backoffice.employees.password'}",
"password_create_help": "@:{'phrases.compat.limited_backoffice.employees.password_create_help'}",
"password_update_help": "@:{'phrases.compat.limited_backoffice.employees.password_update_help'}",
@@ -64,7 +76,13 @@
"active": "@:{'phrases.compat.limited_backoffice.employees.active'}",
"inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}",
"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": {
"viewer": {
@@ -94,7 +112,8 @@
"save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}",
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
"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": {
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
@@ -107,6 +126,11 @@
"groups": {
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
"products": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.products'}",
"customers": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.customers'}",
"vehicles": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.vehicles'}",
"attachments": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.attachments'}",
"scanner": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.scanner'}",
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
@@ -158,6 +182,66 @@
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"complete_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.complete_orders.description'}"
},
"view_product_catalog": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_catalog.description'}"
},
"view_product_recommendations": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_product_recommendations.description'}"
},
"search_customers": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_customers.description'}"
},
"view_customer_details": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_details.description'}"
},
"view_customer_notes": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_notes.description'}"
},
"add_customer_notes": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_customer_notes.description'}"
},
"view_customer_flags": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_customer_flags.description'}"
},
"search_vehicles": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.search_vehicles.description'}"
},
"view_vehicle_matches": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_matches.description'}"
},
"view_vehicle_history": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_vehicle_history.description'}"
},
"view_order_attachments": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_attachments.description'}"
},
"add_order_attachments": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.add_order_attachments.description'}"
},
"download_order_attachments": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.download_order_attachments.description'}"
},
"view_plate_scans": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_plate_scans.description'}"
},
"view_department_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
@@ -265,6 +349,9 @@
"title": "@.capitalize:{'terms.glossary.department'} @:{'terms.glossary.prices'}",
"save": "@.capitalize:{'terms.glossary.save'} @:{'terms.glossary.prices'}",
"saved": "@.capitalize:{'terms.glossary.prices'} @:{'terms.glossary.saved'}.",
"autosave_saving": "Saving prices...",
"autosave_pending": "Price changes pending.",
"autosave_saved": "All price changes saved.",
"product": "@.capitalize:{'terms.glossary.product'}",
"price": "@.capitalize:{'terms.glossary.department'} @:{'terms.glossary.price'}",
"price_required": "Every @:{'terms.glossary.product'} @:{'terms.glossary.must'} @:{'terms.glossary.have'} @:{'terms.glossary.a'} non-negative numeric @:{'terms.glossary.price'}.",
@@ -279,6 +366,15 @@
"edit_title": "@.capitalize:{'terms.glossary.edit'} @:{'terms.glossary.employee'}",
"name": "Name",
"email": "Email",
"user_id": "User ID",
"phone": "Phone",
"phone_validation": "Phone number must be 4-15 digits.",
"country_codes": {
"denmark": "Denmark",
"sweden": "Sweden",
"norway": "Norway",
"finland": "Finland"
},
"password": "@.capitalize:{'terms.glossary.password'}",
"password_create_help": "@.capitalize:{'terms.glossary.required'} @:{'terms.glossary.for'} new @:{'terms.glossary.employees'}. Use @:{'terms.glossary.at'} @:{'terms.glossary.least'} 8 characters.",
"password_update_help": "Leave blank @:{'terms.glossary.to'} keep @:{'terms.replication.article.host_mention'} existing @:{'terms.glossary.password'}.",
@@ -297,16 +393,22 @@
"active": "Active",
"inactive": "@.capitalize:{'terms.glossary.inactive'}",
"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": {
"viewer": {
"label": "Viewer",
"description": "@.capitalize:{'terms.glossary.can'} sign @:{'terms.glossary.in'} @:{'terms.glossary.and'} view @:{'terms.glossary.assigned'} @:{'terms.glossary.department'} data."
"label": "Deactivated",
"description": "Keeps the employee registered without order, booking, or management permissions."
},
"cashier": {
"label": "Cashier",
"description": "@.capitalize:{'terms.glossary.can'} work with @:{'terms.glossary.orders'} @:{'terms.glossary.and'} order lines @:{'terms.glossary.for'} @:{'terms.glossary.assigned'} @:{'terms.glossary.departments'}."
"description": "@.capitalize:{'terms.glossary.can'} work with @:{'terms.glossary.orders'}, order lines @:{'terms.glossary.and'} @:{'terms.glossary.bookings'} @:{'terms.glossary.for'} @:{'terms.glossary.assigned'} @:{'terms.glossary.departments'}."
},
"booking_coordinator": {
"label": "Booking coordinator",
@@ -327,7 +429,8 @@
"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'}.",
"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": {
"title": "Role permissions",
@@ -340,6 +443,11 @@
"groups": {
"account": "Account",
"orders": "Orders",
"products": "Products",
"customers": "Customers",
"vehicles": "Vehicles",
"attachments": "Attachments",
"scanner": "Scanners",
"bookings": "Bookings",
"time_bookings": "Time bookings",
"reports": "Reports",
@@ -391,6 +499,66 @@
"label": "Charge orders",
"description": "Can take payment or charge a payment intent for an order."
},
"complete_orders": {
"label": "Complete orders",
"description": "Can mark orders as completed."
},
"view_product_catalog": {
"label": "View product catalog",
"description": "Can load POS categories, products, prices, and addons."
},
"view_product_recommendations": {
"label": "View product recommendations",
"description": "Can see product suggestions based on orders and vehicle data."
},
"search_customers": {
"label": "Search customers",
"description": "Can search and select customers for POS orders."
},
"view_customer_details": {
"label": "View customer details",
"description": "Can load customer details from a customer number."
},
"view_customer_notes": {
"label": "View customer notes",
"description": "Can read customer notes in POS."
},
"add_customer_notes": {
"label": "Add customer notes",
"description": "Can add customer notes from POS."
},
"view_customer_flags": {
"label": "View customer flags",
"description": "Can see customer flags that affect order requirements and product rules."
},
"search_vehicles": {
"label": "Search vehicles",
"description": "Can search license plates and see vehicle status."
},
"view_vehicle_matches": {
"label": "View vehicle matches",
"description": "Can see vehicle customer matches and unknown vehicle suggestions."
},
"view_vehicle_history": {
"label": "View vehicle history",
"description": "Can see recent order history for a license plate."
},
"view_order_attachments": {
"label": "View order attachments",
"description": "Can list attachments on orders."
},
"add_order_attachments": {
"label": "Add order attachments",
"description": "Can upload files and attach wash certificates to orders."
},
"download_order_attachments": {
"label": "Download order attachments",
"description": "Can open and download order attachments."
},
"view_plate_scans": {
"label": "View plate scans",
"description": "Can view recent license plate scans and scanners for assigned departments."
},
"view_department_bookings": {
"label": "View department bookings",
"description": "Can see bookings for assigned departments."
@@ -34,6 +34,14 @@
"subtitle_suffix": "@:{'phrases.compat.admin.daily_report.product_sales.subtitle_suffix'}",
"title": "@:{'phrases.compat.admin.daily_report.product_sales.title'}"
},
"product_targets": {
"cancel_aria": "@:{'phrases.compat.admin.daily_report.product_targets.cancel_aria'}",
"edit_aria": "@:{'phrases.compat.admin.daily_report.product_targets.edit_aria'}",
"input_label": "@:{'phrases.compat.admin.daily_report.product_targets.input_label'}",
"save_aria": "@:{'phrases.compat.admin.daily_report.product_targets.save_aria'}",
"save_error": "@:{'phrases.compat.admin.daily_report.product_targets.save_error'}",
"validation_range": "@:{'phrases.compat.admin.daily_report.product_targets.validation_range'}"
},
"reports": {
"avg_revenue": {
"label": "@:{'phrases.compat.department_reports.avg_revenue_per_wash'}",
@@ -293,6 +301,7 @@
"delete": "@:{'phrases.compat.global.delete'}",
"details": "@:{'phrases.compat.admin.pos.details'}",
"download": "@:{'phrases.compat.guest.pwa.title'}",
"ean": "@:{'phrases.compat.admin.pos.ean'}",
"drafts_assignment": {
"button": "@:{'phrases.compat.admin.pos.drafts_assignment.button'}",
"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_desc": "@:{'phrases.compat.tables.bookings.wash_desc'}",
"cvr_label": "@:{'phrases.compat.customers.cvr'}",
"ean_label": "@:{'phrases.compat.customer_creation.customer.ean_label'}",
"email_label": "@:{'phrases.compat.customer_creation.customer.email_label'}",
"intro": "@:{'phrases.compat.customer_creation.customer.intro'}",
"phone_label": "@:{'phrases.compat.customer_creation.customer.phone_label'}",
@@ -38,6 +38,13 @@
"title": "@:{'phrases.compat.departments.tab.opening_hours'}"
},
"phone": "@:common.phone",
"pricing": {
"custom_pricing_disabled": "@:{'phrases.compat.departments.pricing.custom_pricing_disabled'}",
"custom_pricing_enabled": "@:{'phrases.compat.departments.pricing.custom_pricing_enabled'}",
"custom_pricing_missing_price": "@:{'phrases.compat.departments.pricing.custom_pricing_missing_price'}",
"custom_pricing_only": "@:{'phrases.compat.departments.pricing.custom_pricing_only'}",
"effective_department_price": "@:{'phrases.compat.departments.pricing.effective_department_price'}"
},
"products": {
"new_product": "@:products.new_product"
},
@@ -0,0 +1,13 @@
{
"invoicing_period": {
"multi_month_invoice_warning": {
"invoice_together": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.invoice_together'}",
"split_by_month": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.split_by_month'}",
"split_error_title": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.split_error_title'}",
"split_success_text": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.split_success_text'}",
"split_success_title": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.split_success_title'}",
"text": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.text'}",
"title": "@:{'phrases.compat.invoicing_period.multi_month_invoice_warning.title'}"
}
}
}
@@ -194,6 +194,7 @@
"departments": {
"columns": {
"archived": "@:{'phrases.compat.objects.departments.columns.archived'}",
"custom_pricing_only": "@:{'phrases.compat.objects.departments.columns.custom_pricing_only'}",
"dimension": "@:{'phrases.compat.objects.departments.columns.dimension'}",
"economic_department": "@:{'phrases.compat.objects.departments.columns.economic_department'}",
"latitude": "@:{'phrases.compat.objects.departments.columns.latitude'}",
@@ -91,6 +91,22 @@
"title": "@:{'phrases.compat.roles.permissions.title'}",
"title_with_name": "@:{'phrases.compat.roles.permissions.title_with_name'}"
},
"templates": {
"apply": "@:{'phrases.compat.roles.templates.apply'}",
"confirm_button": "@:{'phrases.compat.roles.templates.confirm_button'}",
"confirm_text": "@:{'phrases.compat.roles.templates.confirm_text'}",
"confirm_title": "@:{'phrases.compat.roles.templates.confirm_title'}",
"error_text": "@:{'phrases.compat.roles.templates.error_text'}",
"error_title": "@:{'phrases.compat.roles.templates.error_title'}",
"load_error": "@:{'phrases.compat.roles.templates.load_error'}",
"loading": "@:{'phrases.compat.roles.templates.loading'}",
"section": "@:{'phrases.compat.roles.templates.section'}",
"success_text": "@:{'phrases.compat.roles.templates.success_text'}",
"success_title": "@:{'phrases.compat.roles.templates.success_title'}",
"unavailable": "@:{'phrases.compat.roles.templates.unavailable'}",
"up_to_date_text": "@:{'phrases.compat.roles.templates.up_to_date_text'}",
"up_to_date_title": "@:{'phrases.compat.roles.templates.up_to_date_title'}"
},
"subtitle": "@:{'phrases.compat.roles.subtitle'}",
"title": "@:{'phrases.compat.common.roles'}"
}
@@ -40,6 +40,82 @@
"subtitle": "@:{'phrases.compat.superuser_dashboard.departments.subtitle'}",
"title": "@:common.departments"
},
"department_navigation": {
"overview": "Overview",
"modules": "Modules",
"branding": "Profile & Branding",
"gateways": "@.capitalize:{'terms.glossary.gateways'}",
"stripe": "@:{'terms.glossary.stripe'}",
"pricing": "Pricing",
"categories": "Categories"
},
"department_overview": {
"title": "Department overview",
"subtitle": "Operational overview for the selected department",
"loading": "Loading department overview",
"date_from": "From",
"date_to": "To",
"range_label": "{from} to {to}",
"empty_value": "-",
"out_of": "of {total}",
"presets": {
"today": "Today",
"last_seven_days": "Last 7 days"
},
"metrics": {
"bookings": "Bookings",
"complaints": "Complaints",
"night_washes": "Night washes",
"overtime": "Overtime",
"products_sold": "Products sold",
"revenue": "Revenue",
"transactions": "Transactions",
"washes": "Washes",
"water_usage": "Water"
},
"units": {
"hours": "h",
"liters": "L"
},
"products": {
"title": "Product mix",
"subtitle": "Tracked wash products in the selected period",
"empty": "No product activity for the selected period"
},
"profile": {
"title": "Department profile",
"no_description": "No department description",
"department_id": "Department ID",
"economic_department_id": "Economic department",
"branding": "Branding",
"created_at": "@:common.created",
"updated_at": "Updated"
},
"hardware": {
"title": "Hardware readiness",
"subtitle": "Gateway-backed lane and relay state",
"gateways": "Gateways online",
"lanes": "Lanes",
"gates": "Gates",
"relays": "Relays",
"scanners": "Scanners",
"issues": "Issues"
},
"quick_links": {
"title": "Department tools",
"subtitle": "Open the focused setup areas for this department",
"modules": "@:superuser_dashboard.department_navigation.modules",
"branding": "@:superuser_dashboard.department_overview.profile.branding",
"gateways": "@:superuser_dashboard.department_navigation.gateways",
"stripe": "@:superuser_dashboard.department_navigation.stripe",
"pricing": "@:superuser_dashboard.department_navigation.pricing",
"categories": "@:superuser_dashboard.department_navigation.categories"
},
"errors": {
"invalid_department": "A valid department is required",
"load": "Unable to load the department overview"
}
},
"employees": {
"edit_employee_title": "@:employees.edit_employee",
"new_employee_subtitle": "@:{'phrases.compat.superuser_dashboard.employees.new_employee_subtitle'}",
@@ -22,6 +22,31 @@
"transaction_history": "@:{'phrases.compat.user_admin.orders.transaction_history'}"
},
"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'}",
"subtitle": "@:user_admin.user_data",
"title": "@:{'phrases.compat.common.user'}",
@@ -27,6 +27,14 @@
"subtitle_suffix": "@:{'terms.glossary.produktsalg'}",
"title": "@.capitalize:{'terms.glossary.produktsalg'}"
},
"product_targets": {
"cancel_aria": "Avbryt mål",
"edit_aria": "Rediger målprosent",
"input_label": "Målprosent",
"save_aria": "Lagre mål",
"save_error": "Målet kunne ikke lagres.",
"validation_range": "Målprosenten må være mellom 0 og 100."
},
"reports": {
"bookings": {
"completed_count": "@.capitalize:{'terms.glossary.fullført'} @:{'terms.glossary.telling'}",
@@ -221,6 +229,7 @@
"company_phone": "Firmatelefon",
"contact_email": "@.capitalize:{'terms.glossary.kontakt'} @:{'terms.glossary.e'}-@:{'terms.glossary.post'}",
"contact_phone": "@.capitalize:{'terms.glossary.kontakt'} @:{'terms.glossary.telefon'}",
"ean": "EAN",
"created_by": "Laget @:{'terms.glossary.av'}",
"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'}.",
@@ -7,6 +7,7 @@
"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_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'}",
"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'}",
@@ -21,6 +21,13 @@
"opening_hours": {
"closed": "Stengt"
},
"pricing": {
"custom_pricing_disabled": "Fallback aktiv",
"custom_pricing_enabled": "Kun egendefinerte priser",
"custom_pricing_missing_price": "Manglende avdelingspriser blir 999999.",
"custom_pricing_only": "Ingen fallback-priser",
"effective_department_price": "Effektiv avdelingspris"
},
"search_placeholder": "@.capitalize:{'terms.glossary.søk'} @:{'terms.glossary.etter'} @:{'terms.glossary.avdelingsnavn'}",
"select_department": "@.capitalize:{'terms.glossary.velg'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.avdeling'}",
"subtitle": "@.capitalize:{'terms.glossary.administrer'} @:{'terms.glossary.avdelinger'}",
@@ -0,0 +1,15 @@
{
"compat": {
"invoicing_period": {
"multi_month_invoice_warning": {
"invoice_together": "Fakturer samlet",
"split_by_month": "Del opp etter måned",
"split_error_title": "Månedsdeling mislyktes",
"split_success_text": "Behandlet {processed} fakturasamlinger. Delte opp {changed}, hoppet over {skipped}.",
"split_success_title": "Månedsdeling fullført",
"text": "Du er i ferd med å fakturere ordrer fra flere måneder samlet ({months}). Skal de i stedet deles opp etter måned?",
"title": "Ordrer fra flere måneder"
}
}
}
}
@@ -140,6 +140,7 @@
"departments": {
"columns": {
"archived": "Arkivert",
"custom_pricing_only": "@:{'phrases.compat.departments.pricing.custom_pricing_only'}",
"dimension": "Dimensjon",
"economic_department": "@.upper:{'terms.glossary.e'}-@:{'terms.glossary.økonomisk'} @:{'terms.glossary.avdeling'}",
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
@@ -91,6 +91,22 @@
"subtitle": "Rolletillatelser",
"title": "Tillatelser for rolle {id}",
"title_with_name": "Tillatelser for {name}"
},
"templates": {
"apply": "Bruk {template}-mal",
"confirm_button": "Bruk mal",
"confirm_text": "Legg til {count} manglende tillatelser fra {template} til {role}?",
"confirm_title": "Bruke tillatelsesmal?",
"error_text": "En eller flere tillatelser fra {template} kunne ikke legges til i {role}. Rollelisten ble lastet inn pa nytt.",
"error_title": "Kunne ikke bruke mal",
"load_error": "Kunne ikke laste maler. Prov igjen",
"loading": "Laster maler...",
"section": "Begrenset backoffice-maler",
"success_text": "La til {count} tillatelser fra {template} i {role}.",
"success_title": "Mal brukt",
"unavailable": "Ingen maler er tilgjengelige.",
"up_to_date_text": "{role} har allerede alle tillatelser i {template}.",
"up_to_date_title": "Rollen er oppdatert"
}
}
}
@@ -78,6 +78,7 @@
},
"employees": {
"create": "@:{'terms.glossary.opprett'} @:{'terms.glossary.ny'} medarbeider",
"migrate_to_limited_backoffice": "Migrer til begrenset backoffice",
"subtitle": "@.capitalize:{'terms.glossary.her'} @:{'terms.glossary.kan'} @:{'terms.glossary.du'} @:{'terms.glossary.se'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.liste'} @:{'terms.glossary.over'} @:{'terms.glossary.alle'} @:{'terms.glossary.ansatte'} @:{'terms.glossary.i'} @:{'terms.glossary.systemet'}"
},
"orders": {
@@ -14,6 +14,31 @@
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
},
"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'}",
"user_data": "@.capitalize:{'terms.glossary.brukerdata'}",
"user_id": "@.capitalize:{'terms.glossary.bruker'}-@.upper:{'terms.glossary.id'}",
@@ -2,6 +2,20 @@
"compat": {
"vehicles": {
"add": "@:{'terms.glossary.lagg'} @:{'terms.glossary.till'} @:{'terms.glossary.fordon'}",
"add_modal": {
"customer_label": "Kunde",
"customer_placeholder": "Søk etter kundenavn eller kundenummer",
"error": "Kunne ikke legge til kjøretøy.",
"no_customer_results": "Ingen kunder funnet",
"reference_placeholder": "Valgfri referanse",
"registration_placeholder": "Registreringsnummer",
"selected_customer": "Valgt kunde",
"submit": "Legg til kjøretøy",
"title": "Legg til kjøretøy",
"type_load_error": "Kunne ikke laste kjøretøytyper.",
"type_placeholder": "Velg kjøretøytype",
"validation_error": "Velg kunde, registreringsnummer og kjøretøytype."
},
"brand": "Märke",
"color": "Färg",
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",

Some files were not shown because too many files have changed in this diff Show More