Compare commits

...
Author SHA1 Message Date
Jeppe Bundgaard 398045b519 Merge remote-tracking branch 'origin/master' into codex/github-hosted-test-runners
# Conflicts:
#	.github/workflows/tests.yml
#	tests/e2e/self-serve-wash.spec.js
#	tests/unit/playwright-full-workflow.spec.js
2026-07-06 22:37:52 +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 Bundgaard bfb1386d49 Merge remote-tracking branch 'origin/master' into codex/github-hosted-test-runners
# 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/departments/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
2026-07-06 22:35:31 +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 e495a099e4 Gate full E2E on focused regressions 2026-07-06 22:30:39 +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 c4e29a896a Stabilize release assignment E2E selection 2026-07-06 21:26:16 +02:00
Jeppe Bundgaard 3c49cec213 Review dynamic i18n view keys 2026-07-06 20:47:51 +02:00
Jeppe Bundgaard 9f7ebad5fe Fix hosted full E2E i18n failures 2026-07-06 20:16:16 +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 98d0fbc8c9 Merge remote-tracking branch 'origin/master' into codex/github-hosted-test-runners
# Conflicts:
#	.github/workflows/tests.yml
2026-07-06 19:31:21 +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 fef1fc3c12 Allow hosted runners for manual frontend tests 2026-07-06 19:26:28 +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 BandJeppe Bundgaard 278298ecd2 Submit error reports without screenshots (#153)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 14:28:24 +02:00
Jeppe Bundgaard b8959cb7bb Cleanup superuser dashboard tabs 2026-07-06 14:07:36 +02:00
Jeppe BandJeppe Bundgaard dec90bbbbf Block restricted mobile additional items (#148)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 14:07:04 +02:00
Jeppe BandJeppe Bundgaard a4b9d2f04d Add superuser vehicle creation flow (#152)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 13:59:40 +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 BandJeppe Bundgaard 22f8b5f203 Improve superuser role permissions page (#149)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 12:43:03 +02:00
Jeppe B 987acc5b0f Add role permission help modal (#142)
Adds the Rolle help icon and grouped permissions modal for limited backoffice employees, plus i18n and Playwright coverage.
2026-07-06 12:25:01 +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
Jeppe BandJeppe Bundgaard cf1509e34f Fix POS step metadata persistence (#138)
* Fix POS step metadata persistence

* Preserve registration save payload formatting

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 10:20:13 +02:00
Jeppe BandJeppe Bundgaard ce2e671768 Fix department notification SMS table refresh
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 09:57:30 +02:00
Jeppe BandJeppe Bundgaard cf2add29dd Default vehicle subscriptions to false (#136)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 09:21:31 +02:00
269 changed files with 18436 additions and 1956 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
+103 -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/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,55 @@ 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: [
/^src\/views\/dashboards\/superUserDashboard\/roles\/SuperUserRolesPermissions\.vue$/u,
/^src\/views\/dashboards\/superUserDashboard\/roles\/rolePermissionCatalog\.js$/u,
],
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],
@@ -42,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,
},
{
@@ -52,6 +117,31 @@ export const sourceMappings = [
specs: ["tests/e2e/edge-gateways.smoke.spec.js", "tests/e2e/edge-gateways.routes.spec.js"],
projects: chromiumProjects,
},
{
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],
@@ -74,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],
+4
View File
@@ -57,6 +57,7 @@ export const ownedFilesByRole = {
admin: [
"admin-bookings-mobile.spec.ts",
"admin-daily-report.spec.ts",
"admin-department-notifications.spec.ts",
"admin-department-visibility.spec.ts",
"admin-overview-mobile.spec.ts",
"admin-overview-night-washes.spec.ts",
@@ -100,11 +101,14 @@ 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",
"superuser-roles-permissions.spec.ts",
"superuser-system-status.smoke.spec.js",
"superuser-users.spec.ts",
"superuser-vehicles.smoke.spec.js",
+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 = "";
}
}
};
@@ -5,6 +5,7 @@ import { useWindowSize } from "@vueuse/core";
import { BMessage } from "buefy";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
type DateRange = {
startDate: Date,
@@ -115,23 +116,11 @@ const availableYears = computed(() => (
));
const formatDateInputValue = (date: Date) => {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
return "";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
return formatLocalDateOnly(date);
};
const parseDateInputValue = (value: string) => {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
if (!match) {
return new Date(value);
}
return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
return parseLocalDateOnly(value);
};
const isSameDateInputValue = (left: Date, right: Date) => (
@@ -4,12 +4,15 @@ import { useI18n } from 'vue-i18n';
const { t } = useI18n();
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
import { loadList } from "@/components/pagination/paginatedList.vue";
const props = defineProps({
objects: {
type: Array,
required: true,
},
loadList: {
type: Function,
required: true,
},
});
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { ref } from 'vue';
@@ -22,6 +25,52 @@ const su_object = SessionUser.objects.department_notification_sms;
const hasUpdatePermission = () => {
return (SessionUser.hasPermission('department_notification_sms_update') || SessionUser.canAccessSuperUser());
};
const enabledSavingIds = ref(new Set());
const isEnabled = (value) => {
return value === true || value === 1 || value === "1" || value === "true";
};
const isEnabledSaving = (id) => {
return enabledSavingIds.value.has(Number(id));
};
const setEnabledSaving = (id, saving) => {
const nextIds = new Set(enabledSavingIds.value);
const normalizedId = Number(id);
if (saving) {
nextIds.add(normalizedId);
} else {
nextIds.delete(normalizedId);
}
enabledSavingIds.value = nextIds;
};
const toggleEnabled = async (obj, event) => {
const target = event?.target;
const previousValue = isEnabled(obj.enabled);
if (!target || !hasUpdatePermission() || isEnabledSaving(obj.id)) {
if (target) {
target.checked = previousValue;
}
return;
}
const nextValue = Boolean(target.checked);
setEnabledSaving(obj.id, true);
try {
await su_object.set.enabled(obj.id, nextValue);
await props.loadList();
} catch (error) {
target.checked = previousValue;
console.error('Unable to update department notification SMS active state', error);
} finally {
setEnabledSaving(obj.id, false);
}
};
</script>
<template>
@@ -42,7 +91,7 @@ const hasUpdatePermission = () => {
<!-- Label -->
<EditableTableColumn
:object="obj"
:loadList="loadList"
:loadList="props.loadList"
:editFunction="su_object.showEditObjectFieldForm"
column="label"
:permission-check-function="hasUpdatePermission"
@@ -50,7 +99,7 @@ const hasUpdatePermission = () => {
<!-- Phone country code -->
<EditableTableColumn
:object="obj"
:loadList="loadList"
:loadList="props.loadList"
:editFunction="su_object.showEditObjectFieldForm"
column="phone_country_code"
:permission-check-function="hasUpdatePermission"
@@ -58,22 +107,27 @@ const hasUpdatePermission = () => {
<!-- Phone -->
<EditableTableColumn
:object="obj"
:loadList="loadList"
:loadList="props.loadList"
:editFunction="su_object.showEditObjectFieldForm"
column="phone"
:permission-check-function="hasUpdatePermission"
/>
<!-- Enabled -->
<EditableTableColumn
:object="obj"
:loadList="loadList"
:editFunction="su_object.showEditObjectFieldForm"
column="enabled"
:parse-function="(value) => {
return value ? t('common.yes') : t('common.no');
}"
:permission-check-function="hasUpdatePermission"
/>
<td>
<input
:id="`department-notification-sms-enabled-${obj.id}`"
class="switch is-rounded is-small"
type="checkbox"
:checked="isEnabled(obj.enabled)"
:disabled="!hasUpdatePermission() || isEnabledSaving(obj.id)"
:data-testid="`department-notification-sms-enabled-toggle-${obj.id}`"
:aria-label="`${su_object.columns.enabled.label}: ${obj.label}`"
@change="toggleEnabled(obj, $event)"
/>
<label :for="`department-notification-sms-enabled-${obj.id}`">
{{ isEnabled(obj.enabled) ? t('common.yes') : t('common.no') }}
</label>
</td>
<!-- Actions -->
<td>
<ActionSettingsWheelButton class="is-float-right">
@@ -81,9 +135,10 @@ const hasUpdatePermission = () => {
<RequiresPermission :permission="'department_notification_sms_delete'">
<template #default>
<ActionSettingsWheelItem
@click="su_object.functions.showDeleteObjectForm(obj.id, loadList)"
@click="su_object.functions.showDeleteObjectForm(obj.id, props.loadList)"
:icon="SessionUser.objects.global.icons.delete"
:label="t('global.delete')"
:test-id="`department-notification-sms-delete-${obj.id}`"
template="danger"
/>
</template>
@@ -91,6 +146,7 @@ const hasUpdatePermission = () => {
<ActionSettingsWheelItem
:icon="SessionUser.objects.global.icons.delete"
:label="t('global.delete')"
:test-id="`department-notification-sms-delete-${obj.id}`"
template="danger"
disabled
/>
@@ -111,4 +167,4 @@ const hasUpdatePermission = () => {
<style scoped>
</style>
</style>
@@ -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>
@@ -1,9 +1,12 @@
<script setup>
import { computed, nextTick, ref, watch } from "vue";
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
import { isBlankPosMetadataValue } from "@/components/shop/POSDepartmentProcess.vue";
import {
isBlankPosMetadataValue,
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
const props = defineProps({
order_id: {
@@ -123,7 +126,7 @@ const referenceField = createCustomerWishField({
key: "reference",
label: computed(() => t("common.reference")),
source: () => props.reference,
saveValue: (value) => SessionUser.objects.orders.set.reference(props.order_id, value),
saveValue: (value) => saveOrderMetadataField("reference", value, props.order_id),
testIdBase: "pos-order-customer-wishes-reference",
isRequired: () => props.referenceRequired,
warningStateWhenEmpty: "danger",
@@ -134,7 +137,7 @@ const poField = createCustomerWishField({
key: "po",
label: computed(() => t("admin.pos.po_number")),
source: () => props.po,
saveValue: (value) => SessionUser.objects.orders.set.po(props.order_id, value),
saveValue: (value) => saveOrderMetadataField("po", value, props.order_id),
testIdBase: "pos-order-customer-wishes-po",
isRequired: () => props.poRequired,
warningStateWhenEmpty: "warning",
@@ -145,7 +148,7 @@ const safetySealField = createCustomerWishField({
key: "safety_seal",
label: computed(() => t("admin.pos.safety_seal")),
source: () => props.safetySeal,
saveValue: (value) => SessionUser.objects.orders.set.safety_seal(props.order_id, value),
saveValue: (value) => saveOrderMetadataField("safety_seal", value, props.order_id),
testIdBase: "pos-order-customer-wishes-safety-seal",
isRequired: () => false,
warningStateWhenEmpty: null,
@@ -161,6 +164,15 @@ const safetySealField = createCustomerWishField({
const fields = computed(() => {
return props.showSafetySeal ? [referenceField, poField, safetySealField] : [referenceField, poField];
});
const flushCustomerWishFields = async () => {
await Promise.all(fields.value.map((field) => field.autosave.flushAndWait()));
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(flushCustomerWishFields);
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
});
</script>
<template>
@@ -1,8 +1,11 @@
<script setup>
import { computed, nextTick, ref, watch } from 'vue';
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
import {
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
const props = defineProps({
order_id: {
@@ -30,8 +33,7 @@ const isEditing = ref(false);
const autosave = useOrderMetadataAutosave({
source: () => props.note,
saveValue: async (value) => {
await SessionUser.objects.orders.set.notes(props.order_id, value);
return value;
return saveOrderMetadataField("notes", value, props.order_id);
},
onSaved: async (savedValue, requestedValue) => {
if (typeof props.onSaved === "function") {
@@ -68,6 +70,12 @@ const lostFocus = async () => {
isEditing.value = false;
await autosave.flush();
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(() => autosave.flushAndWait());
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
});
</script>
<template>
@@ -1,8 +1,11 @@
<script setup>
import { computed, nextTick, ref, watch } from "vue";
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
import {
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
const props = defineProps({
index: {
@@ -36,8 +39,7 @@ autosave = useOrderMetadataAutosave({
source: () => props.modelValue,
normalizeValue: normalizeRegistrationDraft,
saveValue: async (value) => {
await SessionUser.objects.orders.set[fieldKey.value](props.order_id, value);
return value;
return saveOrderMetadataField(fieldKey.value, value, props.order_id);
},
onSaved: async () => {
await props.loadOrder();
@@ -74,6 +76,12 @@ const closeEditor = async () => {
isEditing.value = false;
await autosave.flush();
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(() => autosave.flushAndWait());
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
});
</script>
<template>
@@ -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>
@@ -30,6 +30,7 @@ import PosLastScannedLicensePlatesV2 from "@/components/displays/department/pos/
import PosDesktopOrderBookingSelectorModal from "@/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue";
import PosDesktopCustomerConflictModal from "@/components/displays/department/pos/steps/elements/PosDesktopCustomerConflictModal.vue";
import PosDesktopDuplicateWarning from "@/components/displays/department/pos/steps/elements/PosDesktopDuplicateWarning.vue";
import { todayLocalDateOnly } from "@/services/dateOnly.js";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { parsePosRouteSearch } from "@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js";
import {
@@ -377,8 +378,8 @@ const fetchDuplicateOrdersForContext = async (context) => {
try {
const response = await SessionUser.request(SessionUser.objects.orders.meta.endpoint, "GET", {
filters: `reg_1:${normalizedContext.reg1},department_id:${department_id.value},created_at-date_from:${
new Date().toISOString().split("T")[0]
},created_at-date_to:${new Date().toISOString().split("T")[0]}`,
todayLocalDateOnly()
},created_at-date_to:${todayLocalDateOnly()}`,
limit: 5,
});
@@ -4,7 +4,7 @@ import {
setBackgroundColor,
backgroundColors,
} from "@/components/viewport/page/headers/ViewportHeaderSettings.vue";
import { onMounted, ref, watch, computed } from "vue";
import { onBeforeUnmount, onMounted, ref, watch, computed } from "vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import PosDepartmentStepMobile2Product from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Product.vue";
import PosDepartmentStepMobile2LastOrder from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2LastOrder.vue";
@@ -43,6 +43,8 @@ import {
customer_name,
isAddonRestricted,
canBuyAdditionalServices,
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
import { createOrderItem, getOrderItems, removeOrderItem } from "@/components/shop/OrdersItems.vue";
import { PosProduct } from "@/components/displays/department/pos/steps/mobile/objects/PosProduct.vue";
@@ -196,8 +198,7 @@ const applyPendingBookingFromSelection = async () => {
// Set PO number on the order if provided by the booking
if (booking.po && typeof booking.po === "string" && booking.po.trim() !== "") {
try {
await SessionUser.objects.orders.set.po(order_id.value, booking.po);
order_po.value = booking.po.trim();
order_po.value = await saveOrderMetadataField("po", booking.po, order_id.value);
} catch (e) {
console.error("Failed to set PO on order from booking", e);
}
@@ -217,9 +218,9 @@ const applyPendingBookingFromSelection = async () => {
const bookingReference = String(fullBookingData.reference ?? fullBookingData.reference_number ?? "").trim();
if (bookingReference !== "") {
try {
await SessionUser.objects.orders.set.reference(order_id.value, bookingReference);
persistedReference.value = bookingReference;
metadata.setReference(bookingReference);
const savedReference = await saveOrderMetadataField("reference", bookingReference, order_id.value);
persistedReference.value = savedReference;
metadata.setReference(savedReference);
hydrateReferenceFromSources();
} catch (setRefError) {
console.error("Failed to set reference on order from booking:", setRefError);
@@ -228,10 +229,10 @@ const applyPendingBookingFromSelection = async () => {
const bookingNotes = String(fullBookingData.notes ?? fullBookingData.note ?? "").trim();
if (bookingNotes !== "") {
try {
await SessionUser.objects.orders.set.notes(order_id.value, bookingNotes);
order_notes.value = bookingNotes;
notes.value = bookingNotes;
metadata.setNotes(bookingNotes);
const savedNotes = await saveOrderMetadataField("notes", bookingNotes, order_id.value);
order_notes.value = savedNotes;
notes.value = savedNotes;
metadata.setNotes(savedNotes);
} catch (setNotesError) {
console.error("Failed to set notes on order from booking:", setNotesError);
}
@@ -243,13 +244,11 @@ const applyPendingBookingFromSelection = async () => {
const currentReg2 = normalizeRegistrationValue(reg_2.value);
if (bookingReg1 && bookingReg1 !== currentReg1) {
reg_1.value = bookingReg1;
await SessionUser.objects.orders.set.reg_1(order_id.value, bookingReg1);
reg_1.value = await saveOrderMetadataField("reg_1", bookingReg1, order_id.value);
}
if (bookingReg2 !== currentReg2) {
reg_2.value = bookingReg2;
await SessionUser.objects.orders.set.reg_2(order_id.value, bookingReg2);
reg_2.value = await saveOrderMetadataField("reg_2", bookingReg2, order_id.value);
}
const bookingMatches = Array.isArray(vehicles.vehicle_1.value?.booking_matches)
@@ -538,8 +537,7 @@ const notesAutosave = useOrderMetadataAutosave({
return value;
}
await SessionUser.objects.orders.set.notes(normalizedOrderId, value);
return value;
return saveOrderMetadataField("notes", value, normalizedOrderId);
},
onSaved: async (value) => {
order_notes.value = value;
@@ -555,8 +553,7 @@ const referenceAutosave = useOrderMetadataAutosave({
return value;
}
await SessionUser.objects.orders.set.reference(normalizedOrderId, value);
return value;
return saveOrderMetadataField("reference", value, normalizedOrderId);
},
onSaved: async (value) => {
persistedReference.value = value;
@@ -572,8 +569,7 @@ const safetySealAutosave = useOrderMetadataAutosave({
return value;
}
await SessionUser.objects.orders.set.safety_seal(normalizedOrderId, value);
return value;
return saveOrderMetadataField("safety_seal", value, normalizedOrderId);
},
onSaved: async (value) => {
order_safety_seal.value = value;
@@ -585,6 +581,18 @@ const stepTwoNotesInput = notesAutosave.draft;
const stepTwoReferenceInput = referenceAutosave.draft;
const stepTwoSafetySealInput = safetySealAutosave.draft;
const showSafetySealField = computed(() => transactionItems.containsWashCertificate());
const flushStepTwoMetadataFields = async () => {
await Promise.all([
notesAutosave.flushAndWait(),
referenceAutosave.flushAndWait(),
safetySealAutosave.flushAndWait(),
]);
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(flushStepTwoMetadataFields);
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
});
const resolveReferenceFromSources = () => {
const orderReference = normalizeReferenceValue(persistedReference.value);
@@ -22,7 +22,7 @@ import PosDepartmentStep2MobileVehicleSelection
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
import PosDepartmentStepMobile2FloatingCart
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2FloatingCart.vue";
import { isAddonRestricted, canBuyAdditionalServices } from "@/components/shop/POSDepartmentProcess.vue";
import { isAddonRestricted, canBuyAdditionalServices, isProductRestricted } from "@/components/shop/POSDepartmentProcess.vue";
const props = defineProps({
label: {
@@ -46,6 +46,7 @@ const props = defineProps({
},
});
const canSelectAdditionalItems = computed(() => canBuyAdditionalServices());
const checked = ref(props.defaultChecked);
// Function to generate a summary from the last order
function generateSummary(order: PosOrder): string {
@@ -72,6 +73,12 @@ const displaySubtitle = computed(() => {
});
// Emit event on toggle
function onToggle(isOpen: boolean) {
if (isOpen && !canSelectAdditionalItems.value) {
checked.value = false;
pos.views.additionalItemSelection.value = false;
return;
}
checked.value = isOpen;
// Open the additional item selection view if toggled open
if (isOpen) {
@@ -118,17 +125,45 @@ const getAvailableAdditionalItems = () => {
}));
}
const availableAdditionalItems = ref<Addon[]>(getAvailableAdditionalItems());
const isAdditionalItemRestricted = (product: PosProduct) => {
if (!canSelectAdditionalItems.value) {
return true;
}
if (isProductRestricted(product)) {
return true;
}
return isAddonRestricted(convertProductToAddon(product));
}
watch(canSelectAdditionalItems, (canSelect) => {
if (!canSelect) {
checked.value = false;
pos.views.additionalItemSelection.value = false;
}
});
// Computed property to filter out restricted additional items based on customer attributes
const filteredAdditionalItems = computed(() => {
// If additional services are restricted, return empty array
if (!canBuyAdditionalServices()) {
if (!canSelectAdditionalItems.value) {
return [];
}
// Filter out individually restricted addons
return availableAdditionalItems.value.filter((addon: Addon) => !isAddonRestricted(addon));
return availableAdditionalItems.value.filter((addon: Addon) => {
if (isAddonRestricted(addon)) {
return false;
}
if (addon.product && isProductRestricted(addon.product)) {
return false;
}
return true;
});
});
const onClickAddOtherProduct = () => {
if (!canSelectAdditionalItems.value) {
pos.views.additionalItemSelection.value = false;
return;
}
pos.views.additionalItemSelection.value = !pos.views.additionalItemSelection.value;
}
@@ -159,7 +194,9 @@ watch(() => pos.transactionItems.additionalItems.value, (newVal) => {
}, { deep: true });
const onClickAddProduct = async (product: PosProduct) => {
// If the product requires note, open note input.
if (isAdditionalItemRestricted(product)) {
return;
}
pos.transactionItems.addAdditionalItem(product);
// If the view is fullscreen, close it after adding
//if (pos.views.additionalItemSelection.value) {
@@ -171,7 +208,7 @@ const onClickAddProduct = async (product: PosProduct) => {
<template>
<div data-testid="pos-mobile-additional-items">
<!-- Minimal view, when not set as fullscreen view -->
<WhiteBoxCard :toggleable="pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length == 0"
<WhiteBoxCard :toggleable="canSelectAdditionalItems && pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length == 0"
:defaultOpen="pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length > 0"
@toggle="onToggle"
:forceState="(pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length > 0) ? true : (pos.views.additionalItemSelection.value)"
@@ -201,7 +238,7 @@ const onClickAddProduct = async (product: PosProduct) => {
</div>
</template>
<!-- Footer -->
<template #footer>
<template #footer v-if="canSelectAdditionalItems">
<!-- Select other product button -->
<a class="card-footer-item" data-testid="pos-mobile-additional-items-open" @click="onClickAddOtherProduct">
<span class="icon">
@@ -212,7 +249,7 @@ const onClickAddProduct = async (product: PosProduct) => {
</template>
</WhiteBoxCard>
<!-- Fullscreen view, when selecting other products -->
<template v-else>
<template v-else-if="canSelectAdditionalItems">
<div data-testid="pos-mobile-additional-items-selection">
<!-- Categories of products -->
<PosDepartmentStep2MobileVehicleSelection :onAddProduct="onClickAddProduct" :onSearchClick="() => console.warn('AdditionalItem Search Clicked')"/><!-- :asAddons="true" :addons="availableAdditionalItems" @update:addons="availableAdditionalItems = $event"/>-->
@@ -20,6 +20,7 @@ import {
hasAttribute,
uploadAttachment,
restoreStoredPosOrderId,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
import * as POSDepartmentProcess from "@/components/shop/POSDepartmentProcess.vue";
import { errors } from "@/components/request/HandleGlobalError.vue";
@@ -96,17 +97,30 @@ const props = defineProps({
},
});
const updateReference = () => {
if (!!order_id.value && order_id.value > 0) SessionUser.objects.orders.set.reference(order_id.value, reference.value);
const getActiveOrderId = () => {
const parsedOrderId = Number.parseInt(String(order_id.value ?? ""), 10);
return Number.isInteger(parsedOrderId) && parsedOrderId > 0 ? parsedOrderId : null;
};
const updateNotes = () => {
if (!!order_id.value && order_id.value > 0) SessionUser.objects.orders.set.notes(order_id.value, order_notes.value);
const updateReference = async () => {
const activeOrderId = getActiveOrderId();
if (activeOrderId) {
await saveOrderMetadataField("reference", reference.value, activeOrderId);
}
};
const updateSafetySeal = () => {
if (!!order_id.value && order_id.value > 0)
SessionUser.objects.orders.set.safety_seal(order_id.value, order_safety_seal.value);
const updateNotes = async () => {
const activeOrderId = getActiveOrderId();
if (activeOrderId) {
await saveOrderMetadataField("notes", order_notes.value, activeOrderId);
}
};
const updateSafetySeal = async () => {
const activeOrderId = getActiveOrderId();
if (activeOrderId) {
await saveOrderMetadataField("safety_seal", order_safety_seal.value, activeOrderId);
}
};
// Helpers: extracted for clarity and reuse
@@ -196,9 +210,9 @@ const step1 = async () => {
// Reset the vehicle selection
views.vehicleSelection.value = false;
// Merge references and notes
mergeReferences();
mergeNotes();
mergeSafetySeal();
await mergeReferences();
await mergeNotes();
await mergeSafetySeal();
// We only need to check if the customer requires reference, if the field is empty.
if (!isNonEmptyString(reference.value)) {
const isValid = await validateReferenceRequirements();
@@ -214,37 +228,39 @@ const mergeReferences = () => {
if (metadata.getReference() && metadata.getReference().length >= 1) {
// If there is no reference, set it to the registration number of vehicle 1
reference.value = metadata.getReference();
updateReference();
return updateReference();
} else if (reference.value && reference.value.length >= 1) {
// If there is a reference in the field, set it to the metadata
metadata.setReference(reference.value);
updateReference();
return updateReference();
} else if (vehicles.vehicle_1.value?.reference && vehicles.vehicle_1.value?.reference.length >= 1) {
// If there is no reference, set it to the registration number of vehicle 1
reference.value = vehicles.vehicle_1.value?.reference;
metadata.setReference(reference.value);
updateReference();
return updateReference();
}
return Promise.resolve();
};
const mergeNotes = () => {
if (metadata.getNotes() && metadata.getNotes().length >= 1) {
order_notes.value = metadata.getNotes();
updateNotes();
return updateNotes();
} else if (order_notes.value && order_notes.value.length >= 1) {
metadata.setNotes(order_notes.value);
updateNotes();
return updateNotes();
}
return Promise.resolve();
};
const mergeSafetySeal = () => {
const resolvedSafetySeal = getResolvedMobileSafetySeal();
order_safety_seal.value = resolvedSafetySeal;
syncMobileSafetySealState(resolvedSafetySeal);
updateSafetySeal();
return updateSafetySeal();
};
const finalizeStep1 = async () => {
mergeReferences();
mergeNotes();
mergeSafetySeal();
await mergeReferences();
await mergeNotes();
await mergeSafetySeal();
// Upload the attachments if there are any (And the order id has been created)
if (attachments.getBase64().length > 0 && order_id.value > 0) {
const uploadResults = await Promise.all(
@@ -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),
};
}
@@ -5,6 +5,7 @@ import {
order_safety_seal,
reference,
clearStoredPosOrderId,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
import { metadata } from "./PosDepartmentStepMobileFlow.vue";
@@ -52,9 +53,9 @@ export const persistCurrentMobileOrderMetadata = async () => {
order_notes.value = normalizedNotes;
await Promise.all([
SessionUser.objects.orders.set.reference(normalizedOrderId, normalizedReference),
SessionUser.objects.orders.set.notes(normalizedOrderId, normalizedNotes),
SessionUser.objects.orders.set.safety_seal(normalizedOrderId, normalizedSafetySeal),
saveOrderMetadataField("reference", normalizedReference, normalizedOrderId),
saveOrderMetadataField("notes", normalizedNotes, normalizedOrderId),
saveOrderMetadataField("safety_seal", normalizedSafetySeal, normalizedOrderId),
]);
return normalizedOrderId;
@@ -1,6 +1,13 @@
<script setup lang="ts">
import { ref, onBeforeUnmount, onMounted, watch } from "vue";
import { order_id, reg_1, reg_2, reg_3, getOrderDetails } from "@/components/shop/POSDepartmentProcess.vue";
import {
order_id,
reg_1,
reg_2,
reg_3,
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
import PosDepartmentStepMobile1RegistrationNumberInputField from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1RegistrationNumberInputField.vue";
import BarcodeScanner from "@/components/viewport/elements/icons/BarcodeScanner.vue";
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
@@ -103,9 +110,7 @@ const createRegistrationAutosave = (vehicleIndex: number) =>
return value;
}
await SessionUser.objects.orders.set[`reg_${vehicleIndex}`](normalizedOrderId, value);
const refreshedOrder = await getOrderDetails(normalizedOrderId);
return refreshedOrder?.[`reg_${vehicleIndex}`] ?? value;
return saveOrderMetadataField(`reg_${vehicleIndex}`, value, normalizedOrderId);
},
onSaved: async (value) => {
setPersistedRegistrationValue(vehicleIndex, value);
@@ -194,6 +199,11 @@ const flushAllRegistrations = async () => {
await Promise.all([1, 2, 3].map((vehicleIndex) => registrationAutosaves[vehicleIndex].flush()));
};
const waitForAllRegistrationSaves = async () => {
await Promise.all([1, 2, 3].map((vehicleIndex) => registrationAutosaves[vehicleIndex].flushAndWait()));
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(waitForAllRegistrationSaves);
// Function to set a value if it is not null
const createVehicleSelectionFromSearchResult = (value: PosSearchResult) => ({
...value,
@@ -266,11 +276,12 @@ onMounted(() => {
});
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
void flushAllRegistrations();
});
const closeManualInput = async () => {
await flushAllRegistrations();
await waitForAllRegistrationSaves();
emit("close");
};
</script>
@@ -12,6 +12,7 @@ import PosDepartmentStepMobileButtonNextStep from "@/components/displays/departm
import SessionUser from "@/components/session/token/SessionUser.vue";
import { PosOrder } from "@/components/displays/department/pos/steps/mobile/objects/PosOrder.vue";
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
import { todayLocalDateOnly } from "@/services/dateOnly.js";
// Define the close event to emit when the component is closed
const emit = defineEmits(["close"]);
/** Display variables */
@@ -126,7 +127,7 @@ const liveTransactions = ref(null);
const isLoading = ref(true);
const syncListTransactionHistory = async () => {
let dateToday = new Date().toISOString().split("T")[0]; // Get today's date in YYYY-MM-DD format
let dateToday = todayLocalDateOnly(); // Get today's date in YYYY-MM-DD format
isLoading.value = true;
// Fetch the list of orders created today for the current department
SessionUser.objects.orders.get
@@ -22,7 +22,7 @@ const props = defineProps({
import {computed, ref, watch} from 'vue';
import { departments, getDepartments, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {orderBy, orderDirection, setOrder, loadList} from "@/components/pagination/paginatedList.vue";
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
import Swal from "sweetalert2";
import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue";
@@ -38,6 +38,8 @@ import {
setCachedXlvaskUsageAmount
} from "@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js";
const { orderBy, orderDirection, setOrder, loadList } = usePaginatedListInstance();
const redirectDepartmentOrderPage = (orderId, departmentId) => {
// Send the user to the order page (In a new tab)
// `/admin/${departmentId}/modules/pos/orders/${orderId}`
@@ -0,0 +1 @@
export const SELFWASH_PERIOD_ALL_LIMIT = 10000;
@@ -1,6 +1,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { useI18n } from 'vue-i18n';
import { formatLocalDateOnly } from '@/services/dateOnly.js';
type startEndDate = {
start: Date; // The start date of the range
end: Date; // The end date of the range
@@ -27,7 +28,7 @@ const datePresetFunctions = {
month: {
// Get the first day of the month based on the provided date first day at 00:00:01
firstDayOfMonth: (date: Date): Date => {
return new Date(date.getFullYear(), date.getMonth(), 2, 0, 0, 1, 0); // Set to the first day of the month at 00:00:01
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0); // Set to the first day of the month at 00:00:00
},
// Get the last day of the month based on the provided date last day at 23:59:59
lastDayOfMonth: (date: Date): Date => {
@@ -116,7 +117,7 @@ export const datePresets = <datePreset[]>[
*/
const convertToISO = (date: Date): string => {
return date.toISOString().split('T')[0]; // Convert to YYYY-MM-DD format
return formatLocalDateOnly(date); // Convert to YYYY-MM-DD format
};
export const dateFunctions = {
@@ -150,4 +151,4 @@ export default defineComponent({
},
});
</script>
<template></template>
<template></template>
@@ -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">
@@ -47,7 +48,7 @@ if (router.currentRoute.value.params.departmentId) {
<span>{{ t('common.add') }} {{ SessionUser.objects.department_notification_sms.meta.labels.single }}</span>
</button>
</template>
<DepartmentNotificationsPhoneTable :objects="list" />
<DepartmentNotificationsPhoneTable :objects="list" :load-list="loadList" />
</TableLabeledPagination>
</template>
@@ -21,9 +21,32 @@ const props = defineProps({
type: Number, default: 0
}
})
import {computed, ref, watch } from "vue";
import {computed, provide, ref, watch } from "vue";
import { useRouter } from "vue-router";
import {
PaginatedListKey,
usePaginatedList,
} from "@/components/pagination/paginatedList.vue";
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
import PaginationDisplayTemplateDates
from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
import ShowErrorField from "@/components/global/ShowErrorField.vue";
import { useI18n } from 'vue-i18n'
import { BSwitch } from "buefy";
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
import { SELFWASH_PERIOD_ALL_LIMIT } from "@/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n()
const router = useRouter();
const paginatedList = usePaginatedList();
provide(PaginatedListKey, paginatedList);
const {
isLoading,
list,
loadList,
@@ -38,22 +61,8 @@ import {
setOrder,
hideSearchField,
setHideSearchField,
} from "@/components/pagination/paginatedList.vue";
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
} = paginatedList;
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
import PaginationDisplayTemplateDates
from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
import ShowErrorField from "@/components/global/ShowErrorField.vue";
import { useI18n } from 'vue-i18n'
import { BSwitch } from "buefy";
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
const { t } = useI18n()
const router = useRouter();
const SELFWASH_PERIOD_ALL_LIMIT = 10000;
setEndpoint("/modules/xlvask/services/usage/orders", false);
if (props.loadAllAtOnce) {
setMetaItemsPerPage(SELFWASH_PERIOD_ALL_LIMIT, false);
@@ -63,7 +72,7 @@ const parseInitialDate = (value) => {
return new Date();
}
const parsed = new Date(`${value}T00:00:00`);
const parsed = parseLocalDateOnly(value);
return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
};
@@ -129,7 +138,7 @@ const dateFrom = ref(parseInitialDate(props.initialDateFrom));
const dateTo = ref(parseInitialDate(props.initialDateTo));
const parsedDate = (date) => {
return new Date(date);
return parseLocalDateOnly(date);
};
const reloadScheduled = ref(false);
@@ -148,14 +157,14 @@ const actions = {
from: {
select: (date) => {
dateFrom.value = parsedDate(date);
setFilter("StartTime-date_from", date.toISOString().split("T")[0], false);
setFilter("StartTime-date_from", formatLocalDateOnly(date), false);
scheduleReload();
}
},
to: {
select: (date) => {
dateTo.value = parsedDate(date);
setFilter("StartTime-date_to", date.toISOString().split("T")[0], false);
setFilter("StartTime-date_to", formatLocalDateOnly(date), false);
scheduleReload();
}
},
@@ -109,8 +109,8 @@ import OrdersTable from "@/components/displays/department/pos/orders/ordersTable
import {SessionUser} from "@/components/session/token/SessionUser.vue";
import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue";
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
import {now} from "@vueuse/core";
import { useI18n } from 'vue-i18n'
import { formatLocalDateOnly, parseLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n()
const router = useRouter();
@@ -176,8 +176,8 @@ if (props.autoLoad) {
const date_from = ref(null);
const date_to = ref(null);
const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => {
const formattedStartDate = newSelectionStartDate.toISOString().split("T")[0];
const formattedEndDate = newSelectionToDate.toISOString().split("T")[0];
const formattedStartDate = formatLocalDateOnly(newSelectionStartDate);
const formattedEndDate = formatLocalDateOnly(newSelectionToDate);
date_from.value = formattedStartDate;
date_to.value = formattedEndDate;
setFilter("created_at-date_from", formattedStartDate, true);
@@ -361,7 +361,7 @@ const doesEndpointMatch = (matcher) => {
<!-- Shortcuts for date filters -->
<DatePeriodSelector :on-selection-change="onDateRangeSelected"
:visibility="{ showDailySelector: false, showWeeklySelector: false, showMultipleMonthWarning: false, showUpdateButton: false, showMonthSelector: false, showStartDate: false, showEndDate: false, showSelectionValidity: false, showYearSelector: false, showToLabel: false }"
v-bind:selection="{ startDate: date_from ? new Date(date_from) : new Date(now()), endDate: date_to ? new Date(date_to) : new Date(now()) }"/>
v-bind:selection="{ startDate: parseLocalDateOnly(date_from || todayLocalDateOnly()), endDate: parseLocalDateOnly(date_to || todayLocalDateOnly()) }"/>
</div>
</template>
<template #default>
@@ -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
@@ -24,13 +24,16 @@ 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'
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("*");
@@ -52,7 +55,7 @@ watch(() => getFilter("status"), (value) => {
// If the route starts with /user, set the endpoint to /user/bookings
if (router.currentRoute.value.path.startsWith("/user")) {
setEndpoint("/user/bookings", false);
setFilter("date", new Date().toISOString().split("T")[0], false);
setFilter("date", todayLocalDateOnly(), false);
setOrder("created_at", "desc");
}
// If the route starts with /superuser, set the endpoint to /bookings
@@ -81,7 +84,7 @@ loadList();
const showingToday = ref(true);
watch(() => getFilter("date"), (value) => {
showingToday.value = value === new Date().toISOString().split("T")[0];
showingToday.value = value === todayLocalDateOnly();
});
// If the screen is mobile, set the is small variable to true
@@ -147,17 +150,17 @@ 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">
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? new Date().toISOString().split('T')[0] : '*') }" checked="checked" :class="{ 'is-link': showingToday }" />
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? todayLocalDateOnly() : '*') }" checked="checked" :class="{ 'is-link': showingToday }" />
<label for="today"></label>
</div>
</div>
</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)"
@@ -188,4 +191,4 @@ const showNewOrderBookingsPortal = () => {
<style scoped>
</style>
</style>
@@ -8,6 +8,7 @@ import OrderBookingsTable from "@/views/dashboards/userDashboard/bookings/displa
import { departments, getDepartments } from "@/components/pagination/departmentTabs.vue";
import { Colors } from "@/ThemeConfig.vue";
import { useI18n } from "vue-i18n";
import { endOfLocalDate, startOfLocalDate, todayLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n();
/**
@@ -16,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
*/
@@ -52,11 +54,9 @@ const onOnlyTodayFilterChange = (event, autoLoadList = true) => {
setFilter("datetime-date_from", null, false);
setFilter("datetime-date_to", null, false);
} else {
const startOfDay = new Date().setHours(0, 0, 0, 0);
const endOfDay = new Date().setHours(23, 59, 59, 999);
//setFilter('datetime', null, false);
setFilter("datetime-date_from", new Date(startOfDay).toISOString(), false);
setFilter("datetime-date_to", new Date(endOfDay).toISOString(), false);
setFilter("datetime-date_from", startOfLocalDate(val).toISOString(), false);
setFilter("datetime-date_to", endOfLocalDate(val).toISOString(), false);
}
if (autoLoadList) {
loadList();
@@ -95,7 +95,7 @@ onMounted(() => {
departmentFilter.value = value;
} else if (key === "only_today" && value === true) {
setFilterKey = false; // Since the only_today filter is handled separately
onOnlyTodayFilterChange({ target: { value: new Date().toISOString().split("T")[0] } }, false);
onOnlyTodayFilterChange({ target: { value: todayLocalDateOnly() } }, false);
}
if (setFilterKey) {
setFilter(key, value, false);
@@ -170,7 +170,7 @@ onMounted(() => {
<div class="select">
<select :value="onlyTodayFilter" @change="onOnlyTodayFilterChange">
<option value="*">{{ t("common.all") }}</option>
<option :value="new Date().toISOString().split('T')[0]">{{ t("common.yes") }}</option>
<option :value="todayLocalDateOnly()">{{ t("common.yes") }}</option>
</select>
</div>
</div>
@@ -193,7 +193,7 @@ onMounted(() => {
@change="
(event) => {
onOnlyTodayFilterChange({
target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' },
target: { value: event.target.checked ? todayLocalDateOnly() : '*' },
});
}
"
@@ -203,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"
@@ -226,7 +226,7 @@ onMounted(() => {
@change="
(event) => {
onOnlyTodayFilterChange(
{ target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' } },
{ target: { value: event.target.checked ? todayLocalDateOnly() : '*' } },
false
);
onOrderIdFilterChange({ target: { value: event.target.checked ? 'is null' : '*' } });
@@ -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: {
@@ -52,9 +55,13 @@ loadList();
:columnLabels="{ type: SessionUser.objects.vehicles.columns.type.label }"
/>
</div>
<div v-if="$slots.actions" class="vehicles-pagination__add-action">
<label class="label is-small">&nbsp;</label>
<slot name="actions" />
</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,6 +1,7 @@
<script setup lang="ts">
import PaginationDisplayItemColumn from "@/components/displays/pagination/PaginationDisplayItemColumn.vue";
import { ref, computed } from "vue";
import { computed } from "vue";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
const props = defineProps({
label: {
type: String,
@@ -19,7 +20,7 @@ defineExpose({
});
const formattedDate = computed(() => {
return date.value.toISOString().split('T')[0];
return formatLocalDateOnly(date.value);
});
// This component is used to display a date input in a pagination display item column.
@@ -36,8 +37,7 @@ const formattedDate = computed(() => {
class="input"
:value="formattedDate"
@input="(e) => {
const newDate = new Date(e.target.value);
console.log('Selected date:', newDate);
const newDate = parseLocalDateOnly(e.target.value);
emit('update:date', newDate);
}"
/>
@@ -47,4 +47,4 @@ const formattedDate = computed(() => {
<style scoped>
</style>
</style>
@@ -1,5 +1,4 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { SessionUser } from '@/components/session/token/SessionUser.vue';
@@ -10,7 +9,7 @@ import PaginationDisplayTemplateButton
from "@/components/displays/pagination/templates/PaginationDisplayTemplateButton.vue";
import { datePresets , dateFunctions} from '@/components/displays/pagination/PaginationDisplayDates.vue';
const props = defineProps({
defineProps({
startDate: {
type: Date,
required: true,
@@ -46,6 +45,7 @@ function updateStartDate(date: Date) {
// Update the end date model when the date is changed
function updateEndDate(date: Date) {
endDateModel.value = date;
emits('update:endDate', date);
}
</script>
@@ -79,4 +79,4 @@ function updateEndDate(date: Date) {
<style scoped>
</style>
</style>
@@ -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>
@@ -11,7 +11,9 @@ defineProps({
});
const editUser = (user) => {
showEditUserForm(user.id, user.customer_number, user.display_name, user.group_id);
showEditUserForm(user.id, user.customer_number, user.display_name, user.group_id, {
limitedBackofficeManaged: Boolean(user.limited_backoffice_managed),
});
};
</script>
@@ -13,6 +13,7 @@ import { showDownloadWashCertificate } from "@/components/shop/DownloadWashCerti
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import Swal from 'sweetalert2';
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
import { todayLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
const redirectBookingObjectPage = (objectId) => {
// Send the user to the object page
window.location.href = `/user/bookings/${objectId}`;
@@ -237,7 +238,8 @@ window.addEventListener('resize', () => {
* To organize the bookings, we want to show the bookings that are created today first, then yesterday, then all other days
* @type {string}
*/
const currentDate = new Date().toISOString().split("T")[0];
const currentDate = todayLocalDateOnly();
const previousDate = yesterdayLocalDateOnly();
/**
* Sort the bookings by date
@@ -362,7 +364,7 @@ const canUserEditObject = (object) => {
</span>
<span class="has-text-grey">
<!-- Human readable date (Today, Yesterday, etc.) -->
{{ object.date === currentDate ? $t('tables.bookings.today') : object.date === new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split("T")[0] ? $t('tables.bookings.yesterday') : object.date }}
{{ object.date === currentDate ? $t('tables.bookings.today') : object.date === previousDate ? $t('tables.bookings.yesterday') : object.date }}
<!-- Number of bookings on the date -->
({{ $t('tables.bookings.bookings_count', { count: objects.filter((booking) => booking.date === object.date).length }) }})
</span>
@@ -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) => {
@@ -8,9 +8,9 @@ const props = defineProps(['label', 'isText']);
const label = computed(() => props.label || t('admin.pos.buttons.go_back'));
// Go to previous step
const goToPreviousStep = () => {
const goToPreviousStep = async () => {
// Get the department id
previousStep();
await previousStep();
}
const emptyIfText = (string) => props.isText ? '' : string;
@@ -32,4 +32,4 @@ const emptyIfText = (string) => props.isText ? '' : string;
<style scoped>
</style>
</style>
@@ -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
@@ -0,0 +1,241 @@
<script setup>
import { computed, nextTick, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import CustomerSearchSelect from "@/components/search/economic/CustomerSearchSelect.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
const emit = defineEmits(["close", "created"]);
const { t } = useI18n();
const selectedCustomer = ref(null);
const registrationNumber = ref("");
const vehicleType = ref("");
const washSubscription = ref(false);
const reference = ref("");
const vehicleTypeOptions = ref([]);
const isLoadingVehicleTypes = ref(false);
const isSubmitting = ref(false);
const errorMessage = ref("");
const vehicleTypeError = ref("");
const getCustomerNumber = (customer) => {
const parsedValue = Number.parseInt(
String(customer?.customerNumber ?? customer?.customer_number ?? customer?.customer_id ?? customer?.id ?? ""),
10
);
return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null;
};
const selectedCustomerNumber = computed(() => getCustomerNumber(selectedCustomer.value));
const normalizedVehicleType = computed(() => {
if (vehicleType.value === "") {
return null;
}
const parsedValue = Number.parseInt(String(vehicleType.value), 10);
return Number.isInteger(parsedValue) && parsedValue >= 0 ? parsedValue : null;
});
const normalizedRegistrationNumber = computed(() => {
return registrationNumber.value.trim().toUpperCase();
});
const canSubmit = computed(() => {
return (
!isSubmitting.value &&
!isLoadingVehicleTypes.value &&
selectedCustomerNumber.value !== null &&
normalizedRegistrationNumber.value.length > 0 &&
normalizedVehicleType.value !== null
);
});
const parseErrorMessage = (error) => {
return SessionUser.functions.parseErrorMessage(error) || t("vehicles.add_modal.error");
};
const loadVehicleTypes = async () => {
isLoadingVehicleTypes.value = true;
vehicleTypeError.value = "";
try {
vehicleTypeOptions.value = await SessionUser.objects.vehicles.columns.type.options();
} catch (error) {
vehicleTypeError.value = parseErrorMessage(error) || t("vehicles.add_modal.type_load_error");
} finally {
isLoadingVehicleTypes.value = false;
}
};
const closeModal = () => {
if (!isSubmitting.value) {
emit("close");
}
};
const submitVehicle = async () => {
if (!canSubmit.value) {
errorMessage.value = t("vehicles.add_modal.validation_error");
return;
}
isSubmitting.value = true;
errorMessage.value = "";
try {
const response = await SessionUser.objects.vehicles.add(
normalizedVehicleType.value,
normalizedRegistrationNumber.value,
washSubscription.value,
selectedCustomerNumber.value,
reference.value.trim() || null
);
emit("created", response);
} catch (error) {
errorMessage.value = parseErrorMessage(error);
} finally {
isSubmitting.value = false;
}
};
onMounted(async () => {
await loadVehicleTypes();
await nextTick();
document.getElementById("superuser-add-vehicle-customer-search")?.focus();
});
</script>
<template>
<div class="modal is-active" data-testid="superuser-add-vehicle-modal">
<div class="modal-background" @click="closeModal"></div>
<div class="modal-card superuser-add-vehicle-modal">
<header class="modal-card-head">
<p class="modal-card-title">{{ t("vehicles.add_modal.title") }}</p>
<button
class="delete"
type="button"
:aria-label="t('common.close')"
data-testid="superuser-add-vehicle-close"
@click="closeModal"
></button>
</header>
<section class="modal-card-body">
<div v-if="errorMessage" class="notification is-danger is-light" data-testid="superuser-add-vehicle-error">
{{ errorMessage }}
</div>
<CustomerSearchSelect
v-model="selectedCustomer"
input-id="superuser-add-vehicle-customer-search"
test-id-prefix="superuser-add-vehicle-customer"
:disabled="isSubmitting"
:placeholder="t('vehicles.add_modal.customer_placeholder')"
/>
<div class="field">
<label class="label" for="superuser-add-vehicle-registration">{{ t("vehicles.form.license_plate") }}</label>
<div class="control">
<input
id="superuser-add-vehicle-registration"
v-model="registrationNumber"
class="input"
type="text"
autocomplete="off"
:placeholder="t('vehicles.add_modal.registration_placeholder')"
:disabled="isSubmitting"
data-testid="superuser-add-vehicle-registration"
/>
</div>
</div>
<div class="field">
<label class="label" for="superuser-add-vehicle-type">{{ t("vehicles.form.type") }}</label>
<div class="control" :class="{ 'is-loading': isLoadingVehicleTypes }">
<div class="select is-fullwidth">
<select
id="superuser-add-vehicle-type"
v-model="vehicleType"
:disabled="isSubmitting || isLoadingVehicleTypes || vehicleTypeOptions.length === 0"
data-testid="superuser-add-vehicle-type"
>
<option disabled value="">{{ t("vehicles.add_modal.type_placeholder") }}</option>
<option v-for="option in vehicleTypeOptions" :key="option.id" :value="option.id">
{{ option.name }}
</option>
</select>
</div>
</div>
<p v-if="vehicleTypeError" class="help is-danger" data-testid="superuser-add-vehicle-type-error">
{{ vehicleTypeError }}
</p>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="washSubscription"
type="checkbox"
:disabled="isSubmitting"
data-testid="superuser-add-vehicle-wash-subscription"
/>
{{ t("objects.vehicles.columns.wash_subscription") }}
</label>
</div>
<div class="field">
<label class="label" for="superuser-add-vehicle-reference">{{ t("common.reference") }}</label>
<div class="control">
<input
id="superuser-add-vehicle-reference"
v-model="reference"
class="input"
type="text"
autocomplete="off"
:placeholder="t('vehicles.add_modal.reference_placeholder')"
:disabled="isSubmitting"
data-testid="superuser-add-vehicle-reference"
/>
</div>
</div>
</section>
<footer class="modal-card-foot is-justify-content-flex-end">
<button
class="button"
type="button"
:disabled="isSubmitting"
data-testid="superuser-add-vehicle-cancel"
@click="closeModal"
>
{{ t("common.cancel") }}
</button>
<button
class="button is-link"
type="button"
:class="{ 'is-loading': isSubmitting }"
:disabled="!canSubmit"
data-testid="superuser-add-vehicle-submit"
@click="submitVehicle"
>
{{ t("vehicles.add_modal.submit") }}
</button>
</footer>
</div>
</div>
</template>
<style scoped>
.superuser-add-vehicle-modal {
max-width: min(44rem, calc(100vw - 2rem));
width: 100%;
}
.modal-card-body {
overflow: visible;
}
</style>
@@ -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>
@@ -19,17 +19,17 @@ const SENSITIVE_QUERY_KEYS = /authorization|password|passwd|secret|token|api[_-]
const FALLBACK_LABELS = {
"error_report.button": "Report error",
"error_report.title": "Report error",
"error_report.subtitle": "Send the current screen and recent error details to support.",
"error_report.subtitle": "Send recent error details to support. A screenshot is attached when available.",
"error_report.before_error": "What were you doing before the error occurred?",
"error_report.expected": "What did you expect would happen?",
"error_report.actual": "What actually happened?",
"error_report.before_error_placeholder": "Describe the action you were taking, for example opening orders or selecting a customer.",
"error_report.expected_placeholder": "Describe the result you expected to see.",
"error_report.actual_placeholder": "Describe what you saw instead, including any error text.",
"error_report.consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
"error_report.consent": "I accept that recent request errors, Vue errors, browser details, my answers, and an app screenshot when available are collected for troubleshooting.",
"error_report.submit": "Submit report",
"error_report.submitted": "Error report submitted.",
"error_report.capture_failed": "The screen capture failed. Please try again.",
"error_report.capture_failed": "The screen capture failed. The report will be sent without a screenshot.",
"error_report.submit_failed": "The error report could not be submitted.",
"error_report.required": "All fields and data collection acceptance are required.",
};
@@ -277,15 +277,18 @@ const submit = async () => {
isSubmitting.value = true;
try {
let screenshot = "";
let screenshot = null;
try {
screenshot = await captureScreenshot();
} catch {
captureError.value = tr("error_report.capture_failed");
return;
}
const context = buildContext();
context.screenshot_attachment = {
status: screenshot ? "stored" : "capture_failed",
attached: Boolean(screenshot),
};
await submitErrorReport({
before_error: form.before_error.trim(),
expected: form.expected.trim(),
@@ -340,7 +343,7 @@ const submit = async () => {
<div v-if="submitted" class="notification is-success is-light" data-testid="error-report-submitted">
{{ tr("error_report.submitted") }}
</div>
<div v-if="captureError" class="notification is-danger is-light">
<div v-if="captureError" class="notification is-warning is-light" data-testid="error-report-capture-warning">
{{ captureError }}
</div>
<div v-if="submitError" class="notification is-danger is-light">
+4 -3
View File
@@ -4,6 +4,7 @@ import { ref, watch, onMounted } from "vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useRoute } from "vue-router";
import { isAccessibleVisibleNamedDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js";
import { todayLocalDateOnly } from "@/services/dateOnly.js";
// Get the department ID from the URL
const route = useRoute();
const department_id = route.params.departmentId || SessionUser.functions.getDepartmentIdFromUrl() || null;
@@ -73,9 +74,9 @@ const emphasis = ref({
"department:" +
department_id +
",status:pending,date-date_from:" +
new Date().toISOString().split("T")[0] +
todayLocalDateOnly() +
",date-date_to:" +
new Date().toISOString().split("T")[0],
todayLocalDateOnly(),
limit: 1, // Limit to 1 booking, we only need to know if there are any bookings or not
page: 1,
})
@@ -97,7 +98,7 @@ const emphasis = ref({
* Check if the department has a daily report for today
*/
SessionUser.request(SessionUser.objects.department_daily_reports.meta.endpoint + "/get", "GET", {
date: new Date().toISOString().split("T")[0],
date: todayLocalDateOnly(),
id: department_id, // This refers to the department ID
})
.then((response) => {
@@ -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'),
@@ -0,0 +1,288 @@
<script setup>
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { isSearching, searchCustomer, searchCustomerResults } from "@/components/search/economic/customerSearch.vue";
const props = defineProps({
modelValue: {
type: Object,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
inputId: {
type: String,
default: "customer-search-select-input",
},
testIdPrefix: {
type: String,
default: "customer-search-select",
},
placeholder: {
type: String,
default: "",
},
});
const emit = defineEmits(["update:modelValue", "selected", "cleared"]);
const { t } = useI18n();
const searchQuery = ref("");
const showResults = ref(false);
const selectedResultIndex = ref(-1);
const getCustomerNumber = (customer) => {
const parsedValue = Number.parseInt(
String(customer?.customerNumber ?? customer?.customer_number ?? customer?.customer_id ?? customer?.id ?? ""),
10
);
return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null;
};
const getCustomerName = (customer) => {
return String(customer?.name ?? customer?.customer_name ?? customer?.customerName ?? "").trim();
};
const getCustomerCity = (customer) => {
return String(customer?.city ?? customer?.address_city ?? "").trim();
};
const formatCustomer = (customer) => {
if (!customer) {
return "";
}
const name = getCustomerName(customer);
const customerNumber = getCustomerNumber(customer);
return [name, customerNumber ? `#${customerNumber}` : null].filter(Boolean).join(" - ");
};
const selectedCustomerNumber = computed(() => getCustomerNumber(props.modelValue));
const selectedCustomerName = computed(() => getCustomerName(props.modelValue));
const selectedCustomerCity = computed(() => getCustomerCity(props.modelValue));
const hasResults = computed(() => searchCustomerResults.value.length > 0);
const placeholderText = computed(() => props.placeholder || t("vehicles.add_modal.customer_placeholder"));
const resetSearchResults = () => {
searchCustomer(null);
selectedResultIndex.value = -1;
};
const setSelectedCustomer = (customer) => {
emit("update:modelValue", customer);
emit("selected", customer);
searchQuery.value = formatCustomer(customer);
showResults.value = false;
selectedResultIndex.value = -1;
};
const clearSelectedCustomer = async () => {
emit("update:modelValue", null);
emit("cleared");
searchQuery.value = "";
showResults.value = false;
resetSearchResults();
await nextTick();
document.getElementById(props.inputId)?.focus();
};
const handleSearchInput = () => {
if (props.modelValue) {
emit("update:modelValue", null);
}
const query = searchQuery.value.trim();
selectedResultIndex.value = -1;
if (!query) {
showResults.value = false;
resetSearchResults();
return;
}
showResults.value = true;
searchCustomer(query);
};
const handleFocus = () => {
if (searchQuery.value.trim() && hasResults.value) {
showResults.value = true;
}
};
const handleBlur = () => {
window.setTimeout(() => {
showResults.value = false;
selectedResultIndex.value = -1;
}, 150);
};
const handleKeydown = (event) => {
if (!showResults.value || !hasResults.value) {
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
selectedResultIndex.value = Math.min(selectedResultIndex.value + 1, searchCustomerResults.value.length - 1);
} else if (event.key === "ArrowUp") {
event.preventDefault();
selectedResultIndex.value = Math.max(selectedResultIndex.value - 1, 0);
} else if (event.key === "Enter") {
event.preventDefault();
const selectedCustomer = searchCustomerResults.value[selectedResultIndex.value] || searchCustomerResults.value[0];
if (selectedCustomer) {
setSelectedCustomer(selectedCustomer);
}
} else if (event.key === "Escape") {
showResults.value = false;
selectedResultIndex.value = -1;
}
};
watch(
() => props.modelValue,
(customer) => {
if (customer) {
searchQuery.value = formatCustomer(customer);
} else if (!document.activeElement || document.activeElement.id !== props.inputId) {
searchQuery.value = "";
}
},
{ immediate: true }
);
onBeforeUnmount(() => {
resetSearchResults();
});
</script>
<template>
<div class="customer-search-select">
<div class="field">
<label class="label" :for="inputId">{{ t("vehicles.add_modal.customer_label") }}</label>
<div class="control has-icons-left" :class="{ 'is-loading': isSearching }">
<input
:id="inputId"
v-model="searchQuery"
class="input"
type="text"
autocomplete="off"
:placeholder="placeholderText"
:disabled="disabled"
:aria-expanded="showResults && hasResults"
:data-testid="`${testIdPrefix}-input`"
@input="handleSearchInput"
@focus="handleFocus"
@blur="handleBlur"
@keydown="handleKeydown"
/>
<span class="icon is-left">
<i class="fas fa-search"></i>
</span>
</div>
</div>
<div
v-if="showResults && hasResults"
class="dropdown is-active customer-search-select__dropdown"
:data-testid="`${testIdPrefix}-results`"
>
<div class="dropdown-menu customer-search-select__menu" role="listbox">
<div class="dropdown-content">
<button
v-for="(customer, index) in searchCustomerResults"
:key="getCustomerNumber(customer) || index"
type="button"
class="dropdown-item customer-search-select__option"
:class="{ 'is-active': selectedResultIndex === index }"
:data-testid="`${testIdPrefix}-option-${index}`"
@mousedown.prevent="setSelectedCustomer(customer)"
>
<span class="customer-search-select__option-main">{{ getCustomerName(customer) }}</span>
<span class="customer-search-select__option-meta">
#{{ getCustomerNumber(customer) }}
<template v-if="getCustomerCity(customer)"> · {{ getCustomerCity(customer) }}</template>
</span>
</button>
</div>
</div>
</div>
<div v-if="modelValue" class="customer-search-select__selected" :data-testid="`${testIdPrefix}-selected`">
<div>
<p class="has-text-weight-semibold">{{ t("vehicles.add_modal.selected_customer") }}</p>
<p>{{ selectedCustomerName }}</p>
<p class="is-size-7 has-text-grey">
#{{ selectedCustomerNumber }}
<span v-if="selectedCustomerCity"> · {{ selectedCustomerCity }}</span>
</p>
</div>
<button
type="button"
class="button is-small is-light"
:disabled="disabled"
:data-testid="`${testIdPrefix}-clear`"
@click="clearSelectedCustomer"
>
{{ t("common.clear") }}
</button>
</div>
</div>
</template>
<style scoped>
.customer-search-select {
position: relative;
}
.customer-search-select__dropdown,
.customer-search-select__menu {
width: 100%;
}
.customer-search-select__dropdown {
left: 0;
position: absolute;
right: 0;
top: 4.75rem;
z-index: 40;
}
.customer-search-select__option {
align-items: flex-start;
background: transparent;
border: 0;
display: flex;
flex-direction: column;
text-align: left;
white-space: normal;
width: 100%;
}
.customer-search-select__option-main {
font-weight: 600;
}
.customer-search-select__option-meta {
color: #6b7280;
font-size: 0.8rem;
}
.customer-search-select__selected {
align-items: flex-start;
background: #f5f8fc;
border: 1px solid #d8e2ef;
border-radius: 6px;
display: flex;
gap: 1rem;
justify-content: space-between;
margin-bottom: 1rem;
padding: 0.85rem 1rem;
}
</style>
@@ -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();
},
@@ -5,6 +5,7 @@ import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/Ob
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {ref} from "vue";
import i18n from '@/i18n';
import { todayLocalDateOnly } from "@/services/dateOnly.js";
const t = (key) => i18n.global.t(key);
@@ -241,7 +242,7 @@ export const Bookings = {
'/bookings',
'GET',
{
filters: `department:${department},status:pending,date-date_from:${new Date().toISOString().split('T')[0]},date-date_to:${new Date().toISOString().split('T')[0]}`,
filters: `department:${department},status:pending,date-date_from:${todayLocalDateOnly()},date-date_to:${todayLocalDateOnly()}`,
limit: 100,
page: 1
}
@@ -289,4 +290,4 @@ export const Bookings = {
);
}
};
</script>
</script>
@@ -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;
@@ -5,6 +5,7 @@ import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/Ob
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {ref} from "vue";
import i18n from '@/i18n';
import { todayLocalDateOnly } from "@/services/dateOnly.js";
const t = (key) => i18n.global.t(key);
@@ -80,7 +81,7 @@ const t = (key) => i18n.global.t(key);
water_usage_morning: parseInt(water_usage_morning),
water_usage: parseInt(water_usage),
notes: notes ? notes : 'Daily report',
date: date ? date : new Date().toISOString().split('T')[0]
date: date ? date : todayLocalDateOnly()
}
);
},
@@ -163,6 +164,21 @@ const t = (key) => i18n.global.t(key);
}
);
},
setProductTarget: async ({ department_id, product_id, target_percentage }) => {
const normalizedTarget = target_percentage === null || target_percentage === undefined || target_percentage === ""
? null
: Number(target_percentage);
return authenticatedRequest(
"/departments/daily-reports/product-targets",
"PUT",
{
department_id: parseInt(department_id, 10),
product_id: parseInt(product_id, 10),
target_percentage: normalizedTarget,
}
);
},
createComplaint: async ({
department_id,
customer_number = null,
@@ -1,6 +1,7 @@
<script>
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { formatLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
/**
* The DepartmentTimeBookingsEntries object
@@ -230,10 +231,10 @@ export const DepartmentTimeBookingsEntries = {
if (!dateFrom) {
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
dateFrom = lastWeek.toISOString().split('T')[0]; // Format as YYYY-MM-DD
dateFrom = formatLocalDateOnly(lastWeek); // Format as YYYY-MM-DD
}
if (!dateTo) {
dateTo = new Date().toISOString().split('T')[0]; // Format as YYYY-MM-DD
dateTo = todayLocalDateOnly(); // Format as YYYY-MM-DD
}
console.log("Fetching public available times for department:", departmentId, "from", dateFrom, "to", dateTo);
// Make the request to fetch public available times for the department
@@ -279,4 +280,4 @@ export const DepartmentTimeBookingsEntries = {
);
}
};
</script>
</script>
@@ -108,6 +108,14 @@ export const Departments = {
required: false,
},
},
custom_pricing_only: {
label: t("objects.departments.columns.custom_pricing_only"),
type: "boolean",
sortable: true,
creation: {
required: false,
},
},
longitude: {
label: t("objects.departments.columns.longitude"),
type: "number",
@@ -164,6 +172,14 @@ export const Departments = {
archived: async (id, archived) => {
return ObjectsGlobal.set.column(Departments.meta.endpoint, id, "archived", ObjectsGlobal.parse.boolean(archived));
},
custom_pricing_only: async (id, custom_pricing_only) => {
return ObjectsGlobal.set.column(
Departments.meta.endpoint,
id,
"custom_pricing_only",
ObjectsGlobal.parse.boolean(custom_pricing_only)
);
},
longitude: async (id, longitude) => {
return ObjectsGlobal.set.column(Departments.meta.endpoint, id, "longitude", parseFloat(longitude));
},
@@ -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) => {
@@ -620,6 +633,13 @@ export const ObjectsGlobal = {
function getLockedValue(column, defaultValue = '') {
return lockedValues[column] !== undefined ? lockedValues[column] : '';
}
function getCreationDefaultValue(column, defaultValue = '') {
return columns[column].creation.default !== undefined ? columns[column].creation.default : defaultValue;
}
function isBooleanDefault(column, value) {
const defaultValue = getCreationDefaultValue(column, true);
return String(defaultValue) === String(value);
}
// Add the form fields
for (const column in columns) {
if (columns[column].creation.required) {
@@ -647,8 +667,8 @@ export const ObjectsGlobal = {
<!-- Select -->
<div class="select is-fullwidth">
<select id="${column}" class="has-background-light has-text-black">
<option value="true">Ja</option>
<option value="false">Nej</option>
<option value="true" ${isBooleanDefault(column, true) ? 'selected' : ''}>Ja</option>
<option value="false" ${isBooleanDefault(column, false) ? 'selected' : ''}>Nej</option>
</select>
</div>
</div>`;
@@ -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>
@@ -97,6 +97,7 @@ export const Vehicles = {
sortable: true,
creation: {
required: true,
default: false,
}
},
customer_id: {
@@ -117,7 +118,7 @@ export const Vehicles = {
},
};
},
add: async (type, reg, wash_subscription, customer_id, reference = null) => {
add: async (type, reg, wash_subscription = false, customer_id, reference = null) => {
return ObjectsGlobal.add.object(
Vehicles.meta.endpoint,
{
@@ -22,7 +22,12 @@ export const getUsersVehicles = async () => {
}
export const addVehicleUser = async (reg, type, notes) => {
return await authenticatedRequest("/user/vehicles", "POST", {reg, type, notes})
return await authenticatedRequest("/vehicles", "POST", {
reg,
type: parseInt(type),
wash_subscription: false,
...(notes ? { notes } : {}),
})
.then(
(response) => {
vehicles.value.push(response.data.data);
@@ -35,4 +40,4 @@ export const addVehicleUser = async (reg, type, notes) => {
);
}
</script>
</script>
+260 -113
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";
/**
@@ -52,6 +56,42 @@ export const nextStepDelay = ref(0); // 2 seconds delay
export const isCreatingOrder = ref(false);
export const desktopStep1PreflightHandler = ref(null);
let createOrderRequest = null;
const posStepSaveBarriers = new Set();
let latestOrderDetailsRequestId = 0;
let latestOrderMetadataMutationSequence = 0;
let activeOrderMetadataMutationCount = 0;
export const registerPosStepSaveBarrier = (handler) => {
if (typeof handler !== "function") {
return () => {};
}
posStepSaveBarriers.add(handler);
return () => {
posStepSaveBarriers.delete(handler);
};
};
export const unregisterPosStepSaveBarrier = (handler) => {
posStepSaveBarriers.delete(handler);
};
export const flushPosStepSaveBarriers = async () => {
const barriers = Array.from(posStepSaveBarriers);
if (barriers.length === 0) {
return true;
}
const results = await Promise.allSettled(barriers.map((handler) => handler()));
const failedResult = results.find((result) => result.status === "rejected");
if (failedResult) {
parseError(failedResult.reason, "stepError");
return false;
}
return true;
};
export const setDesktopStep1PreflightHandler = (handler = null) => {
desktopStep1PreflightHandler.value = typeof handler === "function" ? handler : null;
@@ -109,10 +149,16 @@ export const nextStep = async (options = { isMobile: false, orderCreation: true
}
// Set the delay to 2 seconds
setNextStepDelay(2);
if (!(await flushPosStepSaveBarriers())) {
nextStepDelay.value = 0;
return;
}
// Clear the errors
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;
@@ -188,8 +234,22 @@ export const nextStep = async (options = { isMobile: false, orderCreation: true
step.value++;
};
export const previousStep = () => {
step.value--;
export const previousStep = async () => {
if (nextStepDelay.value > 0) {
return;
}
if (!(await flushPosStepSaveBarriers())) {
return;
}
step.value = Math.max(1, step.value - 1);
if (order_id.value) {
pushPosRouteState(`id=${order_id.value}&customer_id=${customer_id.value}&step=${step.value}`);
} else {
pushPosRouteState(`step=${step.value}`);
}
};
export const getCurrentStep = () => {
@@ -784,6 +844,129 @@ watch([reg_1, reg_2, reg_3], () => {
reg_3.value = normalizeRegistrationValue(reg_3.value);
});
const applyOrderMetadataFieldToPosState = (field, value) => {
if (field === "reference") {
reference.value = normalizeOrderMetadataString(value);
return reference.value;
}
if (field === "notes") {
order_notes.value = normalizeOrderMetadataString(value);
return order_notes.value;
}
if (field === "po") {
order_po.value = normalizeOrderMetadataString(value);
return order_po.value;
}
if (field === "safety_seal") {
order_safety_seal.value = normalizeOrderMetadataString(value);
return order_safety_seal.value;
}
if (field === "reg_1") {
reg_1.value = normalizeRegistrationValue(value);
return reg_1.value;
}
if (field === "reg_2") {
reg_2.value = normalizeRegistrationValue(value);
return reg_2.value;
}
if (field === "reg_3") {
reg_3.value = normalizeRegistrationValue(value);
return reg_3.value;
}
return normalizeOrderMetadataString(value);
};
export const applyOrderDetailsToPosState = (orderData, includes = {}) => {
if (!orderData || typeof orderData !== "object") {
return null;
}
const includedCustomer = includes?.customer ?? null;
const normalizedOrderCustomerNumber = resolveCustomerNumber(orderData?.customer_id);
const selectedCustomerBeforeHydration = getSelectedCustomerNumber();
department_id.value = orderData.department_id;
applyOrderMetadataFieldToPosState("reference", orderData.reference);
applyOrderMetadataFieldToPosState("notes", orderData.notes);
applyOrderMetadataFieldToPosState("po", orderData.po);
applyOrderMetadataFieldToPosState("safety_seal", orderData.safety_seal);
applyOrderMetadataFieldToPosState("reg_1", orderData.reg_1);
applyOrderMetadataFieldToPosState("reg_2", orderData.reg_2);
applyOrderMetadataFieldToPosState("reg_3", orderData.reg_3);
invoiceCollectionId.value = orderData.invoice_collection_id || null;
completed_at.value = orderData.completed_at || null;
if (includedCustomer && typeof includedCustomer === "object") {
const customerSource =
includedCustomer.economic_customer && typeof includedCustomer.economic_customer === "object"
? includedCustomer.economic_customer
: includedCustomer;
selectCustomer(
normalizeCustomerRecord(customerSource, {
...includedCustomer,
customerNumber: normalizedOrderCustomerNumber ?? resolveCustomerNumber(includedCustomer),
name: includedCustomer.name ?? includedCustomer.customer_name ?? orderData.customer_name ?? customer_name.value,
customer_name:
includedCustomer.customer_name ?? includedCustomer.name ?? orderData.customer_name ?? customer_name.value,
})
);
} else if (normalizedOrderCustomerNumber) {
customer_id.value = normalizedOrderCustomerNumber;
if (
selectedCustomerBeforeHydration !== normalizedOrderCustomerNumber ||
customer_name.value === "" ||
!customer_data.value
) {
void searchAndSelectCustomer(normalizedOrderCustomerNumber, { forceRefresh: true });
}
} else {
clearSelectedCustomerState();
}
return orderData;
};
export const saveOrderMetadataField = async (field, value, targetOrderId = order_id.value) => {
const normalizedOrderId = toPositiveInteger(targetOrderId);
const setter = SessionUser.objects.orders.set?.[field];
if (!normalizedOrderId || typeof setter !== "function") {
throw new Error(`Unable to save order field: ${field}`);
}
const mutationSequence = latestOrderMetadataMutationSequence + 1;
latestOrderMetadataMutationSequence = mutationSequence;
activeOrderMetadataMutationCount += 1;
const isRegistrationField = field === "reg_1" || field === "reg_2" || field === "reg_3";
const requestValue = isRegistrationField
? String(value ?? "").toUpperCase()
: normalizeOrderMetadataString(value);
const normalizedValue = isRegistrationField ? normalizeRegistrationValue(value) : requestValue;
try {
const response = await setter(normalizedOrderId, requestValue);
const updatedOrder = response?.data?.data;
if (updatedOrder && toPositiveInteger(updatedOrder.id) === normalizedOrderId) {
if (mutationSequence === latestOrderMetadataMutationSequence && activeOrderMetadataMutationCount === 1) {
applyOrderDetailsToPosState(updatedOrder, response?.data?.includes ?? {});
}
return applyOrderMetadataFieldToPosState(field, updatedOrder[field] ?? normalizedValue);
}
return applyOrderMetadataFieldToPosState(field, normalizedValue);
} finally {
activeOrderMetadataMutationCount = Math.max(0, activeOrderMetadataMutationCount - 1);
}
};
/** Get the order details by order id */
export const getOrderDetails = async (id = null) => {
const token = localStorage.getItem("token");
@@ -791,6 +974,9 @@ export const getOrderDetails = async (id = null) => {
if (!token || !targetOrderId) {
return null;
}
const requestId = ++latestOrderDetailsRequestId;
const metadataMutationSequenceAtRequest = latestOrderMetadataMutationSequence;
const hadActiveMetadataMutationAtRequest = activeOrderMetadataMutationCount > 0;
return axios
.get(API_URL + "/order?id=" + targetOrderId, {
headers: {
@@ -799,54 +985,21 @@ export const getOrderDetails = async (id = null) => {
})
.then((response) => {
const order_data = response.data.data;
const includedCustomer = response?.data?.includes?.customer ?? null;
const normalizedOrderCustomerNumber = resolveCustomerNumber(order_data?.customer_id);
const selectedCustomerBeforeHydration = getSelectedCustomerNumber();
// Set the order details
department_id.value = order_data.department_id;
reference.value = order_data.reference || "";
order_notes.value = order_data.notes || "";
order_po.value = order_data.po || "";
order_safety_seal.value = normalizeOrderMetadataString(order_data.safety_seal);
reg_1.value = order_data.reg_1 || "";
reg_2.value = order_data.reg_2 || "";
reg_3.value = order_data.reg_3 || "";
invoiceCollectionId.value = order_data.invoice_collection_id || null;
completed_at.value = order_data.completed_at || null;
if (!order_data) {
return null;
}
if (includedCustomer && typeof includedCustomer === "object") {
const customerSource =
includedCustomer.economic_customer && typeof includedCustomer.economic_customer === "object"
? includedCustomer.economic_customer
: includedCustomer;
const normalizedResponseOrderId = toPositiveInteger(order_data.id ?? targetOrderId);
const normalizedCurrentOrderId = toPositiveInteger(order_id.value);
const isStaleForDifferentOrder =
requestId !== latestOrderDetailsRequestId &&
normalizedCurrentOrderId &&
normalizedResponseOrderId &&
normalizedCurrentOrderId !== normalizedResponseOrderId;
const isStaleBeforeMetadataMutation = metadataMutationSequenceAtRequest !== latestOrderMetadataMutationSequence;
selectCustomer(
normalizeCustomerRecord(customerSource, {
...includedCustomer,
customerNumber: normalizedOrderCustomerNumber ?? resolveCustomerNumber(includedCustomer),
name:
includedCustomer.name ??
includedCustomer.customer_name ??
order_data.customer_name ??
customer_name.value,
customer_name:
includedCustomer.customer_name ??
includedCustomer.name ??
order_data.customer_name ??
customer_name.value,
})
);
} else if (normalizedOrderCustomerNumber) {
customer_id.value = normalizedOrderCustomerNumber;
if (
selectedCustomerBeforeHydration !== normalizedOrderCustomerNumber ||
customer_name.value === "" ||
!customer_data.value
) {
void searchAndSelectCustomer(normalizedOrderCustomerNumber, { forceRefresh: true });
}
} else {
clearSelectedCustomerState();
if (!isStaleForDifferentOrder && !isStaleBeforeMetadataMutation && !hadActiveMetadataMutationAtRequest) {
applyOrderDetailsToPosState(order_data, response?.data?.includes ?? {});
}
// Return the order data
return order_data;
@@ -1338,7 +1491,7 @@ const copyLastWashReferenceToEmptyCurrentOrder = async (sourceReference) => {
const normalizedOrderId = toPositiveInteger(order_id.value);
if (normalizedOrderId) {
await SessionUser.objects.orders.set.reference(normalizedOrderId, normalizedSourceReference);
await saveOrderMetadataField("reference", normalizedSourceReference, normalizedOrderId);
}
};
@@ -1466,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 */
@@ -1529,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 = {}) {
@@ -1697,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;
@@ -1801,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),
@@ -2208,32 +2360,27 @@ export const hydrateSelectedOrderBookingForDesktop = async () => {
const bookingReference = getOrderBookingReferenceValue(booking);
if (bookingReference !== "" && bookingReference !== reference.value) {
await SessionUser.objects.orders.set.reference(normalizedOrderId, bookingReference);
reference.value = bookingReference;
reference.value = await saveOrderMetadataField("reference", bookingReference, normalizedOrderId);
}
const bookingReg1 = getOrderBookingReg1Value(booking);
if (bookingReg1 !== "" && bookingReg1 !== reg_1.value) {
await SessionUser.objects.orders.set.reg_1(normalizedOrderId, bookingReg1);
reg_1.value = bookingReg1;
reg_1.value = await saveOrderMetadataField("reg_1", bookingReg1, normalizedOrderId);
}
const bookingReg2 = getOrderBookingReg2Value(booking);
if (bookingReg2 !== reg_2.value) {
await SessionUser.objects.orders.set.reg_2(normalizedOrderId, bookingReg2);
reg_2.value = bookingReg2;
reg_2.value = await saveOrderMetadataField("reg_2", bookingReg2, normalizedOrderId);
}
const bookingNotes = getOrderBookingNotesValue(booking);
if (bookingNotes !== "" && bookingNotes !== order_notes.value) {
await SessionUser.objects.orders.set.notes(normalizedOrderId, bookingNotes);
order_notes.value = bookingNotes;
order_notes.value = await saveOrderMetadataField("notes", bookingNotes, normalizedOrderId);
}
const bookingPo = String(booking?.po ?? "").trim();
if (bookingPo !== "" && isBlankPosMetadataValue(order_po.value)) {
await SessionUser.objects.orders.set.po(normalizedOrderId, bookingPo);
order_po.value = bookingPo;
order_po.value = await saveOrderMetadataField("po", bookingPo, normalizedOrderId);
}
const currentOrderItems = await loadOrderItems();
@@ -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'),
+76 -32
View File
@@ -23,10 +23,12 @@ export const useOrderMetadataAutosave = ({
const draft = ref(normalizeValue(resolveSourceValue(source)));
const lastSavedValue = ref(normalizeValue(resolveSourceValue(source)));
const isSaving = ref(false);
const lastError = ref(null);
const isDirty = computed(() => normalizeValue(draft.value) !== normalizeValue(lastSavedValue.value));
let timerId = null;
let saveAgainAfterCurrentRequest = false;
let activeSavePromise = null;
let nextSequence = 0;
let lastAppliedSequence = 0;
@@ -66,45 +68,57 @@ export const useOrderMetadataAutosave = ({
const sequence = ++nextSequence;
let saveCompleted = false;
isSaving.value = true;
lastError.value = null;
try {
const result = await saveValue(requestedValue);
const savedValue = normalizeValue(result ?? requestedValue);
const run = (async () => {
try {
const result = await saveValue(requestedValue);
const savedValue = normalizeValue(result ?? requestedValue);
if (sequence >= lastAppliedSequence) {
lastAppliedSequence = sequence;
lastSavedValue.value = savedValue;
if (sequence >= lastAppliedSequence) {
lastAppliedSequence = sequence;
lastSavedValue.value = savedValue;
if (normalizeValue(draft.value) === requestedValue) {
draft.value = savedValue;
if (normalizeValue(draft.value) === requestedValue) {
draft.value = savedValue;
}
}
if (typeof onSaved === "function") {
await onSaved(savedValue, requestedValue);
}
saveCompleted = true;
return true;
} catch (error) {
lastError.value = error;
if (typeof onError === "function") {
onError(error);
} else {
console.error(error);
}
return false;
} finally {
isSaving.value = false;
const shouldSaveAgain = saveAgainAfterCurrentRequest;
saveAgainAfterCurrentRequest = false;
if (saveCompleted && (shouldSaveAgain || isDirty.value) && isDirty.value) {
void persist();
}
}
})();
if (typeof onSaved === "function") {
await onSaved(savedValue, requestedValue);
const trackedSavePromise = run.finally(() => {
if (activeSavePromise === trackedSavePromise) {
activeSavePromise = null;
}
saveCompleted = true;
return true;
} catch (error) {
if (typeof onError === "function") {
onError(error);
} else {
console.error(error);
}
return false;
} finally {
isSaving.value = false;
const shouldSaveAgain = saveAgainAfterCurrentRequest;
saveAgainAfterCurrentRequest = false;
if (saveCompleted && (shouldSaveAgain || isDirty.value) && isDirty.value) {
void persist();
}
}
});
activeSavePromise = trackedSavePromise;
return trackedSavePromise;
};
const scheduleSave = () => {
@@ -123,6 +137,34 @@ export const useOrderMetadataAutosave = ({
return persist();
};
const flushAndWait = async () => {
clearPendingSave();
while (isSaving.value && activeSavePromise) {
await activeSavePromise;
}
if (!isDirty.value) {
return false;
}
const didSave = await persist();
if (!didSave && isDirty.value) {
throw lastError.value || new Error("Unable to save order metadata");
}
while (isSaving.value && activeSavePromise) {
await activeSavePromise;
}
if (isDirty.value) {
return flushAndWait();
}
return didSave;
};
onBeforeUnmount(() => {
clearPendingSave();
@@ -139,10 +181,12 @@ export const useOrderMetadataAutosave = ({
return {
draft,
lastSavedValue,
lastError,
isDirty,
isSaving,
scheduleSave,
flush,
flushAndWait,
clearPendingSave,
syncFromSource,
};
+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;
};
+450 -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,132 @@
"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'}",
"tooltip": "@:{'templates.generated.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'templates.generated.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'templates.generated.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'templates.generated.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'templates.generated.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'templates.generated.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"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'}",
"limited_backoffice": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.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'}"
},
"view_own_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -1374,6 +1517,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'}",
@@ -1568,6 +1719,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'}.",
@@ -3116,6 +3268,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'}",
@@ -3331,6 +3484,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'}",
@@ -3354,17 +3514,17 @@
"error_report": {
"button": "Rapporter fejl",
"title": "Rapporter fejl",
"subtitle": "Send det aktuelle skærmbillede og de seneste fejldetaljer til support.",
"subtitle": "Send de seneste fejldetaljer til support. Et skærmbillede vedhæftes, når det er muligt.",
"before_error": "Hvad lavede du, før fejlen opstod?",
"expected": "Hvad forventede du, der ville ske?",
"actual": "Hvad skete der faktisk?",
"before_error_placeholder": "Beskriv handlingen, du var i gang med, for eksempel at åbne ordrer eller vælge en kunde.",
"expected_placeholder": "Beskriv det resultat, du forventede at se.",
"actual_placeholder": "Beskriv, hvad du så i stedet, inklusive eventuel fejltekst.",
"consent": "Jeg accepterer, at det aktuelle app-skærmbillede, de seneste request-fejl, Vue-fejl, browseroplysninger og mine svar indsamles til fejlfinding.",
"consent": "Jeg accepterer, at de seneste request-fejl, Vue-fejl, browseroplysninger, mine svar og et app-skærmbillede, når det er muligt, indsamles til fejlfinding.",
"submit": "Send rapport",
"submitted": "Fejlrapporten er sendt.",
"capture_failed": "Skærmbilledet kunne ikke oprettes. Prøv igen.",
"capture_failed": "Skærmbilledet kunne ikke oprettes. Rapporten sendes uden skærmbillede.",
"submit_failed": "Fejlrapporten kunne ikke sendes.",
"required": "Alle felter og accept af dataindsamling er påkrævet.",
"page_subtitle": "Rapporterede skærme, request-fejl og Vue-fejl",
@@ -3904,6 +4064,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": {
@@ -4161,6 +4330,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'}",
@@ -4722,6 +4892,97 @@
"subtitle": "@.capitalize:{'words.generated.administreret'} @:{'words.replication.services.database'}-, @:{'words.replication.services.redis'}- @:{'words.generated.og'} @:{'words.replication.services.minio'}-@:{'words.generated.replikering'}"
},
"roles": {
"permissions": {
"access_scope": "Adgangsomrade",
"actions": {
"clear_all": "Ryd gruppe",
"reload": "Genindlaes",
"retry": "Prov igen",
"select_all": "Vaelg gruppe"
},
"columns": {
"enabled": "Aktiveret",
"permission": "Tilladelse",
"route": "Rute",
"status": "Status"
},
"department_access_description": "Giver adgang til {name}.",
"empty": "Der er ingen tilladelser tilgaengelige.",
"empty_group": "Denne gruppe har ingen tilladelser.",
"errors": {
"load": "Rolletilladelser kunne ikke indlaeses.",
"update": "En eller flere tilladelser kunne ikke opdateres. Rollens aktuelle tilladelser blev genindlaest."
},
"filtered_summary": "{count} matcher",
"groups": {
"access": {
"description": "Sidetype- og afdelingsadgang.",
"label": "Adgang"
},
"bookings": {
"description": "Tilladelser til bookinger, tidsbookinger og planlaegning.",
"label": "Bookinger"
},
"customers_users": {
"description": "Tilladelser til kunder, brugere, medarbejdere, roller og login.",
"label": "Kunder og brugere"
},
"departments_operations": {
"description": "Afdelingsdrift, baner, relaer, notifikationer, rapporter og mal.",
"label": "Afdelinger og drift"
},
"invoicing_economic": {
"description": "Tilladelser til fakturaer, faktureringsperioder, opkraevning og e-conomic.",
"label": "Fakturering og e-conomic"
},
"orders_pos": {
"description": "Tilladelser til ordrer, POS, betalinger og ordrelinjer.",
"label": "Ordrer og POS"
},
"other": {
"description": "Tilladelser, der ikke matcher en anden gruppe.",
"label": "Andet"
},
"products_pricing": {
"description": "Tilladelser til produkter, priser, rabatter og valuta.",
"label": "Produkter og priser"
},
"selfserve_edge": {
"description": "Tilladelser til selvbetjening, vask, scannere, gateways og edge-udstyr.",
"label": "Selvbetjening og edge"
},
"system_config": {
"description": "System, konfiguration, release, replikering og integrationer.",
"label": "System og konfiguration"
}
},
"no_description": "Der er ingen beskrivelse.",
"no_search_results": "Ingen tilladelser matcher sogningen.",
"page_access": {
"admin": {
"description": "Giver adgang til administrationssider.",
"label": "Admin"
},
"superuser": {
"description": "Giver adgang til superbrugersider.",
"label": "Superbruger"
},
"user": {
"description": "Giver adgang til brugersider.",
"label": "Bruger"
}
},
"search_label": "Sog i tilladelser",
"search_placeholder": "Sog pa label, nogle, beskrivelse, rute eller metode",
"selected_summary": "{selected} af {total} valgt",
"status": {
"disabled": "Deaktiveret",
"enabled": "Aktiveret"
},
"subtitle": "Rolletilladelser",
"title": "Tilladelser for rolle {id}",
"title_with_name": "Tilladelser for {name}"
},
"subtitle": "@.capitalize:{'words.generated.brugerroller'}"
},
"self_wash": {
@@ -5511,6 +5772,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'}",
@@ -5717,6 +6003,20 @@
},
"vehicles": {
"add": "@:{'words.generated.tilføj'} @:{'words.generated.køretøj'}",
"add_modal": {
"customer_label": "Kunde",
"customer_placeholder": "Søg efter kundenavn eller kundenummer",
"error": "Køretøjet kunne ikke tilføjes.",
"no_customer_results": "Ingen kunder fundet",
"reference_placeholder": "Valgfri reference",
"registration_placeholder": "Registreringsnummer",
"selected_customer": "Valgt kunde",
"submit": "Tilføj køretøj",
"title": "Tilføj køretøj",
"type_load_error": "Køretøjstyper kunne ikke indlæses.",
"type_placeholder": "Vælg køretøjstype",
"validation_error": "Vælg kunde, registreringsnummer og køretøjstype."
},
"brand": "@:{'templates.generated.compat.admin.pos.make'}",
"color": "Farve",
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
@@ -5781,6 +6081,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.",
@@ -5795,6 +6098,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'}.",
@@ -5813,7 +6125,13 @@
"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": {
@@ -5843,7 +6161,132 @@
"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",
"tooltip": "Vis rolletilladelser",
"intro": "Roller samler de præcise funktioner, der tildeles en medarbejder, når rollen gemmes.",
"scope": "Alle funktioner er begrænset til de afdelinger, der vælges for medarbejderen.",
"selected": "Valgt",
"close": "Luk",
"unavailable": "Tilladelsesdetaljer er ikke tilgængelige for denne rolle, før rollekataloget er opdateret.",
"groups": {
"account": "Konto",
"orders": "Ordrer",
"bookings": "Bookinger",
"time_bookings": "Tidsbookinger",
"reports": "Rapporter",
"limited_backoffice": "Backoffice",
"unknown": "Øvrigt"
},
"capabilities": {
"sign_in": {
"label": "Log ind",
"description": "Giver medarbejderen adgang til at logge ind i Pleno."
},
"view_own_permissions": {
"label": "Se egne tilladelser",
"description": "Viser medarbejderen, hvilke adgangsrettigheder kontoen har."
},
"view_orders": {
"label": "Se ordrer",
"description": "Kan åbne ordrelister og ordredetaljer for tildelte afdelinger."
},
"create_orders": {
"label": "Opret ordrer",
"description": "Kan oprette nye ordrer for tildelte afdelinger."
},
"edit_orders": {
"label": "Rediger ordrer",
"description": "Kan ændre ordredetaljer for tildelte afdelinger."
},
"delete_orders": {
"label": "Slet ordrer",
"description": "Kan slette ordrer i tildelte afdelinger."
},
"view_order_items": {
"label": "Se ordrelinjer",
"description": "Kan se produkter, ydelser og priser på ordrer."
},
"create_order_items": {
"label": "Opret ordrelinjer",
"description": "Kan tilføje produkter og ydelser til ordrer."
},
"update_order_lines": {
"label": "Rediger ordrelinjer",
"description": "Kan ændre produkter, antal og priser på ordrer."
},
"remove_order_lines": {
"label": "Slet ordrelinjer",
"description": "Kan fjerne produkter og ydelser fra ordrer."
},
"charge_orders": {
"label": "Opkræv ordrer",
"description": "Kan tage betaling eller opkræve en betalingsreservation for en ordre."
},
"view_department_bookings": {
"label": "Se afdelingsbookinger",
"description": "Kan se bookinger for tildelte afdelinger."
},
"view_own_bookings": {
"label": "Se egne bookinger",
"description": "Kan se bookingantal og bookinger knyttet til medarbejderkontoen."
},
"update_bookings": {
"label": "Rediger bookinger",
"description": "Kan ændre tilladte bookingdetaljer."
},
"create_bookings": {
"label": "Opret bookinger",
"description": "Kan oprette nye bookinger for tildelte afdelinger."
},
"mark_bookings_complete": {
"label": "Afslut bookinger",
"description": "Kan markere bookinger som afsluttet."
},
"send_booking_confirmations": {
"label": "Gensend bookingbekræftelser",
"description": "Kan gensende bookingbekræftelser og afslutningsmails."
},
"view_time_booking_entries": {
"label": "Se tidsbookinger",
"description": "Kan gennemse tidsbookingposter for tildelte afdelinger."
},
"create_time_booking_entries": {
"label": "Opret tidsbookinger",
"description": "Kan oprette nye tidsbookingposter."
},
"edit_time_booking_entries": {
"label": "Rediger tidsbookinger",
"description": "Kan redigere tidsbookingposter, når backend-ruten understøtter det."
},
"view_order_statistics": {
"label": "Se ordrestatistik",
"description": "Kan åbne statistikvisninger for ordrer i tildelte afdelinger."
},
"view_booking_statistics": {
"label": "Se bookingstatistik",
"description": "Kan åbne statistikvisninger for bookinger i tildelte afdelinger."
},
"open_limited_backoffice": {
"label": "Åbn begrænset backoffice",
"description": "Kan tilgå det begrænsede backofficeområde."
},
"manage_department_prices": {
"label": "Administrer afdelingspriser",
"description": "Kan redigere eksplicitte priser for tildelte afdelinger."
},
"manage_employee_access": {
"label": "Administrer medarbejderadgang",
"description": "Kan oprette, redigere og deaktivere begrænsede backofficemedarbejdere."
},
"unknown": {
"label": "Ekstra funktion",
"description": "Denne funktion kommer fra rollekataloget, men er endnu ikke beskrevet i grænsefladen."
}
}
}
}
}
+450 -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,132 @@
"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'}",
"tooltip": "@:{'templates.generated.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'templates.generated.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'templates.generated.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'templates.generated.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'templates.generated.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'templates.generated.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"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'}",
"limited_backoffice": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.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'}"
},
"view_own_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -1485,6 +1628,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'}",
@@ -1679,6 +1830,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'}.",
@@ -3227,6 +3379,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'}",
@@ -3442,6 +3595,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'}",
@@ -3465,17 +3625,17 @@
"error_report": {
"button": "Fehler melden",
"title": "Fehler melden",
"subtitle": "Sende den aktuellen Bildschirm und die letzten Fehlerdetails an den Support.",
"subtitle": "Sende die letzten Fehlerdetails an den Support. Ein Screenshot wird angehängt, wenn möglich.",
"before_error": "Was haben Sie getan, bevor der Fehler auftrat?",
"expected": "Was hatten Sie erwartet?",
"actual": "Was ist tatsächlich passiert?",
"before_error_placeholder": "Beschreiben Sie die Aktion, zum Beispiel Bestellungen öffnen oder einen Kunden auswählen.",
"expected_placeholder": "Beschreiben Sie das erwartete Ergebnis.",
"actual_placeholder": "Beschreiben Sie, was stattdessen zu sehen war, inklusive Fehlermeldung.",
"consent": "Ich akzeptiere, dass der aktuelle App-Bildschirm, letzte Request-Fehler, Vue-Fehler, Browserdetails und meine Antworten zur Fehlersuche erfasst werden.",
"consent": "Ich akzeptiere, dass letzte Request-Fehler, Vue-Fehler, Browserdetails, meine Antworten und ein App-Screenshot, wenn möglich, zur Fehlersuche erfasst werden.",
"submit": "Bericht senden",
"submitted": "Fehlerbericht gesendet.",
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Der Bericht wird ohne Screenshot gesendet.",
"submit_failed": "Der Fehlerbericht konnte nicht gesendet werden.",
"required": "Alle Felder und die Zustimmung zur Datenerfassung sind erforderlich.",
"page_subtitle": "Gemeldete Bildschirme, Request-Fehler und Vue-Fehler",
@@ -4015,6 +4175,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": {
@@ -4272,6 +4441,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'}",
@@ -4833,6 +5003,97 @@
"subtitle": "@.capitalize:{'words.generated.managed'} @:{'words.generated.database'} @:{'words.generated.and'} @:{'words.replication.services.redis'} @:{'words.generated.replication'}"
},
"roles": {
"permissions": {
"access_scope": "Zugriffsbereich",
"actions": {
"clear_all": "Gruppe leeren",
"reload": "Neu laden",
"retry": "Erneut versuchen",
"select_all": "Gruppe auswaehlen"
},
"columns": {
"enabled": "Aktiviert",
"permission": "Berechtigung",
"route": "Route",
"status": "Status"
},
"department_access_description": "Erlaubt Zugriff auf {name}.",
"empty": "Es sind keine Berechtigungen verfuegbar.",
"empty_group": "Diese Gruppe hat keine Berechtigungen.",
"errors": {
"load": "Rollenberechtigungen konnten nicht geladen werden.",
"update": "Eine oder mehrere Berechtigungen konnten nicht aktualisiert werden. Die aktuellen Rollenberechtigungen wurden neu geladen."
},
"filtered_summary": "{count} Treffer",
"groups": {
"access": {
"description": "Seitentyp- und Abteilungszugriff.",
"label": "Zugriff"
},
"bookings": {
"description": "Berechtigungen fuer Buchungen, Zeitbuchungen und Planung.",
"label": "Buchungen"
},
"customers_users": {
"description": "Berechtigungen fuer Kunden, Benutzer, Mitarbeiter, Rollen und Anmeldung.",
"label": "Kunden und Benutzer"
},
"departments_operations": {
"description": "Abteilungsbetrieb, Spuren, Relais, Benachrichtigungen, Berichte und Ziele.",
"label": "Abteilungen und Betrieb"
},
"invoicing_economic": {
"description": "Berechtigungen fuer Rechnungen, Abrechnungsperioden, Inkasso und e-conomic.",
"label": "Rechnungen und e-conomic"
},
"orders_pos": {
"description": "Berechtigungen fuer Auftraege, POS, Zahlungen und Auftragszeilen.",
"label": "Auftraege und POS"
},
"other": {
"description": "Berechtigungen, die keiner anderen Gruppe entsprechen.",
"label": "Sonstige"
},
"products_pricing": {
"description": "Berechtigungen fuer Produkte, Preise, Rabatte und Waehrungen.",
"label": "Produkte und Preise"
},
"selfserve_edge": {
"description": "Berechtigungen fuer Self-Service, Waschen, Scanner, Gateways und Edge-Hardware.",
"label": "Self-Service und Edge"
},
"system_config": {
"description": "System, Konfiguration, Release, Replikation und Integrationen.",
"label": "System und Konfiguration"
}
},
"no_description": "Keine Beschreibung verfuegbar.",
"no_search_results": "Keine Berechtigungen entsprechen der Suche.",
"page_access": {
"admin": {
"description": "Erlaubt Zugriff auf Admin-Dashboardseiten.",
"label": "Admin"
},
"superuser": {
"description": "Erlaubt Zugriff auf Superuser-Dashboardseiten.",
"label": "Superuser"
},
"user": {
"description": "Erlaubt Zugriff auf Benutzer-Dashboardseiten.",
"label": "Benutzer"
}
},
"search_label": "Berechtigungen suchen",
"search_placeholder": "Nach Label, Schluessel, Beschreibung, Route oder Methode suchen",
"selected_summary": "{selected} von {total} ausgewaehlt",
"status": {
"disabled": "Deaktiviert",
"enabled": "Aktiviert"
},
"subtitle": "Rollenberechtigungen",
"title": "Berechtigungen fuer Rolle {id}",
"title_with_name": "Berechtigungen fuer {name}"
},
"subtitle": "Benutzerrollen"
},
"self_wash": {
@@ -5622,6 +5883,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'}",
@@ -5828,6 +6114,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'}",
@@ -5892,6 +6192,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'}.",
@@ -5906,6 +6209,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.",
@@ -5924,7 +6236,13 @@
"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": {
@@ -5954,7 +6272,132 @@
"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",
"tooltip": "Rollenberechtigungen anzeigen",
"intro": "Rollen bündeln die genauen Funktionen, die einem Mitarbeiter beim Speichern der Rolle zugewiesen werden.",
"scope": "Jede Funktion ist auf die Abteilungen begrenzt, die für den Mitarbeiter ausgewählt sind.",
"selected": "Ausgewählt",
"close": "Schließen",
"unavailable": "Berechtigungsdetails sind für diese Rolle erst verfügbar, wenn der Rollenkatalog aktualisiert wurde.",
"groups": {
"account": "Konto",
"orders": "Aufträge",
"bookings": "Buchungen",
"time_bookings": "Zeitbuchungen",
"reports": "Berichte",
"limited_backoffice": "Backoffice",
"unknown": "Sonstiges"
},
"capabilities": {
"sign_in": {
"label": "Anmelden",
"description": "Erlaubt dem Mitarbeiter, sich bei Pleno anzumelden."
},
"view_own_permissions": {
"label": "Eigene Berechtigungen anzeigen",
"description": "Zeigt dem Mitarbeiter, welche Zugriffsrechte das Konto hat."
},
"view_orders": {
"label": "Aufträge anzeigen",
"description": "Kann Auftragslisten und Auftragsdetails für zugewiesene Abteilungen öffnen."
},
"create_orders": {
"label": "Aufträge erstellen",
"description": "Kann neue Aufträge für zugewiesene Abteilungen erstellen."
},
"edit_orders": {
"label": "Aufträge bearbeiten",
"description": "Kann Auftragsdetails für zugewiesene Abteilungen ändern."
},
"delete_orders": {
"label": "Aufträge löschen",
"description": "Kann Aufträge in zugewiesenen Abteilungen löschen."
},
"view_order_items": {
"label": "Auftragspositionen anzeigen",
"description": "Kann Produkte, Leistungen und Preise auf Aufträgen sehen."
},
"create_order_items": {
"label": "Auftragspositionen erstellen",
"description": "Kann Produkte und Leistungen zu Aufträgen hinzufügen."
},
"update_order_lines": {
"label": "Auftragspositionen bearbeiten",
"description": "Kann Produkte, Mengen und Preise auf Aufträgen ändern."
},
"remove_order_lines": {
"label": "Auftragspositionen löschen",
"description": "Kann Produkte und Leistungen aus Aufträgen entfernen."
},
"charge_orders": {
"label": "Aufträge abrechnen",
"description": "Kann Zahlungen annehmen oder eine Zahlungsreservierung für einen Auftrag belasten."
},
"view_department_bookings": {
"label": "Abteilungsbuchungen anzeigen",
"description": "Kann Buchungen für zugewiesene Abteilungen sehen."
},
"view_own_bookings": {
"label": "Eigene Buchungen anzeigen",
"description": "Kann Buchungszahlen und Buchungen sehen, die mit dem Mitarbeiterkonto verbunden sind."
},
"update_bookings": {
"label": "Buchungen bearbeiten",
"description": "Kann erlaubte Buchungsdetails ändern."
},
"create_bookings": {
"label": "Buchungen erstellen",
"description": "Kann neue Buchungen für zugewiesene Abteilungen erstellen."
},
"mark_bookings_complete": {
"label": "Buchungen abschließen",
"description": "Kann Buchungen als abgeschlossen markieren."
},
"send_booking_confirmations": {
"label": "Buchungsbestätigungen erneut senden",
"description": "Kann Buchungsbestätigungen und Abschluss-E-Mails erneut senden."
},
"view_time_booking_entries": {
"label": "Zeitbuchungen anzeigen",
"description": "Kann Zeitbuchungseinträge für zugewiesene Abteilungen prüfen."
},
"create_time_booking_entries": {
"label": "Zeitbuchungen erstellen",
"description": "Kann neue Zeitbuchungseinträge hinzufügen."
},
"edit_time_booking_entries": {
"label": "Zeitbuchungen bearbeiten",
"description": "Kann Zeitbuchungseinträge bearbeiten, wenn die Backend-Route dies unterstützt."
},
"view_order_statistics": {
"label": "Auftragsstatistik anzeigen",
"description": "Kann Auftragsstatistiken für zugewiesene Abteilungen öffnen."
},
"view_booking_statistics": {
"label": "Buchungsstatistik anzeigen",
"description": "Kann Buchungsstatistiken für zugewiesene Abteilungen öffnen."
},
"open_limited_backoffice": {
"label": "Begrenztes Backoffice öffnen",
"description": "Kann den begrenzten Backofficebereich öffnen."
},
"manage_department_prices": {
"label": "Abteilungspreise verwalten",
"description": "Kann explizite Preise für zugewiesene Abteilungen bearbeiten."
},
"manage_employee_access": {
"label": "Mitarbeiterzugang verwalten",
"description": "Kann begrenzte Backoffice-Mitarbeiter erstellen, bearbeiten und deaktivieren."
},
"unknown": {
"label": "Zusätzliche Funktion",
"description": "Diese Funktion kommt aus dem Rollenkatalog, ist aber in der Oberfläche noch nicht beschrieben."
}
}
}
}
}
+450 -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,132 @@
"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'}",
"tooltip": "@:{'templates.generated.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'templates.generated.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'templates.generated.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'templates.generated.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'templates.generated.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'templates.generated.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"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'}",
"limited_backoffice": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.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'}"
},
"view_own_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -1206,6 +1349,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'}",
@@ -1400,6 +1551,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'}.",
@@ -2948,6 +3100,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'}",
@@ -3163,6 +3316,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'}",
@@ -3186,17 +3346,17 @@
"error_report": {
"button": "Report error",
"title": "Report error",
"subtitle": "Send the current screen and recent error details to support.",
"subtitle": "Send recent error details to support. A screenshot is attached when available.",
"before_error": "What were you doing before the error occurred?",
"expected": "What did you expect would happen?",
"actual": "What actually happened?",
"before_error_placeholder": "Describe the action you were taking, for example opening orders or selecting a customer.",
"expected_placeholder": "Describe the result you expected to see.",
"actual_placeholder": "Describe what you saw instead, including any error text.",
"consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
"consent": "I accept that recent request errors, Vue errors, browser details, my answers, and an app screenshot when available are collected for troubleshooting.",
"submit": "Submit report",
"submitted": "Error report submitted.",
"capture_failed": "The screen capture failed. Please try again.",
"capture_failed": "The screen capture failed. The report will be sent without a screenshot.",
"submit_failed": "The error report could not be submitted.",
"required": "All fields and data collection acceptance are required.",
"page_subtitle": "Reported screens, request failures, and Vue errors",
@@ -3736,6 +3896,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": {
@@ -3993,6 +4162,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'}",
@@ -4554,6 +4724,97 @@
"subtitle": "@.capitalize:{'words.generated.managed'} @:{'words.replication.services.database'}, @:{'words.replication.services.redis'}, @:{'words.generated.and'} @:{'words.replication.services.minio'} @:{'words.generated.replication'}"
},
"roles": {
"permissions": {
"access_scope": "Access scope",
"actions": {
"clear_all": "Clear group",
"reload": "Reload",
"retry": "Try again",
"select_all": "Select group"
},
"columns": {
"enabled": "Enabled",
"permission": "Permission",
"route": "Route",
"status": "Status"
},
"department_access_description": "Allows access to {name}.",
"empty": "No permissions are available.",
"empty_group": "This group has no permissions.",
"errors": {
"load": "Could not load role permissions.",
"update": "Could not update one or more permissions. The current role permissions were reloaded."
},
"filtered_summary": "{count} matching",
"groups": {
"access": {
"description": "Page type and department access permissions.",
"label": "Access"
},
"bookings": {
"description": "Booking, time booking, and planning permissions.",
"label": "Bookings"
},
"customers_users": {
"description": "Customer, user, employee, role, and authentication permissions.",
"label": "Customers and users"
},
"departments_operations": {
"description": "Department operations, lanes, relays, notifications, reports, and goals.",
"label": "Departments and operations"
},
"invoicing_economic": {
"description": "Invoice, billing period, collection, and e-conomic permissions.",
"label": "Invoicing and e-conomic"
},
"orders_pos": {
"description": "Order, POS, payment, and order line permissions.",
"label": "Orders and POS"
},
"other": {
"description": "Permissions that do not match another group.",
"label": "Other"
},
"products_pricing": {
"description": "Product, price, discount, and currency permissions.",
"label": "Products and pricing"
},
"selfserve_edge": {
"description": "Self-serve, wash, scanner, gateway, and edge hardware permissions.",
"label": "Self-serve and edge"
},
"system_config": {
"description": "System, configuration, release, replication, and integration permissions.",
"label": "System and configuration"
}
},
"no_description": "No description is available.",
"no_search_results": "No permissions match the current search.",
"page_access": {
"admin": {
"description": "Allows access to admin dashboard pages.",
"label": "Admin"
},
"superuser": {
"description": "Allows access to superuser dashboard pages.",
"label": "Superuser"
},
"user": {
"description": "Allows access to user dashboard pages.",
"label": "User"
}
},
"search_label": "Search permissions",
"search_placeholder": "Search by label, key, description, route, or method",
"selected_summary": "{selected} of {total} selected",
"status": {
"disabled": "Disabled",
"enabled": "Enabled"
},
"subtitle": "Role permissions",
"title": "Role {id} permissions",
"title_with_name": "{name} permissions"
},
"subtitle": "@.capitalize:{'words.generated.user'} @:{'words.generated.roles'}"
},
"self_wash": {
@@ -5343,6 +5604,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'}",
@@ -5549,6 +5835,20 @@
},
"vehicles": {
"add": "@.capitalize:{'words.generated.add'} @:{'words.generated.vehicle'}",
"add_modal": {
"customer_label": "Customer",
"customer_placeholder": "Search by customer name or number",
"error": "Unable to add vehicle.",
"no_customer_results": "No customers found",
"reference_placeholder": "Optional reference",
"registration_placeholder": "Registration number",
"selected_customer": "Selected customer",
"submit": "Add vehicle",
"title": "Add vehicle",
"type_load_error": "Unable to load vehicle types.",
"type_placeholder": "Select vehicle type",
"validation_error": "Select a customer, registration number, and vehicle type."
},
"brand": "Brand",
"color": "Color",
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
@@ -5613,6 +5913,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'}.",
@@ -5627,6 +5930,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'}.",
@@ -5645,7 +5957,13 @@
"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": {
@@ -5675,7 +5993,132 @@
"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",
"tooltip": "View role permissions",
"intro": "Roles combine the exact capabilities assigned to an employee when the role is saved.",
"scope": "Every capability is limited to the departments selected for the employee.",
"selected": "Selected",
"close": "Close",
"unavailable": "Permission details are unavailable for this role until the role catalog has been updated.",
"groups": {
"account": "Account",
"orders": "Orders",
"bookings": "Bookings",
"time_bookings": "Time bookings",
"reports": "Reports",
"limited_backoffice": "Backoffice",
"unknown": "Other"
},
"capabilities": {
"sign_in": {
"label": "Sign in",
"description": "Allows the employee to sign in to Pleno."
},
"view_own_permissions": {
"label": "View own permissions",
"description": "Shows the employee which access rights their account has."
},
"view_orders": {
"label": "View orders",
"description": "Can open order lists and order details for assigned departments."
},
"create_orders": {
"label": "Create orders",
"description": "Can create new orders for assigned departments."
},
"edit_orders": {
"label": "Edit orders",
"description": "Can update order details for assigned departments."
},
"delete_orders": {
"label": "Delete orders",
"description": "Can delete orders in assigned departments."
},
"view_order_items": {
"label": "View order lines",
"description": "Can see products, services, and prices on orders."
},
"create_order_items": {
"label": "Create order lines",
"description": "Can add products and services to orders."
},
"update_order_lines": {
"label": "Edit order lines",
"description": "Can change products, quantities, and prices on orders."
},
"remove_order_lines": {
"label": "Delete order lines",
"description": "Can remove products and services from orders."
},
"charge_orders": {
"label": "Charge orders",
"description": "Can take payment or charge a payment intent for an order."
},
"view_department_bookings": {
"label": "View department bookings",
"description": "Can see bookings for assigned departments."
},
"view_own_bookings": {
"label": "View own bookings",
"description": "Can see booking counts and bookings tied to the employee account."
},
"update_bookings": {
"label": "Edit bookings",
"description": "Can change permitted booking details."
},
"create_bookings": {
"label": "Create bookings",
"description": "Can create new bookings for assigned departments."
},
"mark_bookings_complete": {
"label": "Complete bookings",
"description": "Can mark bookings as completed."
},
"send_booking_confirmations": {
"label": "Resend booking confirmations",
"description": "Can resend booking confirmation and completion emails."
},
"view_time_booking_entries": {
"label": "View time booking entries",
"description": "Can inspect time-booking entries for assigned departments."
},
"create_time_booking_entries": {
"label": "Create time booking entries",
"description": "Can add new time-booking entries."
},
"edit_time_booking_entries": {
"label": "Edit time booking entries",
"description": "Can edit time-booking entries when the backend route supports it."
},
"view_order_statistics": {
"label": "View order statistics",
"description": "Can open order statistics views for assigned departments."
},
"view_booking_statistics": {
"label": "View booking statistics",
"description": "Can open booking statistics views for assigned departments."
},
"open_limited_backoffice": {
"label": "Open limited backoffice",
"description": "Can access the limited backoffice area."
},
"manage_department_prices": {
"label": "Manage department prices",
"description": "Can edit explicit prices for assigned departments."
},
"manage_employee_access": {
"label": "Manage employee access",
"description": "Can create, edit, and deactivate limited backoffice employees."
},
"unknown": {
"label": "Additional capability",
"description": "This capability is provided by the role catalog but is not yet described in the interface."
}
}
}
}
}
+233
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'}",
@@ -4255,6 +4282,97 @@
"title": "@:replication.labels.replication"
},
"roles": {
"permissions": {
"access_scope": "@:{'templates.generated.compat.roles.permissions.access_scope'}",
"actions": {
"clear_all": "@:{'templates.generated.compat.roles.permissions.actions.clear_all'}",
"reload": "@:{'templates.generated.compat.roles.permissions.actions.reload'}",
"retry": "@:{'templates.generated.compat.roles.permissions.actions.retry'}",
"select_all": "@:{'templates.generated.compat.roles.permissions.actions.select_all'}"
},
"columns": {
"enabled": "@:{'templates.generated.compat.roles.permissions.columns.enabled'}",
"permission": "@:{'templates.generated.compat.roles.permissions.columns.permission'}",
"route": "@:{'templates.generated.compat.roles.permissions.columns.route'}",
"status": "@:{'templates.generated.compat.roles.permissions.columns.status'}"
},
"department_access_description": "@:{'templates.generated.compat.roles.permissions.department_access_description'}",
"empty": "@:{'templates.generated.compat.roles.permissions.empty'}",
"empty_group": "@:{'templates.generated.compat.roles.permissions.empty_group'}",
"errors": {
"load": "@:{'templates.generated.compat.roles.permissions.errors.load'}",
"update": "@:{'templates.generated.compat.roles.permissions.errors.update'}"
},
"filtered_summary": "@:{'templates.generated.compat.roles.permissions.filtered_summary'}",
"groups": {
"access": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.access.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.access.label'}"
},
"bookings": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.bookings.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.bookings.label'}"
},
"customers_users": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.customers_users.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.customers_users.label'}"
},
"departments_operations": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.departments_operations.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.departments_operations.label'}"
},
"invoicing_economic": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.invoicing_economic.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.invoicing_economic.label'}"
},
"orders_pos": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.orders_pos.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.orders_pos.label'}"
},
"other": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.other.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.other.label'}"
},
"products_pricing": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.products_pricing.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.products_pricing.label'}"
},
"selfserve_edge": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.selfserve_edge.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.selfserve_edge.label'}"
},
"system_config": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.system_config.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.system_config.label'}"
}
},
"no_description": "@:{'templates.generated.compat.roles.permissions.no_description'}",
"no_search_results": "@:{'templates.generated.compat.roles.permissions.no_search_results'}",
"page_access": {
"admin": {
"description": "@:{'templates.generated.compat.roles.permissions.page_access.admin.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.page_access.admin.label'}"
},
"superuser": {
"description": "@:{'templates.generated.compat.roles.permissions.page_access.superuser.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.page_access.superuser.label'}"
},
"user": {
"description": "@:{'templates.generated.compat.roles.permissions.page_access.user.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.page_access.user.label'}"
}
},
"search_label": "@:{'templates.generated.compat.roles.permissions.search_label'}",
"search_placeholder": "@:{'templates.generated.compat.roles.permissions.search_placeholder'}",
"selected_summary": "@:{'templates.generated.compat.roles.permissions.selected_summary'}",
"status": {
"disabled": "@:{'templates.generated.compat.roles.permissions.status.disabled'}",
"enabled": "@:{'templates.generated.compat.roles.permissions.status.enabled'}"
},
"subtitle": "@:{'templates.generated.compat.roles.permissions.subtitle'}",
"title": "@:{'templates.generated.compat.roles.permissions.title'}",
"title_with_name": "@:{'templates.generated.compat.roles.permissions.title_with_name'}"
},
"subtitle": "@:{'templates.generated.compat.roles.subtitle'}",
"title": "@:{'templates.generated.compat.common.roles'}"
},
@@ -4468,6 +4586,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'}",
@@ -5446,6 +5640,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'}",
@@ -5743,6 +5962,20 @@
"vehicles": {
"actions": "@:common.actions",
"add": "@:{'templates.generated.compat.vehicles.add'}",
"add_modal": {
"customer_label": "@:{'templates.generated.compat.vehicles.add_modal.customer_label'}",
"customer_placeholder": "@:{'templates.generated.compat.vehicles.add_modal.customer_placeholder'}",
"error": "@:{'templates.generated.compat.vehicles.add_modal.error'}",
"no_customer_results": "@:{'templates.generated.compat.vehicles.add_modal.no_customer_results'}",
"reference_placeholder": "@:{'templates.generated.compat.vehicles.add_modal.reference_placeholder'}",
"registration_placeholder": "@:{'templates.generated.compat.vehicles.add_modal.registration_placeholder'}",
"selected_customer": "@:{'templates.generated.compat.vehicles.add_modal.selected_customer'}",
"submit": "@:{'templates.generated.compat.vehicles.add_modal.submit'}",
"title": "@:{'templates.generated.compat.vehicles.add_modal.title'}",
"type_load_error": "@:{'templates.generated.compat.vehicles.add_modal.type_load_error'}",
"type_placeholder": "@:{'templates.generated.compat.vehicles.add_modal.type_placeholder'}",
"validation_error": "@:{'templates.generated.compat.vehicles.add_modal.validation_error'}"
},
"brand": "@:{'templates.generated.compat.vehicles.brand'}",
"color": "@:{'templates.generated.compat.vehicles.color'}",
"created_at": "@:{'templates.generated.compat.global.generated'}",
+450 -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,132 @@
"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'}",
"tooltip": "@:{'templates.generated.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'templates.generated.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'templates.generated.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'templates.generated.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'templates.generated.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'templates.generated.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"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'}",
"limited_backoffice": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.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'}"
},
"view_own_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -1488,6 +1631,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'}",
@@ -1682,6 +1833,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'}.",
@@ -3230,6 +3382,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'}",
@@ -3445,6 +3598,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'}",
@@ -3468,17 +3628,17 @@
"error_report": {
"button": "Rapporter feil",
"title": "Rapporter feil",
"subtitle": "Send gjeldende skjermbilde og de siste feildetaljene til support.",
"subtitle": "Send de siste feildetaljene til support. Et skjermbilde legges ved når det er mulig.",
"before_error": "Hva gjorde du før feilen oppstod?",
"expected": "Hva forventet du at skulle skje?",
"actual": "Hva skjedde faktisk?",
"before_error_placeholder": "Beskriv handlingen du utførte, for eksempel å åpne ordre eller velge en kunde.",
"expected_placeholder": "Beskriv resultatet du forventet å se.",
"actual_placeholder": "Beskriv hva du så i stedet, inkludert eventuell feiltekst.",
"consent": "Jeg godtar at gjeldende app-skjermbilde, siste request-feil, Vue-feil, nettleserdetaljer og svarene mine samles inn for feilsøking.",
"consent": "Jeg godtar at siste request-feil, Vue-feil, nettleserdetaljer, svarene mine og et app-skjermbilde når det er mulig, samles inn for feilsøking.",
"submit": "Send rapport",
"submitted": "Feilrapport sendt.",
"capture_failed": "Skjermbildet kunne ikke tas. Prøv igjen.",
"capture_failed": "Skjermbildet kunne ikke tas. Rapporten sendes uten skjermbilde.",
"submit_failed": "Feilrapporten kunne ikke sendes.",
"required": "Alle felter og godkjenning av datainnsamling er påkrevd.",
"page_subtitle": "Rapporterte skjermer, request-feil og Vue-feil",
@@ -4018,6 +4178,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": {
@@ -4275,6 +4444,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'}",
@@ -4836,6 +5006,97 @@
"subtitle": "@.capitalize:{'words.generated.managed'} @:{'words.replication.services.database'} @:{'words.generated.and'} @:{'words.replication.services.redis'} @:{'words.generated.replication'}"
},
"roles": {
"permissions": {
"access_scope": "Tilgangsomrade",
"actions": {
"clear_all": "Tomm gruppe",
"reload": "Last inn pa nytt",
"retry": "Prov igjen",
"select_all": "Velg gruppe"
},
"columns": {
"enabled": "Aktivert",
"permission": "Tillatelse",
"route": "Rute",
"status": "Status"
},
"department_access_description": "Gir tilgang til {name}.",
"empty": "Ingen tillatelser er tilgjengelige.",
"empty_group": "Denne gruppen har ingen tillatelser.",
"errors": {
"load": "Kunne ikke laste rolletillatelser.",
"update": "En eller flere tillatelser kunne ikke oppdateres. De gjeldende rolletillatelsene ble lastet inn pa nytt."
},
"filtered_summary": "{count} treff",
"groups": {
"access": {
"description": "Sidetype- og avdelingstilgang.",
"label": "Tilgang"
},
"bookings": {
"description": "Tillatelser for bookinger, tidsbookinger og planlegging.",
"label": "Bookinger"
},
"customers_users": {
"description": "Tillatelser for kunder, brukere, ansatte, roller og innlogging.",
"label": "Kunder og brukere"
},
"departments_operations": {
"description": "Avdelingsdrift, baner, releer, varsler, rapporter og mal.",
"label": "Avdelinger og drift"
},
"invoicing_economic": {
"description": "Tillatelser for fakturaer, faktureringsperioder, innkreving og e-conomic.",
"label": "Fakturering og e-conomic"
},
"orders_pos": {
"description": "Tillatelser for ordrer, POS, betalinger og ordrelinjer.",
"label": "Ordrer og POS"
},
"other": {
"description": "Tillatelser som ikke matcher en annen gruppe.",
"label": "Annet"
},
"products_pricing": {
"description": "Tillatelser for produkter, priser, rabatter og valuta.",
"label": "Produkter og priser"
},
"selfserve_edge": {
"description": "Tillatelser for selvbetjening, vask, skannere, gatewayer og edge-utstyr.",
"label": "Selvbetjening og edge"
},
"system_config": {
"description": "System, konfigurasjon, release, replikering og integrasjoner.",
"label": "System og konfigurasjon"
}
},
"no_description": "Ingen beskrivelse er tilgjengelig.",
"no_search_results": "Ingen tillatelser matcher soket.",
"page_access": {
"admin": {
"description": "Gir tilgang til administrasjonssider.",
"label": "Admin"
},
"superuser": {
"description": "Gir tilgang til superbrukersider.",
"label": "Superbruker"
},
"user": {
"description": "Gir tilgang til brukersider.",
"label": "Bruker"
}
},
"search_label": "Sok i tillatelser",
"search_placeholder": "Sok pa label, nokkel, beskrivelse, rute eller metode",
"selected_summary": "{selected} av {total} valgt",
"status": {
"disabled": "Deaktivert",
"enabled": "Aktivert"
},
"subtitle": "Rolletillatelser",
"title": "Tillatelser for rolle {id}",
"title_with_name": "Tillatelser for {name}"
},
"subtitle": "@.capitalize:{'words.generated.user'} roles"
},
"self_wash": {
@@ -5625,6 +5886,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'}",
@@ -5831,6 +6117,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'}",
@@ -5895,6 +6195,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.",
@@ -5909,6 +6212,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'}.",
@@ -5927,7 +6239,13 @@
"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": {
@@ -5957,7 +6275,132 @@
"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",
"tooltip": "Vis rolletillatelser",
"intro": "Roller samler de nøyaktige funksjonene som tildeles en ansatt når rollen lagres.",
"scope": "Alle funksjoner er begrenset til avdelingene som velges for den ansatte.",
"selected": "Valgt",
"close": "Lukk",
"unavailable": "Tillatelsesdetaljer er ikke tilgjengelige for denne rollen før rollekatalogen er oppdatert.",
"groups": {
"account": "Konto",
"orders": "Ordrer",
"bookings": "Bookinger",
"time_bookings": "Tidsbookinger",
"reports": "Rapporter",
"limited_backoffice": "Backoffice",
"unknown": "Annet"
},
"capabilities": {
"sign_in": {
"label": "Logg inn",
"description": "Lar den ansatte logge inn i Pleno."
},
"view_own_permissions": {
"label": "Se egne tillatelser",
"description": "Viser den ansatte hvilke tilgangsrettigheter kontoen har."
},
"view_orders": {
"label": "Se ordrer",
"description": "Kan åpne ordrelister og ordredetaljer for tildelte avdelinger."
},
"create_orders": {
"label": "Opprett ordrer",
"description": "Kan opprette nye ordrer for tildelte avdelinger."
},
"edit_orders": {
"label": "Rediger ordrer",
"description": "Kan endre ordredetaljer for tildelte avdelinger."
},
"delete_orders": {
"label": "Slett ordrer",
"description": "Kan slette ordrer i tildelte avdelinger."
},
"view_order_items": {
"label": "Se ordrelinjer",
"description": "Kan se produkter, tjenester og priser på ordrer."
},
"create_order_items": {
"label": "Opprett ordrelinjer",
"description": "Kan legge produkter og tjenester til ordrer."
},
"update_order_lines": {
"label": "Rediger ordrelinjer",
"description": "Kan endre produkter, antall og priser på ordrer."
},
"remove_order_lines": {
"label": "Slett ordrelinjer",
"description": "Kan fjerne produkter og tjenester fra ordrer."
},
"charge_orders": {
"label": "Belast ordrer",
"description": "Kan ta betaling eller belaste en betalingsreservasjon for en ordre."
},
"view_department_bookings": {
"label": "Se avdelingsbookinger",
"description": "Kan se bookinger for tildelte avdelinger."
},
"view_own_bookings": {
"label": "Se egne bookinger",
"description": "Kan se bookingantall og bookinger knyttet til ansattkontoen."
},
"update_bookings": {
"label": "Rediger bookinger",
"description": "Kan endre tillatte bookingdetaljer."
},
"create_bookings": {
"label": "Opprett bookinger",
"description": "Kan opprette nye bookinger for tildelte avdelinger."
},
"mark_bookings_complete": {
"label": "Fullfør bookinger",
"description": "Kan markere bookinger som fullført."
},
"send_booking_confirmations": {
"label": "Send bookingbekreftelser på nytt",
"description": "Kan sende bookingbekreftelser og fullførings-e-poster på nytt."
},
"view_time_booking_entries": {
"label": "Se tidsbookinger",
"description": "Kan gå gjennom tidsbookingoppføringer for tildelte avdelinger."
},
"create_time_booking_entries": {
"label": "Opprett tidsbookinger",
"description": "Kan legge til nye tidsbookingoppføringer."
},
"edit_time_booking_entries": {
"label": "Rediger tidsbookinger",
"description": "Kan redigere tidsbookingoppføringer når backend-ruten støtter det."
},
"view_order_statistics": {
"label": "Se ordrestatistikk",
"description": "Kan åpne ordrestatistikk for tildelte avdelinger."
},
"view_booking_statistics": {
"label": "Se bookingstatistikk",
"description": "Kan åpne bookingstatistikk for tildelte avdelinger."
},
"open_limited_backoffice": {
"label": "Åpne begrenset backoffice",
"description": "Kan åpne det begrensede backofficeområdet."
},
"manage_department_prices": {
"label": "Administrer avdelingspriser",
"description": "Kan redigere eksplisitte priser for tildelte avdelinger."
},
"manage_employee_access": {
"label": "Administrer ansattilgang",
"description": "Kan opprette, redigere og deaktivere begrensede backofficeansatte."
},
"unknown": {
"label": "Ekstra funksjon",
"description": "Denne funksjonen kommer fra rollekatalogen, men er ennå ikke beskrevet i grensesnittet."
}
}
}
}
}
+450 -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,132 @@
"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'}",
"tooltip": "@:{'templates.generated.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'templates.generated.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'templates.generated.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'templates.generated.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'templates.generated.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'templates.generated.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"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'}",
"limited_backoffice": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.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'}"
},
"view_own_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -1538,6 +1681,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'}",
@@ -1732,6 +1883,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'}.",
@@ -3280,6 +3432,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'}",
@@ -3495,6 +3648,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'}",
@@ -3518,17 +3678,17 @@
"error_report": {
"button": "Rapportera fel",
"title": "Rapportera fel",
"subtitle": "Skicka aktuell skärm och de senaste feldetaljerna till support.",
"subtitle": "Skicka de senaste feldetaljerna till support. En skärmbild bifogas när det är möjligt.",
"before_error": "Vad gjorde du innan felet uppstod?",
"expected": "Vad förväntade du dig skulle hända?",
"actual": "Vad hände faktiskt?",
"before_error_placeholder": "Beskriv åtgärden du gjorde, till exempel att öppna ordrar eller välja en kund.",
"expected_placeholder": "Beskriv resultatet du förväntade dig att se.",
"actual_placeholder": "Beskriv vad du såg i stället, inklusive eventuell feltext.",
"consent": "Jag accepterar att aktuell app-skärm, senaste request-fel, Vue-fel, webbläsardetaljer och mina svar samlas in för felsökning.",
"consent": "Jag accepterar att senaste request-fel, Vue-fel, webbläsardetaljer, mina svar och en app-skärmbild när det är möjligt samlas in för felsökning.",
"submit": "Skicka rapport",
"submitted": "Felrapport skickad.",
"capture_failed": "Skärmbilden kunde inte tas. Försök igen.",
"capture_failed": "Skärmbilden kunde inte tas. Rapporten skickas utan skärmbild.",
"submit_failed": "Felrapporten kunde inte skickas.",
"required": "Alla fält och godkännande av datainsamling krävs.",
"page_subtitle": "Rapporterade skärmar, request-fel och Vue-fel",
@@ -4068,6 +4228,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": {
@@ -4325,6 +4494,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",
@@ -4886,6 +5056,97 @@
"subtitle": "@.capitalize:{'words.generated.managed'} @:{'words.generated.database'} @:{'words.generated.and'} @:{'words.replication.services.redis'} @:{'words.generated.replication'}"
},
"roles": {
"permissions": {
"access_scope": "Atkomstomrade",
"actions": {
"clear_all": "Rensa grupp",
"reload": "Ladda om",
"retry": "Forsok igen",
"select_all": "Valj grupp"
},
"columns": {
"enabled": "Aktiverad",
"permission": "Behorighet",
"route": "Rutt",
"status": "Status"
},
"department_access_description": "Ger atkomst till {name}.",
"empty": "Inga behorigheter ar tillgangliga.",
"empty_group": "Den har gruppen har inga behorigheter.",
"errors": {
"load": "Kunde inte lasa in rollbehorigheter.",
"update": "En eller flera behorigheter kunde inte uppdateras. Rollens aktuella behorigheter laddades om."
},
"filtered_summary": "{count} traffar",
"groups": {
"access": {
"description": "Sidtyp- och avdelningsatkomst.",
"label": "Atkomst"
},
"bookings": {
"description": "Behorigheter for bokningar, tidsbokningar och planering.",
"label": "Bokningar"
},
"customers_users": {
"description": "Behorigheter for kunder, anvandare, medarbetare, roller och inloggning.",
"label": "Kunder och anvandare"
},
"departments_operations": {
"description": "Avdelningsdrift, banor, relan, aviseringar, rapporter och mal.",
"label": "Avdelningar och drift"
},
"invoicing_economic": {
"description": "Behorigheter for fakturor, faktureringsperioder, inkasso och e-conomic.",
"label": "Fakturering och e-conomic"
},
"orders_pos": {
"description": "Behorigheter for ordrar, POS, betalningar och orderrader.",
"label": "Ordrar och POS"
},
"other": {
"description": "Behorigheter som inte matchar nagon annan grupp.",
"label": "Ovrigt"
},
"products_pricing": {
"description": "Behorigheter for produkter, priser, rabatter och valuta.",
"label": "Produkter och priser"
},
"selfserve_edge": {
"description": "Behorigheter for sjalvbetjaning, tvatt, skannrar, gateways och edge-utrustning.",
"label": "Sjalvbetjaning och edge"
},
"system_config": {
"description": "System, konfiguration, release, replikering och integrationer.",
"label": "System och konfiguration"
}
},
"no_description": "Ingen beskrivning ar tillganglig.",
"no_search_results": "Inga behorigheter matchar aktuell sokning.",
"page_access": {
"admin": {
"description": "Ger atkomst till administrationssidor.",
"label": "Admin"
},
"superuser": {
"description": "Ger atkomst till superuser-sidor.",
"label": "Superuser"
},
"user": {
"description": "Ger atkomst till anvandarsidor.",
"label": "Anvandare"
}
},
"search_label": "Sok behorigheter",
"search_placeholder": "Sok pa label, nyckel, beskrivning, rutt eller metod",
"selected_summary": "{selected} av {total} valda",
"status": {
"disabled": "Inaktiverad",
"enabled": "Aktiverad"
},
"subtitle": "Rollbehorigheter",
"title": "Behorigheter for roll {id}",
"title_with_name": "Behorigheter for {name}"
},
"subtitle": "@.capitalize:{'words.generated.user'} @:{'words.generated.roles'}"
},
"self_wash": {
@@ -5675,6 +5936,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'}",
@@ -5881,6 +6167,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'}",
@@ -5945,6 +6245,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.",
@@ -5959,6 +6262,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'}.",
@@ -5977,7 +6289,13 @@
"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": {
@@ -6007,7 +6325,132 @@
"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",
"tooltip": "Visa rollbehörigheter",
"intro": "Roller samlar de exakta funktioner som tilldelas en medarbetare när rollen sparas.",
"scope": "Alla funktioner är begränsade till de avdelningar som väljs för medarbetaren.",
"selected": "Vald",
"close": "Stäng",
"unavailable": "Behörighetsdetaljer är inte tillgängliga för den här rollen förrän rollkatalogen har uppdaterats.",
"groups": {
"account": "Konto",
"orders": "Order",
"bookings": "Bokningar",
"time_bookings": "Tidsbokningar",
"reports": "Rapporter",
"limited_backoffice": "Backoffice",
"unknown": "Övrigt"
},
"capabilities": {
"sign_in": {
"label": "Logga in",
"description": "Ger medarbetaren möjlighet att logga in i Pleno."
},
"view_own_permissions": {
"label": "Se egna behörigheter",
"description": "Visar medarbetaren vilka åtkomsträttigheter kontot har."
},
"view_orders": {
"label": "Se order",
"description": "Kan öppna orderlistor och orderdetaljer för tilldelade avdelningar."
},
"create_orders": {
"label": "Skapa order",
"description": "Kan skapa nya order för tilldelade avdelningar."
},
"edit_orders": {
"label": "Redigera order",
"description": "Kan ändra orderdetaljer för tilldelade avdelningar."
},
"delete_orders": {
"label": "Ta bort order",
"description": "Kan ta bort order i tilldelade avdelningar."
},
"view_order_items": {
"label": "Se orderrader",
"description": "Kan se produkter, tjänster och priser på order."
},
"create_order_items": {
"label": "Skapa orderrader",
"description": "Kan lägga till produkter och tjänster på order."
},
"update_order_lines": {
"label": "Redigera orderrader",
"description": "Kan ändra produkter, antal och priser på order."
},
"remove_order_lines": {
"label": "Ta bort orderrader",
"description": "Kan ta bort produkter och tjänster från order."
},
"charge_orders": {
"label": "Debitera order",
"description": "Kan ta betalt eller debitera en betalningsreservation för en order."
},
"view_department_bookings": {
"label": "Se avdelningsbokningar",
"description": "Kan se bokningar för tilldelade avdelningar."
},
"view_own_bookings": {
"label": "Se egna bokningar",
"description": "Kan se bokningsantal och bokningar kopplade till medarbetarkontot."
},
"update_bookings": {
"label": "Redigera bokningar",
"description": "Kan ändra tillåtna bokningsdetaljer."
},
"create_bookings": {
"label": "Skapa bokningar",
"description": "Kan skapa nya bokningar för tilldelade avdelningar."
},
"mark_bookings_complete": {
"label": "Slutför bokningar",
"description": "Kan markera bokningar som slutförda."
},
"send_booking_confirmations": {
"label": "Skicka bokningsbekräftelser igen",
"description": "Kan skicka bokningsbekräftelser och slutförandemejl igen."
},
"view_time_booking_entries": {
"label": "Se tidsbokningar",
"description": "Kan granska tidsbokningsposter för tilldelade avdelningar."
},
"create_time_booking_entries": {
"label": "Skapa tidsbokningar",
"description": "Kan lägga till nya tidsbokningsposter."
},
"edit_time_booking_entries": {
"label": "Redigera tidsbokningar",
"description": "Kan redigera tidsbokningsposter när backend-rutten stöder det."
},
"view_order_statistics": {
"label": "Se orderstatistik",
"description": "Kan öppna orderstatistik för tilldelade avdelningar."
},
"view_booking_statistics": {
"label": "Se bokningsstatistik",
"description": "Kan öppna bokningsstatistik för tilldelade avdelningar."
},
"open_limited_backoffice": {
"label": "Öppna begränsat backoffice",
"description": "Kan öppna det begränsade backofficeområdet."
},
"manage_department_prices": {
"label": "Hantera avdelningspriser",
"description": "Kan redigera uttryckliga priser för tilldelade avdelningar."
},
"manage_employee_access": {
"label": "Hantera medarbetaråtkomst",
"description": "Kan skapa, redigera och inaktivera begränsade backofficemedarbetare."
},
"unknown": {
"label": "Extra funktion",
"description": "Den här funktionen kommer från rollkatalogen men är ännu inte beskriven i gränssnittet."
}
}
}
}
}
@@ -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'}",
@@ -3,17 +3,17 @@
"error_report": {
"button": "Rapporter fejl",
"title": "Rapporter fejl",
"subtitle": "Send det aktuelle skærmbillede og de seneste fejldetaljer til support.",
"subtitle": "Send de seneste fejldetaljer til support. Et skærmbillede vedhæftes, når det er muligt.",
"before_error": "Hvad lavede du, før fejlen opstod?",
"expected": "Hvad forventede du, der ville ske?",
"actual": "Hvad skete der faktisk?",
"before_error_placeholder": "Beskriv handlingen, du var i gang med, for eksempel at åbne ordrer eller vælge en kunde.",
"expected_placeholder": "Beskriv det resultat, du forventede at se.",
"actual_placeholder": "Beskriv, hvad du så i stedet, inklusive eventuel fejltekst.",
"consent": "Jeg accepterer, at det aktuelle app-skærmbillede, de seneste request-fejl, Vue-fejl, browseroplysninger og mine svar indsamles til fejlfinding.",
"consent": "Jeg accepterer, at de seneste request-fejl, Vue-fejl, browseroplysninger, mine svar og et app-skærmbillede, når det er muligt, indsamles til fejlfinding.",
"submit": "Send rapport",
"submitted": "Fejlrapporten er sendt.",
"capture_failed": "Skærmbilledet kunne ikke oprettes. Prøv igen.",
"capture_failed": "Skærmbilledet kunne ikke oprettes. Rapporten sendes uden skærmbillede.",
"submit_failed": "Fejlrapporten kunne ikke sendes.",
"required": "Alle felter og accept af dataindsamling er påkrævet.",
"page_subtitle": "Rapporterede skærme, request-fejl og Vue-fejl",
@@ -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'}",
@@ -0,0 +1,97 @@
{
"compat": {
"roles": {
"permissions": {
"access_scope": "Adgangsomrade",
"actions": {
"clear_all": "Ryd gruppe",
"reload": "Genindlaes",
"retry": "Prov igen",
"select_all": "Vaelg gruppe"
},
"columns": {
"enabled": "Aktiveret",
"permission": "Tilladelse",
"route": "Rute",
"status": "Status"
},
"department_access_description": "Giver adgang til {name}.",
"empty": "Der er ingen tilladelser tilgaengelige.",
"empty_group": "Denne gruppe har ingen tilladelser.",
"errors": {
"load": "Rolletilladelser kunne ikke indlaeses.",
"update": "En eller flere tilladelser kunne ikke opdateres. Rollens aktuelle tilladelser blev genindlaest."
},
"filtered_summary": "{count} matcher",
"groups": {
"access": {
"description": "Sidetype- og afdelingsadgang.",
"label": "Adgang"
},
"bookings": {
"description": "Tilladelser til bookinger, tidsbookinger og planlaegning.",
"label": "Bookinger"
},
"customers_users": {
"description": "Tilladelser til kunder, brugere, medarbejdere, roller og login.",
"label": "Kunder og brugere"
},
"departments_operations": {
"description": "Afdelingsdrift, baner, relaer, notifikationer, rapporter og mal.",
"label": "Afdelinger og drift"
},
"invoicing_economic": {
"description": "Tilladelser til fakturaer, faktureringsperioder, opkraevning og e-conomic.",
"label": "Fakturering og e-conomic"
},
"orders_pos": {
"description": "Tilladelser til ordrer, POS, betalinger og ordrelinjer.",
"label": "Ordrer og POS"
},
"other": {
"description": "Tilladelser, der ikke matcher en anden gruppe.",
"label": "Andet"
},
"products_pricing": {
"description": "Tilladelser til produkter, priser, rabatter og valuta.",
"label": "Produkter og priser"
},
"selfserve_edge": {
"description": "Tilladelser til selvbetjening, vask, scannere, gateways og edge-udstyr.",
"label": "Selvbetjening og edge"
},
"system_config": {
"description": "System, konfiguration, release, replikering og integrationer.",
"label": "System og konfiguration"
}
},
"no_description": "Der er ingen beskrivelse.",
"no_search_results": "Ingen tilladelser matcher sogningen.",
"page_access": {
"admin": {
"description": "Giver adgang til administrationssider.",
"label": "Admin"
},
"superuser": {
"description": "Giver adgang til superbrugersider.",
"label": "Superbruger"
},
"user": {
"description": "Giver adgang til brugersider.",
"label": "Bruger"
}
},
"search_label": "Sog i tilladelser",
"search_placeholder": "Sog pa label, nogle, beskrivelse, rute eller metode",
"selected_summary": "{selected} af {total} valgt",
"status": {
"disabled": "Deaktiveret",
"enabled": "Aktiveret"
},
"subtitle": "Rolletilladelser",
"title": "Tilladelser for rolle {id}",
"title_with_name": "Tilladelser for {name}"
}
}
}
}
@@ -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'}",
@@ -2,6 +2,20 @@
"compat": {
"vehicles": {
"add": "@:{'terms.glossary.tilføj'} @:{'terms.glossary.køretøj'}",
"add_modal": {
"customer_label": "Kunde",
"customer_placeholder": "Søg efter kundenavn eller kundenummer",
"error": "Køretøjet kunne ikke tilføjes.",
"no_customer_results": "Ingen kunder fundet",
"reference_placeholder": "Valgfri reference",
"registration_placeholder": "Registreringsnummer",
"selected_customer": "Valgt kunde",
"submit": "Tilføj køretøj",
"title": "Tilføj køretøj",
"type_load_error": "Køretøjstyper kunne ikke indlæses.",
"type_placeholder": "Vælg køretøjstype",
"validation_error": "Vælg kunde, registreringsnummer og køretøjstype."
},
"brand": "@:{'phrases.compat.admin.pos.make'}",
"color": "Farve",
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
+297 -11
View File
@@ -28,13 +28,16 @@
"title": "@:{'phrases.compat.limited_backoffice.forbidden.title'}",
"department": "@:{'phrases.compat.limited_backoffice.forbidden.department'}"
},
"prices": {
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
"prices": {
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
"setup_required_title": "@:{'phrases.compat.limited_backoffice.prices.setup_required_title'}",
"setup_required_message": "@:{'phrases.compat.limited_backoffice.prices.setup_required_message'}",
"no_products": "@:{'phrases.compat.limited_backoffice.prices.no_products'}"
@@ -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,132 @@
"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'}",
"tooltip": "@:{'phrases.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'phrases.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'phrases.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'phrases.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'phrases.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'phrases.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
"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'}",
"limited_backoffice": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.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'}"
},
"view_own_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -141,6 +284,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.",
@@ -155,6 +301,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'}.",
@@ -173,7 +328,13 @@
"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": {
@@ -203,7 +364,132 @@
"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",
"tooltip": "Vis rolletilladelser",
"intro": "Roller samler de præcise funktioner, der tildeles en medarbejder, når rollen gemmes.",
"scope": "Alle funktioner er begrænset til de afdelinger, der vælges for medarbejderen.",
"selected": "Valgt",
"close": "Luk",
"unavailable": "Tilladelsesdetaljer er ikke tilgængelige for denne rolle, før rollekataloget er opdateret.",
"groups": {
"account": "Konto",
"orders": "Ordrer",
"bookings": "Bookinger",
"time_bookings": "Tidsbookinger",
"reports": "Rapporter",
"limited_backoffice": "Backoffice",
"unknown": "Øvrigt"
},
"capabilities": {
"sign_in": {
"label": "Log ind",
"description": "Giver medarbejderen adgang til at logge ind i Pleno."
},
"view_own_permissions": {
"label": "Se egne tilladelser",
"description": "Viser medarbejderen, hvilke adgangsrettigheder kontoen har."
},
"view_orders": {
"label": "Se ordrer",
"description": "Kan åbne ordrelister og ordredetaljer for tildelte afdelinger."
},
"create_orders": {
"label": "Opret ordrer",
"description": "Kan oprette nye ordrer for tildelte afdelinger."
},
"edit_orders": {
"label": "Rediger ordrer",
"description": "Kan ændre ordredetaljer for tildelte afdelinger."
},
"delete_orders": {
"label": "Slet ordrer",
"description": "Kan slette ordrer i tildelte afdelinger."
},
"view_order_items": {
"label": "Se ordrelinjer",
"description": "Kan se produkter, ydelser og priser på ordrer."
},
"create_order_items": {
"label": "Opret ordrelinjer",
"description": "Kan tilføje produkter og ydelser til ordrer."
},
"update_order_lines": {
"label": "Rediger ordrelinjer",
"description": "Kan ændre produkter, antal og priser på ordrer."
},
"remove_order_lines": {
"label": "Slet ordrelinjer",
"description": "Kan fjerne produkter og ydelser fra ordrer."
},
"charge_orders": {
"label": "Opkræv ordrer",
"description": "Kan tage betaling eller opkræve en betalingsreservation for en ordre."
},
"view_department_bookings": {
"label": "Se afdelingsbookinger",
"description": "Kan se bookinger for tildelte afdelinger."
},
"view_own_bookings": {
"label": "Se egne bookinger",
"description": "Kan se bookingantal og bookinger knyttet til medarbejderkontoen."
},
"update_bookings": {
"label": "Rediger bookinger",
"description": "Kan ændre tilladte bookingdetaljer."
},
"create_bookings": {
"label": "Opret bookinger",
"description": "Kan oprette nye bookinger for tildelte afdelinger."
},
"mark_bookings_complete": {
"label": "Afslut bookinger",
"description": "Kan markere bookinger som afsluttet."
},
"send_booking_confirmations": {
"label": "Gensend bookingbekræftelser",
"description": "Kan gensende bookingbekræftelser og afslutningsmails."
},
"view_time_booking_entries": {
"label": "Se tidsbookinger",
"description": "Kan gennemse tidsbookingposter for tildelte afdelinger."
},
"create_time_booking_entries": {
"label": "Opret tidsbookinger",
"description": "Kan oprette nye tidsbookingposter."
},
"edit_time_booking_entries": {
"label": "Rediger tidsbookinger",
"description": "Kan redigere tidsbookingposter, når backend-ruten understøtter det."
},
"view_order_statistics": {
"label": "Se ordrestatistik",
"description": "Kan åbne statistikvisninger for ordrer i tildelte afdelinger."
},
"view_booking_statistics": {
"label": "Se bookingstatistik",
"description": "Kan åbne statistikvisninger for bookinger i tildelte afdelinger."
},
"open_limited_backoffice": {
"label": "Åbn begrænset backoffice",
"description": "Kan tilgå det begrænsede backofficeområde."
},
"manage_department_prices": {
"label": "Administrer afdelingspriser",
"description": "Kan redigere eksplicitte priser for tildelte afdelinger."
},
"manage_employee_access": {
"label": "Administrer medarbejderadgang",
"description": "Kan oprette, redigere og deaktivere begrænsede backofficemedarbejdere."
},
"unknown": {
"label": "Ekstra funktion",
"description": "Denne funktion kommer fra rollekataloget, men er endnu ikke beskrevet i grænsefladen."
}
}
}
}
}
@@ -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'}",
@@ -3,17 +3,17 @@
"error_report": {
"button": "Fehler melden",
"title": "Fehler melden",
"subtitle": "Sende den aktuellen Bildschirm und die letzten Fehlerdetails an den Support.",
"subtitle": "Sende die letzten Fehlerdetails an den Support. Ein Screenshot wird angehängt, wenn möglich.",
"before_error": "Was haben Sie getan, bevor der Fehler auftrat?",
"expected": "Was hatten Sie erwartet?",
"actual": "Was ist tatsächlich passiert?",
"before_error_placeholder": "Beschreiben Sie die Aktion, zum Beispiel Bestellungen öffnen oder einen Kunden auswählen.",
"expected_placeholder": "Beschreiben Sie das erwartete Ergebnis.",
"actual_placeholder": "Beschreiben Sie, was stattdessen zu sehen war, inklusive Fehlermeldung.",
"consent": "Ich akzeptiere, dass der aktuelle App-Bildschirm, letzte Request-Fehler, Vue-Fehler, Browserdetails und meine Antworten zur Fehlersuche erfasst werden.",
"consent": "Ich akzeptiere, dass letzte Request-Fehler, Vue-Fehler, Browserdetails, meine Antworten und ein App-Screenshot, wenn möglich, zur Fehlersuche erfasst werden.",
"submit": "Bericht senden",
"submitted": "Fehlerbericht gesendet.",
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Der Bericht wird ohne Screenshot gesendet.",
"submit_failed": "Der Fehlerbericht konnte nicht gesendet werden.",
"required": "Alle Felder und die Zustimmung zur Datenerfassung sind erforderlich.",
"page_subtitle": "Gemeldete Bildschirme, Request-Fehler und Vue-Fehler",
@@ -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'}",
@@ -0,0 +1,97 @@
{
"compat": {
"roles": {
"permissions": {
"access_scope": "Zugriffsbereich",
"actions": {
"clear_all": "Gruppe leeren",
"reload": "Neu laden",
"retry": "Erneut versuchen",
"select_all": "Gruppe auswaehlen"
},
"columns": {
"enabled": "Aktiviert",
"permission": "Berechtigung",
"route": "Route",
"status": "Status"
},
"department_access_description": "Erlaubt Zugriff auf {name}.",
"empty": "Es sind keine Berechtigungen verfuegbar.",
"empty_group": "Diese Gruppe hat keine Berechtigungen.",
"errors": {
"load": "Rollenberechtigungen konnten nicht geladen werden.",
"update": "Eine oder mehrere Berechtigungen konnten nicht aktualisiert werden. Die aktuellen Rollenberechtigungen wurden neu geladen."
},
"filtered_summary": "{count} Treffer",
"groups": {
"access": {
"description": "Seitentyp- und Abteilungszugriff.",
"label": "Zugriff"
},
"bookings": {
"description": "Berechtigungen fuer Buchungen, Zeitbuchungen und Planung.",
"label": "Buchungen"
},
"customers_users": {
"description": "Berechtigungen fuer Kunden, Benutzer, Mitarbeiter, Rollen und Anmeldung.",
"label": "Kunden und Benutzer"
},
"departments_operations": {
"description": "Abteilungsbetrieb, Spuren, Relais, Benachrichtigungen, Berichte und Ziele.",
"label": "Abteilungen und Betrieb"
},
"invoicing_economic": {
"description": "Berechtigungen fuer Rechnungen, Abrechnungsperioden, Inkasso und e-conomic.",
"label": "Rechnungen und e-conomic"
},
"orders_pos": {
"description": "Berechtigungen fuer Auftraege, POS, Zahlungen und Auftragszeilen.",
"label": "Auftraege und POS"
},
"other": {
"description": "Berechtigungen, die keiner anderen Gruppe entsprechen.",
"label": "Sonstige"
},
"products_pricing": {
"description": "Berechtigungen fuer Produkte, Preise, Rabatte und Waehrungen.",
"label": "Produkte und Preise"
},
"selfserve_edge": {
"description": "Berechtigungen fuer Self-Service, Waschen, Scanner, Gateways und Edge-Hardware.",
"label": "Self-Service und Edge"
},
"system_config": {
"description": "System, Konfiguration, Release, Replikation und Integrationen.",
"label": "System und Konfiguration"
}
},
"no_description": "Keine Beschreibung verfuegbar.",
"no_search_results": "Keine Berechtigungen entsprechen der Suche.",
"page_access": {
"admin": {
"description": "Erlaubt Zugriff auf Admin-Dashboardseiten.",
"label": "Admin"
},
"superuser": {
"description": "Erlaubt Zugriff auf Superuser-Dashboardseiten.",
"label": "Superuser"
},
"user": {
"description": "Erlaubt Zugriff auf Benutzer-Dashboardseiten.",
"label": "Benutzer"
}
},
"search_label": "Berechtigungen suchen",
"search_placeholder": "Nach Label, Schluessel, Beschreibung, Route oder Methode suchen",
"selected_summary": "{selected} von {total} ausgewaehlt",
"status": {
"disabled": "Deaktiviert",
"enabled": "Aktiviert"
},
"subtitle": "Rollenberechtigungen",
"title": "Berechtigungen fuer Rolle {id}",
"title_with_name": "Berechtigungen fuer {name}"
}
}
}
}
@@ -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'}",
+297 -11
View File
@@ -28,13 +28,16 @@
"title": "@:{'phrases.compat.limited_backoffice.forbidden.title'}",
"department": "@:{'phrases.compat.limited_backoffice.forbidden.department'}"
},
"prices": {
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
"prices": {
"title": "@:{'phrases.compat.limited_backoffice.prices.title'}",
"save": "@:{'phrases.compat.limited_backoffice.prices.save'}",
"saved": "@:{'phrases.compat.limited_backoffice.prices.saved'}",
"autosave_saving": "@:{'phrases.compat.limited_backoffice.prices.autosave_saving'}",
"autosave_pending": "@:{'phrases.compat.limited_backoffice.prices.autosave_pending'}",
"autosave_saved": "@:{'phrases.compat.limited_backoffice.prices.autosave_saved'}",
"product": "@:{'phrases.compat.limited_backoffice.prices.product'}",
"price": "@:{'phrases.compat.limited_backoffice.prices.price'}",
"price_required": "@:{'phrases.compat.limited_backoffice.prices.price_required'}",
"setup_required_title": "@:{'phrases.compat.limited_backoffice.prices.setup_required_title'}",
"setup_required_message": "@:{'phrases.compat.limited_backoffice.prices.setup_required_message'}",
"no_products": "@:{'phrases.compat.limited_backoffice.prices.no_products'}"
@@ -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,132 @@
"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'}",
"tooltip": "@:{'phrases.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'phrases.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'phrases.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'phrases.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'phrases.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'phrases.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
"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'}",
"limited_backoffice": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.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'}"
},
"view_own_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -141,6 +284,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'}.",
@@ -155,6 +301,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.",
@@ -173,7 +328,13 @@
"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": {
@@ -203,7 +364,132 @@
"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",
"tooltip": "Rollenberechtigungen anzeigen",
"intro": "Rollen bündeln die genauen Funktionen, die einem Mitarbeiter beim Speichern der Rolle zugewiesen werden.",
"scope": "Jede Funktion ist auf die Abteilungen begrenzt, die für den Mitarbeiter ausgewählt sind.",
"selected": "Ausgewählt",
"close": "Schließen",
"unavailable": "Berechtigungsdetails sind für diese Rolle erst verfügbar, wenn der Rollenkatalog aktualisiert wurde.",
"groups": {
"account": "Konto",
"orders": "Aufträge",
"bookings": "Buchungen",
"time_bookings": "Zeitbuchungen",
"reports": "Berichte",
"limited_backoffice": "Backoffice",
"unknown": "Sonstiges"
},
"capabilities": {
"sign_in": {
"label": "Anmelden",
"description": "Erlaubt dem Mitarbeiter, sich bei Pleno anzumelden."
},
"view_own_permissions": {
"label": "Eigene Berechtigungen anzeigen",
"description": "Zeigt dem Mitarbeiter, welche Zugriffsrechte das Konto hat."
},
"view_orders": {
"label": "Aufträge anzeigen",
"description": "Kann Auftragslisten und Auftragsdetails für zugewiesene Abteilungen öffnen."
},
"create_orders": {
"label": "Aufträge erstellen",
"description": "Kann neue Aufträge für zugewiesene Abteilungen erstellen."
},
"edit_orders": {
"label": "Aufträge bearbeiten",
"description": "Kann Auftragsdetails für zugewiesene Abteilungen ändern."
},
"delete_orders": {
"label": "Aufträge löschen",
"description": "Kann Aufträge in zugewiesenen Abteilungen löschen."
},
"view_order_items": {
"label": "Auftragspositionen anzeigen",
"description": "Kann Produkte, Leistungen und Preise auf Aufträgen sehen."
},
"create_order_items": {
"label": "Auftragspositionen erstellen",
"description": "Kann Produkte und Leistungen zu Aufträgen hinzufügen."
},
"update_order_lines": {
"label": "Auftragspositionen bearbeiten",
"description": "Kann Produkte, Mengen und Preise auf Aufträgen ändern."
},
"remove_order_lines": {
"label": "Auftragspositionen löschen",
"description": "Kann Produkte und Leistungen aus Aufträgen entfernen."
},
"charge_orders": {
"label": "Aufträge abrechnen",
"description": "Kann Zahlungen annehmen oder eine Zahlungsreservierung für einen Auftrag belasten."
},
"view_department_bookings": {
"label": "Abteilungsbuchungen anzeigen",
"description": "Kann Buchungen für zugewiesene Abteilungen sehen."
},
"view_own_bookings": {
"label": "Eigene Buchungen anzeigen",
"description": "Kann Buchungszahlen und Buchungen sehen, die mit dem Mitarbeiterkonto verbunden sind."
},
"update_bookings": {
"label": "Buchungen bearbeiten",
"description": "Kann erlaubte Buchungsdetails ändern."
},
"create_bookings": {
"label": "Buchungen erstellen",
"description": "Kann neue Buchungen für zugewiesene Abteilungen erstellen."
},
"mark_bookings_complete": {
"label": "Buchungen abschließen",
"description": "Kann Buchungen als abgeschlossen markieren."
},
"send_booking_confirmations": {
"label": "Buchungsbestätigungen erneut senden",
"description": "Kann Buchungsbestätigungen und Abschluss-E-Mails erneut senden."
},
"view_time_booking_entries": {
"label": "Zeitbuchungen anzeigen",
"description": "Kann Zeitbuchungseinträge für zugewiesene Abteilungen prüfen."
},
"create_time_booking_entries": {
"label": "Zeitbuchungen erstellen",
"description": "Kann neue Zeitbuchungseinträge hinzufügen."
},
"edit_time_booking_entries": {
"label": "Zeitbuchungen bearbeiten",
"description": "Kann Zeitbuchungseinträge bearbeiten, wenn die Backend-Route dies unterstützt."
},
"view_order_statistics": {
"label": "Auftragsstatistik anzeigen",
"description": "Kann Auftragsstatistiken für zugewiesene Abteilungen öffnen."
},
"view_booking_statistics": {
"label": "Buchungsstatistik anzeigen",
"description": "Kann Buchungsstatistiken für zugewiesene Abteilungen öffnen."
},
"open_limited_backoffice": {
"label": "Begrenztes Backoffice öffnen",
"description": "Kann den begrenzten Backofficebereich öffnen."
},
"manage_department_prices": {
"label": "Abteilungspreise verwalten",
"description": "Kann explizite Preise für zugewiesene Abteilungen bearbeiten."
},
"manage_employee_access": {
"label": "Mitarbeiterzugang verwalten",
"description": "Kann begrenzte Backoffice-Mitarbeiter erstellen, bearbeiten und deaktivieren."
},
"unknown": {
"label": "Zusätzliche Funktion",
"description": "Diese Funktion kommt aus dem Rollenkatalog, ist aber in der Oberfläche noch nicht beschrieben."
}
}
}
}
}

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