Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fac731ae19 | ||
|
|
2f2beb774a | ||
|
|
a98dc061da |
@@ -18,14 +18,15 @@ jobs:
|
||||
build-upload-and-verify:
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
env:
|
||||
RELEASE_BASE_URL: https://api-v2.truckwash.io/master/frontend
|
||||
PLAYWRIGHT_BASE_URL: https://api-v2.truckwash.io/master/frontend
|
||||
RELEASE_BASE_URL: https://dev.truckwash.io
|
||||
PLAYWRIGHT_BASE_URL: https://dev.truckwash.io
|
||||
PLAYWRIGHT_RELEASE_API_BASE_URL: https://api-v2.truckwash.io
|
||||
PLAYWRIGHT_RELEASE_API_PING_PATHS: /ping,/master/api/ping,/canary/api/ping,/stable/api/ping
|
||||
RELEASE_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
RELEASE_EXPECTED_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
RELEASE_EXPECTED_COMMIT: ${{ github.sha }}
|
||||
RELEASE_WAIT_INITIAL_SECONDS: 45
|
||||
RELEASE_WAIT_TIMEOUT_SECONDS: 600
|
||||
RELEASE_WAIT_INITIAL_SECONDS: 30
|
||||
RELEASE_WAIT_TIMEOUT_SECONDS: 300
|
||||
RELEASE_POLL_INTERVAL_SECONDS: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -71,7 +72,26 @@ jobs:
|
||||
path: dist
|
||||
retention-days: 14
|
||||
|
||||
- name: Wait for Coolify release artifact
|
||||
- name: Install lftp
|
||||
run: |
|
||||
if ! command -v lftp >/dev/null 2>&1; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y lftp
|
||||
fi
|
||||
|
||||
- name: Upload hashed assets before release metadata
|
||||
run: |
|
||||
test -n "$RELEASE_DEPLOY_HOST" || (echo "RELEASE_DEPLOY_HOST is required" >&2; exit 1)
|
||||
test -n "$RELEASE_DEPLOY_USER" || (echo "RELEASE_DEPLOY_USER is required" >&2; exit 1)
|
||||
test -n "$RELEASE_DEPLOY_PASSWORD" || (echo "RELEASE_DEPLOY_PASSWORD is required" >&2; exit 1)
|
||||
npm run release:upload:lftp
|
||||
env:
|
||||
RELEASE_DEPLOY_HOST: ${{ secrets.RELEASE_DEPLOY_HOST }}
|
||||
RELEASE_DEPLOY_USER: ${{ secrets.RELEASE_DEPLOY_USER }}
|
||||
RELEASE_DEPLOY_PASSWORD: ${{ secrets.RELEASE_DEPLOY_PASSWORD }}
|
||||
RELEASE_DEPLOY_REMOTE_ROOT: ${{ secrets.RELEASE_DEPLOY_REMOTE_ROOT }}
|
||||
|
||||
- name: Wait for exact uploaded build
|
||||
run: npm run release:verify-upload
|
||||
|
||||
- name: Public live Playwright gate
|
||||
@@ -98,13 +118,10 @@ jobs:
|
||||
-X POST "$RELEASE_MANAGER_GATE_URL" \
|
||||
-H "Authorization: Bearer $RELEASE_MANAGER_GATE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"$RELEASE_BRANCH\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":true,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"static_artifact\",\"api_gateway\"]}"
|
||||
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$RELEASE_EXPECTED_BUILD_ID\",\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"static_artifact\",\"api_gateway\"]}"
|
||||
env:
|
||||
RELEASE_MANAGER_GATE_URL: ${{ secrets.RELEASE_MANAGER_GATE_URL || 'https://api.truckwash.io/release/gate/test-runs' }}
|
||||
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
|
||||
RELEASE_REPOSITORY: ${{ github.repository }}
|
||||
RELEASE_BRANCH: ${{ github.ref_name }}
|
||||
RELEASE_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: Update server version after verification
|
||||
run: npm run release:update-server-version
|
||||
|
||||
@@ -65,35 +65,15 @@ jobs:
|
||||
if: github.event_name != 'schedule'
|
||||
needs: build-and-unit
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
env:
|
||||
PLAYWRIGHT_PR_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
PLAYWRIGHT_PR_HEAD: ${{ github.sha }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve Playwright diff refs
|
||||
id: playwright-diff
|
||||
shell: bash
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.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"
|
||||
elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then
|
||||
git fetch --no-tags --prune origin "$DEFAULT_BRANCH"
|
||||
base_ref="origin/$DEFAULT_BRANCH"
|
||||
else
|
||||
base_ref="$PUSH_BEFORE_SHA"
|
||||
fi
|
||||
echo "base=$base_ref" >> "$GITHUB_OUTPUT"
|
||||
echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
@@ -107,7 +87,7 @@ jobs:
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run Playwright PR tests
|
||||
run: npm run test:e2e:pr -- --base="${{ steps.playwright-diff.outputs.base }}" --head="${{ steps.playwright-diff.outputs.head }}"
|
||||
run: npm run test:e2e:pr -- --base="$PLAYWRIGHT_PR_BASE" --head="$PLAYWRIGHT_PR_HEAD"
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: failure()
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
FROM node:24-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
ARG RELEASE_COMMIT_SHA=""
|
||||
ARG COMMIT_SHA=""
|
||||
ARG GITHUB_SHA=""
|
||||
ARG SOURCE_COMMIT=""
|
||||
ARG VITE_BASE_PATH=""
|
||||
ENV RELEASE_COMMIT_SHA="${RELEASE_COMMIT_SHA}"
|
||||
ENV COMMIT_SHA="${COMMIT_SHA}"
|
||||
ENV GITHUB_SHA="${GITHUB_SHA}"
|
||||
ENV SOURCE_COMMIT="${SOURCE_COMMIT}"
|
||||
ENV VITE_BASE_PATH="${VITE_BASE_PATH}"
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
@@ -8,14 +18,6 @@ COPY package*.json ./
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
COPY . .
|
||||
ARG SOURCE_COMMIT
|
||||
ARG RELEASE_COMMIT_SHA
|
||||
ARG COMMIT_SHA
|
||||
ARG GITHUB_SHA
|
||||
ENV SOURCE_COMMIT=$SOURCE_COMMIT
|
||||
ENV RELEASE_COMMIT_SHA=$RELEASE_COMMIT_SHA
|
||||
ENV COMMIT_SHA=$COMMIT_SHA
|
||||
ENV GITHUB_SHA=$GITHUB_SHA
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
@@ -22,28 +22,6 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
By default, the Vite dev server proxies `/api/*` to the remote stable API at
|
||||
`https://api-v2.truckwash.io/master/api`. This lets the Vue app run locally
|
||||
without a local PHP API container.
|
||||
|
||||
To develop against a local PHP API instead:
|
||||
|
||||
```powershell
|
||||
$env:VITE_API_PROXY_TARGET="http://localhost"; npm run dev
|
||||
```
|
||||
|
||||
To use another remote API route:
|
||||
|
||||
```powershell
|
||||
$env:VITE_API_PROXY_BASE_PATH="/canary/api"; npm run dev
|
||||
```
|
||||
|
||||
For compatible local gateways that expect the `/api` prefix to be preserved:
|
||||
|
||||
```powershell
|
||||
$env:VITE_API_PROXY_TARGET="http://localhost"; $env:VITE_API_PROXY_STRIP_PREFIX="false"; npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
|
||||
@@ -3056,8 +3056,6 @@ paths:
|
||||
get:
|
||||
tags:
|
||||
- Orders
|
||||
x-api-coverage:
|
||||
happy: true
|
||||
summary: List orders
|
||||
description: Retrieve a paginated list of orders
|
||||
operationId: listOrders
|
||||
|
||||
Generated
+34
-15
@@ -233,6 +233,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -1930,16 +1931,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@bubblewrap/core/node_modules/inquirer/node_modules/@types/node": {
|
||||
"version": "25.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
||||
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@bubblewrap/core/node_modules/mute-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
|
||||
@@ -1963,8 +1954,7 @@
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@bubblewrap/validator": {
|
||||
"version": "1.22.0",
|
||||
@@ -2087,6 +2077,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
@@ -2135,6 +2126,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
@@ -2572,6 +2564,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz",
|
||||
"integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"preact": "~10.12.1"
|
||||
}
|
||||
@@ -2815,6 +2808,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz",
|
||||
"integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/core": "^0.22.12"
|
||||
}
|
||||
@@ -2851,6 +2845,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz",
|
||||
"integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -2863,6 +2858,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz",
|
||||
"integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -2887,6 +2883,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz",
|
||||
"integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12",
|
||||
"tinycolor2": "^1.6.0"
|
||||
@@ -2930,6 +2927,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz",
|
||||
"integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -3053,6 +3051,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz",
|
||||
"integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -3065,6 +3064,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz",
|
||||
"integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -3080,6 +3080,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz",
|
||||
"integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -5342,6 +5343,7 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5376,6 +5378,7 @@
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -5469,7 +5472,8 @@
|
||||
"version": "5.10.4",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.10.4.tgz",
|
||||
"integrity": "sha512-gt0VUqZ2+mr25ScbUcKZgJr96jKYm4vjOcxEWCEh/E5F4dWqhyo3dBhPRvNNnkKiWxkMd2cBwj3ZYH3rK39fkA==",
|
||||
"license": "SEE LICENSE IN LICENSE"
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.1",
|
||||
@@ -5593,6 +5597,7 @@
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
@@ -5816,6 +5821,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -5900,7 +5906,8 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
|
||||
"integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/bulma-block-list": {
|
||||
"version": "1.1.0",
|
||||
@@ -6158,6 +6165,7 @@
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
@@ -6580,6 +6588,7 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -6768,6 +6777,7 @@
|
||||
"integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==",
|
||||
"deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-sfc": "2.7.16",
|
||||
"csstype": "^3.1.0"
|
||||
@@ -8325,7 +8335,8 @@
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz",
|
||||
"integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==",
|
||||
"license": "Apache-2.0"
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
@@ -10392,6 +10403,7 @@
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
@@ -10746,6 +10758,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz",
|
||||
"integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -10929,6 +10942,7 @@
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz",
|
||||
"integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.1.5",
|
||||
@@ -10950,6 +10964,7 @@
|
||||
"integrity": "sha512-Do7u6iRb6K+lrllcTkB1BXcHwOxcKe3rEfOF/GcCLE2w3WpddakRAosJOHFUR37DpsvimQXEt5abs3NzUjEIqg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.5.0",
|
||||
"colorjs.io": "^0.5.0",
|
||||
@@ -12715,6 +12730,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
|
||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -14107,6 +14123,7 @@
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
@@ -14166,6 +14183,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz",
|
||||
"integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.31",
|
||||
"@vue/compiler-sfc": "3.5.31",
|
||||
@@ -14853,6 +14871,7 @@
|
||||
"integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
|
||||
+29
-33
@@ -1,5 +1,4 @@
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
@@ -12,16 +11,15 @@ const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://${devHost}:${devPort}
|
||||
const runtimeNamespace = String(process.env.PLAYWRIGHT_ARTIFACT_NAMESPACE || `port-${devPort}`)
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
||||
const serverLogDir = path.resolve(process.cwd(), "output/playwright");
|
||||
const pidFile = path.resolve(serverLogDir, `dev-server-${runtimeNamespace}.json`);
|
||||
const stdoutLogFile = path.resolve(serverLogDir, `dev-server-${runtimeNamespace}.stdout.log`);
|
||||
const stderrLogFile = path.resolve(serverLogDir, `dev-server-${runtimeNamespace}.stderr.log`);
|
||||
const viteCliPath = path.resolve(process.cwd(), "node_modules/vite/bin/vite.js");
|
||||
const pidFile = path.resolve(process.cwd(), "output/playwright", `dev-server-${runtimeNamespace}.json`);
|
||||
const serverOutputLimit = 80;
|
||||
const activeOutputReaders = [];
|
||||
// Hardlinked Windows worktrees can break Vite's bundled config temp paths during Playwright boot.
|
||||
const viteDevArgs = [
|
||||
...(process.env.PLAYWRIGHT_VITE_FORCE === "1" ? ["--force"] : []),
|
||||
"run",
|
||||
"dev",
|
||||
"--",
|
||||
"--force",
|
||||
...(process.platform === "win32" ? ["--configLoader", "runner"] : []),
|
||||
"--host",
|
||||
devHost,
|
||||
@@ -178,18 +176,14 @@ async function killProcessTree(pid) {
|
||||
}
|
||||
}
|
||||
|
||||
function captureProcessOutput(stream, lines, logStream) {
|
||||
function captureProcessOutput(stream, lines) {
|
||||
const reader = readline.createInterface({ input: stream });
|
||||
reader.on("line", (line) => {
|
||||
lines.push(line);
|
||||
logStream.write(`${line}\n`);
|
||||
if (lines.length > serverOutputLimit) {
|
||||
lines.splice(0, lines.length - serverOutputLimit);
|
||||
}
|
||||
});
|
||||
reader.on("close", () => {
|
||||
logStream.end();
|
||||
});
|
||||
return reader;
|
||||
}
|
||||
|
||||
@@ -374,10 +368,6 @@ async function warmModuleGraph(entryUrl, { depth = 2, timeoutMs = 120_000 } = {}
|
||||
}
|
||||
|
||||
for (const specifier of extractModuleImports(source)) {
|
||||
if (specifier.startsWith("/node_modules/.vite/deps/")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const expectedContentType = resolveExpectedContentType(specifier);
|
||||
const importUrl = new URL(specifier, current.url).toString();
|
||||
|
||||
@@ -446,25 +436,31 @@ export default async function globalSetup() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
const serverProcess = spawn(process.execPath, [viteCliPath, ...viteDevArgs], {
|
||||
cwd: process.cwd(),
|
||||
detached: true,
|
||||
env: {
|
||||
...process.env,
|
||||
PLAYWRIGHT: "1",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
const serverProcess =
|
||||
process.platform === "win32"
|
||||
? spawn("cmd.exe", ["/d", "/s", "/c", `npm.cmd ${viteDevArgs.join(" ")}`], {
|
||||
cwd: process.cwd(),
|
||||
detached: true,
|
||||
env: {
|
||||
...process.env,
|
||||
PLAYWRIGHT: "1",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
})
|
||||
: spawn("npm", viteDevArgs, {
|
||||
cwd: process.cwd(),
|
||||
detached: true,
|
||||
env: {
|
||||
...process.env,
|
||||
PLAYWRIGHT: "1",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
const stdoutLines = [];
|
||||
const stderrLines = [];
|
||||
const stdoutLogStream = createWriteStream(stdoutLogFile, { flags: "w" });
|
||||
const stderrLogStream = createWriteStream(stderrLogFile, { flags: "w" });
|
||||
serverProcess.on("exit", (code, signal) => {
|
||||
stderrLogStream.write(`[playwright-global-setup] vite exited with code ${code ?? "null"} signal ${signal ?? "null"}\n`);
|
||||
});
|
||||
const stdoutReader = captureProcessOutput(serverProcess.stdout, stdoutLines, stdoutLogStream);
|
||||
const stderrReader = captureProcessOutput(serverProcess.stderr, stderrLines, stderrLogStream);
|
||||
const stdoutReader = captureProcessOutput(serverProcess.stdout, stdoutLines);
|
||||
const stderrReader = captureProcessOutput(serverProcess.stderr, stderrLines);
|
||||
activeOutputReaders.push(stdoutReader, stderrReader);
|
||||
serverProcess.unref();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
||||
fullyParallel: true,
|
||||
forbidOnly: isCI,
|
||||
retries: isCI ? 2 : 0,
|
||||
workers: isCI ? 2 : 3,
|
||||
workers: isCI ? 2 : 1,
|
||||
reporter: [["list"], ["html", { open: "never", outputFolder: "output/playwright/prod/report" }]],
|
||||
outputDir: "output/playwright/prod/test-results",
|
||||
use: {
|
||||
|
||||
@@ -34,12 +34,6 @@ export const sourceMappings = [
|
||||
],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "user-vehicles",
|
||||
patterns: [/^src\/components\/displays\/user\/vehicles\//u, /^src\/views\/dashboards\/userDashboard\/vehicles\//u],
|
||||
specs: ["tests/e2e/userVehicles.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "pos",
|
||||
patterns: [/\/pos[/-]/iu, /POS/iu, /^src\/assets\/pos\.css$/u],
|
||||
|
||||
@@ -120,7 +120,7 @@ async function verifyShell(baseUrl, shellPath) {
|
||||
|
||||
async function verifyRelease(baseUrl) {
|
||||
const expectedCommit = process.env.RELEASE_EXPECTED_COMMIT || process.env.GITHUB_SHA || "";
|
||||
const expectedBuildId = process.env.RELEASE_EXPECTED_BUILD_ID || "";
|
||||
const expectedBuildId = process.env.RELEASE_EXPECTED_BUILD_ID || process.env.RELEASE_BUILD_ID || "";
|
||||
const manifest = await fetchJson(baseUrl, "release-manifest.json");
|
||||
const releaseEntry = await fetchJson(baseUrl, "release-entry.json");
|
||||
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const workingDirectory = process.cwd();
|
||||
const playwrightCliPath = path.join(workingDirectory, "node_modules", "@playwright", "test", "cli.js");
|
||||
const execFileAsync = promisify(execFile);
|
||||
export const roles = ["customer", "subuser", "admin", "superuser"];
|
||||
const roles = ["customer", "subuser", "admin", "superuser"];
|
||||
const listEntryPattern = /^\s+\[[^\]]+\]\s+›\s+(.+?):(\d+):(\d+)\s+›\s+(.+)\s*$/u;
|
||||
|
||||
export const ownedFilesByRole = {
|
||||
const ownedFilesByRole = {
|
||||
customer: [
|
||||
"auth.smoke.spec.js",
|
||||
"booking-selfserve.smoke.spec.js",
|
||||
"connectivityIssue.spec.ts",
|
||||
"example.spec.ts",
|
||||
"guest-book-wash-mobile.spec.ts",
|
||||
"i18n-catalog-switch.spec.ts",
|
||||
"i18n-v2-integrity.spec.ts",
|
||||
"i18n.smoke.spec.ts",
|
||||
"i18n.views.spec.ts",
|
||||
"navigation.smoke.spec.js",
|
||||
"qr-new-customer-layout.spec.ts",
|
||||
"release-bootstrap.spec.js",
|
||||
"release-channel-switched.spec.js",
|
||||
"release-channel-unavailable.spec.js",
|
||||
"release-update-widget.spec.js",
|
||||
"self-serve-wash.spec.js",
|
||||
"session-release-runtime.spec.ts",
|
||||
"user-orders.spec.ts",
|
||||
"userBookings.spec.ts",
|
||||
"userBookWash.spec.ts",
|
||||
@@ -62,7 +53,6 @@ export const ownedFilesByRole = {
|
||||
"assign-draft-order-modal-layout.spec.ts",
|
||||
"change-invoice-collection.spec.ts",
|
||||
"change-customer.spec.ts",
|
||||
"default-mobile-redirect.spec.ts",
|
||||
"economic-queue-workflow.spec.js",
|
||||
"pos-customer-rules.spec.js",
|
||||
"pos-desktop-card-payments.spec.js",
|
||||
@@ -72,23 +62,18 @@ export const ownedFilesByRole = {
|
||||
"pos.visual.spec.js",
|
||||
],
|
||||
superuser: [
|
||||
"coolify-infrastructure.spec.js",
|
||||
"edge-gateways.fleet-outline.spec.js",
|
||||
"edge-gateways.routes.spec.js",
|
||||
"edge-gateways.smoke.spec.js",
|
||||
"edge-gateways.visual.spec.js",
|
||||
"errorReports.spec.ts",
|
||||
"failover-config.source.spec.ts",
|
||||
"invoice-distribution.smoke.spec.js",
|
||||
"invoice-transfer-monitor.spec.ts",
|
||||
"invoice-transfer-queue-history.spec.js",
|
||||
"invoicing-period.smoke.spec.js",
|
||||
"issue-repro-duplicates-date.spec.js",
|
||||
"release-manager.spec.js",
|
||||
"self-serve-sessions.spec.js",
|
||||
"self-serve-studio-audit-navigation.spec.js",
|
||||
"self-serve-studio-flow.spec.js",
|
||||
"session-bootstrap.spec.ts",
|
||||
"superuser-bookings.spec.ts",
|
||||
"superuser-customer-complaints.spec.ts",
|
||||
"superuser-customers-mass-import.spec.ts",
|
||||
@@ -99,13 +84,12 @@ export const ownedFilesByRole = {
|
||||
"superuser-drafts.spec.ts",
|
||||
"superuser-products-layout.spec.ts",
|
||||
"superuser-system-status.smoke.spec.js",
|
||||
"superuser-users.spec.ts",
|
||||
"superuser-vehicles.smoke.spec.js",
|
||||
"workfeed-config.smoke.spec.js",
|
||||
],
|
||||
};
|
||||
|
||||
export const titleRules = [
|
||||
const titleRules = [
|
||||
{ role: "customer", file: "userAuth.spec.ts", patterns: [/\[AUTH\]\[User\]/u] },
|
||||
{ role: "subuser", file: "userAuth.spec.ts", patterns: [/\[AUTH\]\[Subuser\]/u] },
|
||||
{ role: "admin", file: "userAuth.spec.ts", patterns: [/\[AUTH\]\[Operator\]/u] },
|
||||
@@ -132,11 +116,6 @@ export const titleRules = [
|
||||
file: "subuser-management.spec.ts",
|
||||
patterns: [/^authorized subuser managers/i, /^subuser self-service/i, /^subusers without /i],
|
||||
},
|
||||
{
|
||||
role: "superuser",
|
||||
file: "subuser-management.spec.ts",
|
||||
patterns: [/^superusers can list and invite chauffeurs/i],
|
||||
},
|
||||
{ role: "subuser", file: "userProfileVisibility.spec.ts", patterns: [/\[PROFILE\]\[Subuser\]\[Visibility\]/u] },
|
||||
];
|
||||
|
||||
@@ -221,7 +200,7 @@ function toBaseName(filePath) {
|
||||
return filePath.split(/[\\/]/u).pop() || filePath;
|
||||
}
|
||||
|
||||
export function parseListedTests(listOutput) {
|
||||
function parseListedTests(listOutput) {
|
||||
return listOutput
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => {
|
||||
@@ -243,7 +222,7 @@ export function parseListedTests(listOutput) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function classifyTest(testEntry) {
|
||||
function classifyTest(testEntry) {
|
||||
const matches = new Set();
|
||||
const directOwner = ownedFileToRole.get(testEntry.fileName);
|
||||
|
||||
@@ -330,8 +309,8 @@ async function runPlaywright(project, testListPath, forwardedArgs) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2)) {
|
||||
const { options, forwardedArgs } = parseCliArgs(argv);
|
||||
async function main() {
|
||||
const { options, forwardedArgs } = parseCliArgs(process.argv.slice(2));
|
||||
validateOptions(options, forwardedArgs);
|
||||
|
||||
const listOutput = await listProjectTests(options.project, forwardedArgs);
|
||||
@@ -363,17 +342,4 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
await runPlaywright(options.project, testListPath, forwardedArgs);
|
||||
}
|
||||
|
||||
async function isDirectRun() {
|
||||
if (!process.argv[1]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentPath = await fs.realpath(fileURLToPath(import.meta.url));
|
||||
const invokedPath = await fs.realpath(process.argv[1]).catch(() => path.resolve(process.argv[1]));
|
||||
|
||||
return currentPath === invokedPath;
|
||||
}
|
||||
|
||||
if (await isDirectRun()) {
|
||||
await main();
|
||||
}
|
||||
await main();
|
||||
|
||||
@@ -159,7 +159,6 @@ async function runPlaywright({ label, commandArgs, artifactSuffix }) {
|
||||
PLAYWRIGHT: "1",
|
||||
PLAYWRIGHT_ARTIFACT_NAMESPACE: getArtifactNamespace(artifactSuffix),
|
||||
PLAYWRIGHT_REPORTER_MODE: "line-html",
|
||||
PLAYWRIGHT_WORKERS: process.env.PLAYWRIGHT_WORKERS || "1",
|
||||
},
|
||||
stdio: "inherit",
|
||||
windowsHide: true,
|
||||
@@ -319,19 +318,11 @@ function groupSpecsByProjects(specProjects) {
|
||||
|
||||
async function runCorePrGate() {
|
||||
const projects = getSelectedProjects();
|
||||
for (const project of projects) {
|
||||
const code = await runPlaywright({
|
||||
label: `core ${prGrep} gate (${project})`,
|
||||
artifactSuffix: `core-${project}`,
|
||||
commandArgs: ["--grep", prGrep, "--project", project],
|
||||
});
|
||||
|
||||
if (code !== 0) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return runPlaywright({
|
||||
label: `core ${prGrep} gate`,
|
||||
artifactSuffix: "core",
|
||||
commandArgs: ["--grep", prGrep, ...buildProjectArgs(projects)],
|
||||
});
|
||||
}
|
||||
|
||||
async function runChangedSelection(selection) {
|
||||
@@ -341,19 +332,11 @@ async function runChangedSelection(selection) {
|
||||
console.log(
|
||||
`[playwright-pr] Falling back to broader ${smokeGrep} coverage because these changed files were unmapped: ${selection.unmappedFiles.join(", ")}`
|
||||
);
|
||||
for (const project of projects) {
|
||||
const code = await runPlaywright({
|
||||
label: `fallback ${smokeGrep} gate (${project})`,
|
||||
artifactSuffix: `smoke-fallback-${project}`,
|
||||
commandArgs: ["--grep", smokeGrep, "--grep-invert", prGrep, "--project", project],
|
||||
});
|
||||
|
||||
if (code !== 0) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return runPlaywright({
|
||||
label: `fallback ${smokeGrep} gate`,
|
||||
artifactSuffix: "smoke-fallback",
|
||||
commandArgs: ["--grep", smokeGrep, "--grep-invert", prGrep, ...buildProjectArgs(projects)],
|
||||
});
|
||||
}
|
||||
|
||||
const groups = groupSpecsByProjects(selection.specProjects);
|
||||
@@ -363,16 +346,14 @@ async function runChangedSelection(selection) {
|
||||
}
|
||||
|
||||
for (const [index, group] of groups.entries()) {
|
||||
for (const project of group.projects) {
|
||||
const code = await runPlaywright({
|
||||
label: `changed-area specs ${index + 1}/${groups.length} (${project})`,
|
||||
artifactSuffix: `changed-${index + 1}-${project}`,
|
||||
commandArgs: [...group.specs, "--project", project],
|
||||
});
|
||||
const code = await runPlaywright({
|
||||
label: `changed-area specs ${index + 1}/${groups.length}`,
|
||||
artifactSuffix: `changed-${index + 1}`,
|
||||
commandArgs: [...group.specs, ...buildProjectArgs(group.projects)],
|
||||
});
|
||||
|
||||
if (code !== 0) {
|
||||
return code;
|
||||
}
|
||||
if (code !== 0) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ const { t, te, locale } = useI18n({ useScope: "global" });
|
||||
const APP_TITLE = "Truck Wash";
|
||||
const LayoutV2 = defineAsyncComponent(() => import("@/components/page/wrappers/LayoutV2.vue"));
|
||||
const DefaultPageWrapper = defineAsyncComponent(() => import("@/components/page/wrappers/DefaultPageWrapper.vue"));
|
||||
const VersionCheck = defineAsyncComponent(() => import("@/components/global/VersionCheck.vue"));
|
||||
const RequestQueueProgress = defineAsyncComponent(() => import("@/components/global/RequestQueueProgress.vue"));
|
||||
const ErrorReportLauncher = defineAsyncComponent(() => import("@/components/global/ErrorReportLauncher.vue"));
|
||||
const ReleaseChannelUnavailable = defineAsyncComponent(() =>
|
||||
@@ -122,6 +123,7 @@ watch([() => route.fullPath, locale], updateDocumentTitle, { immediate: true });
|
||||
<header></header>
|
||||
<main>
|
||||
<DefaultPageWrapper>
|
||||
<VersionCheck />
|
||||
<router-view />
|
||||
</DefaultPageWrapper>
|
||||
</main>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<script setup>
|
||||
import CustomerComplaintsPagination from "@/components/displays/pagination/models/SuperUserDashboard/CustomerComplaintsPagination.vue";
|
||||
import PageTitle from "@/components/global/PageTitle.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-testid="superuser-complaints-page">
|
||||
<CustomerComplaintsPagination :auto-load="true" />
|
||||
<PageTitle
|
||||
:title="t('superuser.pages.complaints.title')"
|
||||
:subtitle="t('superuser.pages.complaints.subtitle')"
|
||||
/>
|
||||
<CustomerComplaintsPagination auto-load="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { getDepartmentListData } from "@/components/session/Session.vue";
|
||||
import { showCreateDepartmentForm } from "@/components/forms/superUser/createDepartmentForm.vue";
|
||||
import PageTitle from "@/components/global/PageTitle.vue";
|
||||
import DepartmentsPagination from "@/components/displays/pagination/models/SuperUserDashboard/DepartmentsPagination.vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const tableData = ref([]);
|
||||
|
||||
getDepartmentListData().then((response) => {
|
||||
tableData.value = response.data.data;
|
||||
});
|
||||
|
||||
const redirect = (path) => {
|
||||
window.location = path;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -25,4 +36,4 @@ const { t } = useI18n();
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
@@ -6,35 +6,15 @@ import SubusersPagination from "@/components/displays/pagination/models/SuperUse
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import SubuserGrantSelector from "@/components/session/subuser/SubuserGrantSelector.vue";
|
||||
|
||||
const props = defineProps({
|
||||
endpoint: {
|
||||
type: String,
|
||||
default: "/subusers",
|
||||
},
|
||||
showCustomer: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
superuserPage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const { t } = useI18n();
|
||||
|
||||
const canInvite = computed(() =>
|
||||
props.superuserPage
|
||||
? SessionUser.canAccessSuperUser()
|
||||
: SessionUser.canAccessUser() || SessionUser.hasPermission("SUBUSERS_ADD")
|
||||
);
|
||||
const canInvite = computed(() => SessionUser.canAccessUser() || SessionUser.hasPermission("SUBUSERS_ADD"));
|
||||
const requiresGrantSelection = computed(
|
||||
() => !props.superuserPage && SessionUser.isSubuser.value && !SessionUser.subuser.selectedGrantCustomerNumber.value
|
||||
() => SessionUser.isSubuser.value && !SessionUser.subuser.selectedGrantCustomerNumber.value
|
||||
);
|
||||
const paginationVersion = ref(0);
|
||||
const paginationKey = computed(() =>
|
||||
props.superuserPage
|
||||
? `subusers-superuser-${paginationVersion.value}`
|
||||
: SessionUser.isSubuser.value
|
||||
SessionUser.isSubuser.value
|
||||
? `subusers-${SessionUser.subuser.selectedGrantCustomerNumber.value || "none"}-${paginationVersion.value}`
|
||||
: `subusers-user-${paginationVersion.value}`
|
||||
);
|
||||
@@ -42,13 +22,13 @@ const paginationKey = computed(() =>
|
||||
const onInviteClick = async () => {
|
||||
await SessionUser.objects.subusers.functions.showInviteForm(() => {
|
||||
paginationVersion.value += 1;
|
||||
}, { superuser: props.superuserPage });
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PageTitle :title="SessionUser.objects.subusers.meta.title" :subtitle="t('superuser.pages.subusers.subtitle')">
|
||||
<PageTitle :title="t('superuser.pages.subusers.title')" :subtitle="t('superuser.pages.subusers.subtitle')">
|
||||
<template #buttons>
|
||||
<button v-if="canInvite" class="button is-dark" type="button" @click="onInviteClick">
|
||||
<span class="icon">
|
||||
@@ -59,7 +39,7 @@ const onInviteClick = async () => {
|
||||
</template>
|
||||
</PageTitle>
|
||||
|
||||
<div v-if="SessionUser.isSubuser.value && !superuserPage" class="mb-5">
|
||||
<div v-if="SessionUser.isSubuser.value" class="mb-5">
|
||||
<SubuserGrantSelector />
|
||||
<p class="help">
|
||||
Vælg den kunde, du vil administrere chauffører for. Listen og rettighederne følger det valgte kundenummer.
|
||||
@@ -70,13 +50,7 @@ const onInviteClick = async () => {
|
||||
Vælg først en kunde for at se og administrere chauffører.
|
||||
</div>
|
||||
|
||||
<SubusersPagination
|
||||
v-else
|
||||
:key="paginationKey"
|
||||
:endpoint="endpoint"
|
||||
:show-customer="showCustomer"
|
||||
auto-load="true"
|
||||
/>
|
||||
<SubusersPagination v-else :key="paginationKey" auto-load="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -63,10 +63,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
reg_3: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
order_booking_id: {
|
||||
type: Number,
|
||||
default: null,
|
||||
@@ -843,38 +839,6 @@ watch(isDropdownOpen, async (isOpen) => {
|
||||
|
||||
const attachmentsFromOrder = ref([]);
|
||||
const attachmentsFromOrderError = ref(null);
|
||||
const SELF_SERVE_WASH_ATTACHMENT_TYPE = "SELF_SERVE_WASH";
|
||||
|
||||
const normalizePositiveInteger = (value) => {
|
||||
const parsed = Number.parseInt(String(value ?? ""), 10);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
||||
};
|
||||
|
||||
const getAttachmentOtherPayload = (attachment) => attachment?.content?.other ?? null;
|
||||
|
||||
const isSelfServeWashAttachment = (attachment) => {
|
||||
const other = getAttachmentOtherPayload(attachment);
|
||||
return Boolean(other && typeof other === "object" && other.type === SELF_SERVE_WASH_ATTACHMENT_TYPE);
|
||||
};
|
||||
|
||||
const getSelfServeWashAttachment = computed(() =>
|
||||
attachmentsFromOrder.value.find((attachment) => isSelfServeWashAttachment(attachment)) || null
|
||||
);
|
||||
|
||||
const getSelfServeWashPayload = computed(() => getAttachmentOtherPayload(getSelfServeWashAttachment.value));
|
||||
|
||||
const getSelfServeWashCustomerNumber = computed(() =>
|
||||
normalizePositiveInteger(getSelfServeWashPayload.value?.customer_number)
|
||||
);
|
||||
|
||||
const canAcceptSelfServeWashDraft = computed(() =>
|
||||
Boolean(
|
||||
props.order_id
|
||||
&& getSelfServeWashCustomerNumber.value
|
||||
&& normalizePositiveInteger(props.customer_number) !== getSelfServeWashCustomerNumber.value
|
||||
&& (SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser())
|
||||
)
|
||||
);
|
||||
|
||||
const isObjectUrl = (value) => typeof value === "string" && value.startsWith("blob:");
|
||||
|
||||
@@ -899,39 +863,16 @@ const clearAttachmentPreviewState = () => {
|
||||
};
|
||||
|
||||
const getAttachmentLabel = (attachment) => {
|
||||
if (isSelfServeWashAttachment(attachment)) {
|
||||
const customerNumber = normalizePositiveInteger(attachment?.content?.other?.customer_number);
|
||||
return customerNumber
|
||||
? t("admin.pos.settings_wheel.self_serve_wash_attachment_for_customer", { customerNumber })
|
||||
: t("admin.pos.settings_wheel.self_serve_wash_attachment");
|
||||
}
|
||||
|
||||
const other = getAttachmentOtherPayload(attachment);
|
||||
const otherLabel = typeof other === "string"
|
||||
? other
|
||||
: other && typeof other === "object"
|
||||
? (other.label || other.type || JSON.stringify(other))
|
||||
: null;
|
||||
|
||||
return (
|
||||
attachment?.content?.document ||
|
||||
attachment?.content?.image ||
|
||||
otherLabel ||
|
||||
attachment?.content?.other ||
|
||||
`Attachment ${attachment?.id ?? ""}`.trim()
|
||||
);
|
||||
};
|
||||
|
||||
const isWashCertificateAttachment = (attachment) => {
|
||||
const marker = String(getAttachmentOtherPayload(attachment) || "").trim().toUpperCase();
|
||||
if (marker === "WASH_CERTIFICATE") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return /(?:^|[/\\])wash[_-]?certificate.*\.pdf$/i.test(getAttachmentLabel(attachment));
|
||||
};
|
||||
|
||||
const getAttachmentExtension = (attachment) => {
|
||||
const match = String(getAttachmentLabel(attachment))
|
||||
const match = getAttachmentLabel(attachment)
|
||||
.toLowerCase()
|
||||
.match(/(\.[a-z0-9]+)$/);
|
||||
|
||||
@@ -953,51 +894,17 @@ const getAttachmentPreviewKind = (attachment) => {
|
||||
return "office";
|
||||
}
|
||||
|
||||
const other = getAttachmentOtherPayload(attachment);
|
||||
if (typeof other === "string" && other.startsWith("http")) {
|
||||
if (String(attachment?.content?.other || "").startsWith("http")) {
|
||||
return "link";
|
||||
}
|
||||
|
||||
if (other) {
|
||||
if (attachment?.content?.other) {
|
||||
return "text";
|
||||
}
|
||||
|
||||
return "none";
|
||||
};
|
||||
|
||||
const formatAttachmentText = (attachment) => {
|
||||
const other = getAttachmentOtherPayload(attachment);
|
||||
if (isSelfServeWashAttachment(attachment)) {
|
||||
const parts = [
|
||||
t("admin.pos.settings_wheel.self_serve_wash_attachment"),
|
||||
other?.customer_number
|
||||
? `${t("admin.pos.settings_wheel.self_serve_customer")}: #${other.customer_number}`
|
||||
: null,
|
||||
other?.subuser?.name || other?.subuser?.username || other?.subuser_id
|
||||
? `${t("admin.pos.settings_wheel.self_serve_driver")}: ${other?.subuser?.name || other?.subuser?.username || `#${other.subuser_id}`}`
|
||||
: null,
|
||||
other?.license_plate
|
||||
? `${t("pos.license_plate")}: ${other.license_plate}`
|
||||
: null,
|
||||
other?.elapsed_wash_time_seconds
|
||||
? `${t("admin.pos.settings_wheel.self_serve_elapsed")}: ${Math.ceil(Number(other.elapsed_wash_time_seconds) / 60)} min`
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
if (typeof other === "string") {
|
||||
return other;
|
||||
}
|
||||
|
||||
if (other && typeof other === "object") {
|
||||
return JSON.stringify(other, null, 2);
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const getAttachmentPreviewPlaceholderIcon = (attachment) => {
|
||||
const previewKind = getAttachmentPreviewKind(attachment);
|
||||
|
||||
@@ -1058,10 +965,6 @@ const activeAttachmentPreviewSource = computed(() => {
|
||||
return previewSourcesById.value[activeAttachment.value.id] ?? null;
|
||||
});
|
||||
|
||||
const hasWashCertificateAttachment = computed(() =>
|
||||
attachmentsFromOrder.value.some((attachment) => isWashCertificateAttachment(attachment))
|
||||
);
|
||||
|
||||
const hasCachedPreviewSource = (attachmentId) => {
|
||||
return Object.prototype.hasOwnProperty.call(previewSourcesById.value, attachmentId);
|
||||
};
|
||||
@@ -1626,132 +1529,6 @@ const showCompleteOrderBookingConfirmation = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const showEmailNotificationActionResult = async (requestAction, successKey, errorKey) => {
|
||||
try {
|
||||
await requestAction();
|
||||
await Swal.fire({
|
||||
title: t(successKey),
|
||||
icon: "success",
|
||||
showConfirmButton: false,
|
||||
timer: 2000,
|
||||
heightAuto: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await Swal.fire({
|
||||
title: t("common.error"),
|
||||
text: [t(errorKey), SessionUser.functions.parseErrorMessage?.(error)].filter(Boolean).join(": "),
|
||||
icon: "error",
|
||||
heightAuto: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resendBookingConfirmation = () =>
|
||||
showEmailNotificationActionResult(
|
||||
() => SessionUser.objects.order_bookings.functions.resendBookingConfirmation(props.order_booking_id),
|
||||
"admin.pos.settings_wheel.resend_booking_confirmation_success",
|
||||
"admin.pos.settings_wheel.resend_booking_confirmation_error"
|
||||
);
|
||||
|
||||
const resendBookingCompletionConfirmation = () =>
|
||||
showEmailNotificationActionResult(
|
||||
() => SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation(props.order_booking_id),
|
||||
"admin.pos.settings_wheel.resend_booking_completion_confirmation_success",
|
||||
"admin.pos.settings_wheel.resend_booking_completion_confirmation_error"
|
||||
);
|
||||
|
||||
const resendWashCertificate = () =>
|
||||
showEmailNotificationActionResult(
|
||||
() => SessionUser.objects.orders.functions.resendWashCertificate(props.order_id),
|
||||
"admin.pos.settings_wheel.resend_wash_certificate_success",
|
||||
"admin.pos.settings_wheel.resend_wash_certificate_error"
|
||||
);
|
||||
|
||||
const getAvailableInvoiceCollectionsForCustomer = async (customerNumber) => {
|
||||
const normalizedCustomerNumber = normalizePositiveInteger(customerNumber);
|
||||
if (!normalizedCustomerNumber) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const response = await SessionUser.request("/collected-invoices", "GET", {
|
||||
page: 1,
|
||||
limit: 100,
|
||||
order: "closed_at:asc",
|
||||
filters: `customer_number:${normalizedCustomerNumber},booked_invoice_id:is_null`,
|
||||
});
|
||||
|
||||
return Array.isArray(response?.data?.data) ? response.data.data : [];
|
||||
};
|
||||
|
||||
const ensureOpenInvoiceCollectionForCustomer = async (customerNumber) => {
|
||||
const collections = await getAvailableInvoiceCollectionsForCustomer(customerNumber);
|
||||
const openCollection = collections.find((collection) => collection?.closed_at === null);
|
||||
const openCollectionId = normalizePositiveInteger(openCollection?.id);
|
||||
if (openCollectionId) {
|
||||
return openCollectionId;
|
||||
}
|
||||
|
||||
const response = await SessionUser.objects.collectedOrderInvoices.add(
|
||||
customerNumber,
|
||||
t("admin.pos.drafts_assignment.new_collection_name"),
|
||||
t("admin.pos.drafts_assignment.new_collection_description"),
|
||||
null
|
||||
);
|
||||
|
||||
return normalizePositiveInteger(response?.data?.data?.id ?? response?.data?.id);
|
||||
};
|
||||
|
||||
const acceptSelfServeWashDraft = async () => {
|
||||
const customerNumber = getSelfServeWashCustomerNumber.value;
|
||||
const orderId = normalizePositiveInteger(props.order_id);
|
||||
if (!customerNumber || !orderId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await Swal.fire({
|
||||
title: t("admin.pos.settings_wheel.accept_self_serve_wash"),
|
||||
text: t("admin.pos.settings_wheel.accept_self_serve_wash_confirm", { customerNumber }),
|
||||
icon: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: t("common.confirm"),
|
||||
cancelButtonText: t("common.cancel"),
|
||||
});
|
||||
|
||||
if (!result.isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const invoiceCollectionId = await ensureOpenInvoiceCollectionForCustomer(customerNumber);
|
||||
if (!invoiceCollectionId) {
|
||||
throw new Error(t("admin.pos.drafts_assignment.invoice_collection_empty"));
|
||||
}
|
||||
|
||||
await SessionUser.objects.orders.functions.assignDraftCustomer({
|
||||
order_id: orderId,
|
||||
customer_id: customerNumber,
|
||||
invoice_collection_id: invoiceCollectionId,
|
||||
department_id: normalizePositiveInteger(props.department_id),
|
||||
recalculate_prices: true,
|
||||
});
|
||||
|
||||
await props.refreshFunction();
|
||||
await Swal.fire({
|
||||
icon: "success",
|
||||
title: t("admin.pos.settings_wheel.accept_self_serve_wash_success"),
|
||||
timer: 1800,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
} catch (error) {
|
||||
await Swal.fire({
|
||||
icon: "error",
|
||||
title: t("admin.pos.drafts_assignment.error"),
|
||||
text: SessionUser.functions.parseErrorMessage(error) || t("admin.pos.drafts_assignment.error"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const flatBuiltInMenuSections = computed(() => {
|
||||
const sections = [];
|
||||
|
||||
@@ -1837,16 +1614,6 @@ const flatBuiltInMenuSections = computed(() => {
|
||||
? redirectDepartmentOrderPage(props.order_id, true)
|
||||
: SessionUser.functions.redirectTo.user("/orders/" + props.order_id, true),
|
||||
}),
|
||||
SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()
|
||||
? canAcceptSelfServeWashDraft.value
|
||||
? buildMenuAction("order-accept-self-serve-wash", {
|
||||
icon: "fas fa-check-circle",
|
||||
label: t("admin.pos.settings_wheel.accept_self_serve_wash"),
|
||||
template: "success",
|
||||
clickAction: acceptSelfServeWashDraft,
|
||||
})
|
||||
: null
|
||||
: null,
|
||||
SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()
|
||||
? buildMenuAction("order-attach-wash-certificate", {
|
||||
icon: "fas fa-paperclip",
|
||||
@@ -1894,40 +1661,6 @@ const flatBuiltInMenuSections = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
if (props.order_booking_id || hasWashCertificateAttachment.value) {
|
||||
const emailNotificationsSection = buildMenuSection(
|
||||
"email-notifications",
|
||||
t("admin.pos.settings_wheel.email_notifications_section"),
|
||||
[
|
||||
props.order_booking_id
|
||||
? buildMenuAction("email-notifications-resend-booking-confirmation", {
|
||||
icon: "fas fa-envelope",
|
||||
label: t("admin.pos.settings_wheel.resend_booking_confirmation"),
|
||||
clickAction: resendBookingConfirmation,
|
||||
})
|
||||
: null,
|
||||
props.order_booking_id && hasWashCertificateAttachment.value
|
||||
? buildMenuAction("email-notifications-resend-booking-completion-confirmation", {
|
||||
icon: "fas fa-envelope-open-text",
|
||||
label: t("admin.pos.settings_wheel.resend_booking_completion_confirmation"),
|
||||
clickAction: resendBookingCompletionConfirmation,
|
||||
})
|
||||
: null,
|
||||
!props.order_booking_id && hasWashCertificateAttachment.value
|
||||
? buildMenuAction("email-notifications-resend-wash-certificate", {
|
||||
icon: "fas fa-file-pdf",
|
||||
label: t("admin.pos.settings_wheel.resend_wash_certificate"),
|
||||
clickAction: resendWashCertificate,
|
||||
})
|
||||
: null,
|
||||
]
|
||||
);
|
||||
|
||||
if (emailNotificationsSection) {
|
||||
sections.push(emailNotificationsSection);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.invoice_collection_id && SessionUser.canAccessSuperUser()) {
|
||||
const invoiceCollectionLinkSection = buildMenuSection(
|
||||
"invoice-collection-link",
|
||||
@@ -2229,10 +1962,10 @@ const flatBuiltInMenuSections = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
if (props.reg_1 || props.reg_2 || props.reg_3) {
|
||||
if (props.reg_1 || props.reg_2) {
|
||||
const vehicleSection = buildMenuSection(
|
||||
"vehicle",
|
||||
[props.reg_1, props.reg_2, props.reg_3].filter(Boolean).length > 1
|
||||
props.reg_1 && props.reg_2
|
||||
? SessionUser.objects.vehicles.meta.labels.multiple
|
||||
: SessionUser.objects.vehicles.meta.labels.single,
|
||||
[
|
||||
@@ -2250,13 +1983,6 @@ const flatBuiltInMenuSections = computed(() => {
|
||||
clickAction: () => SessionUser.functions.redirectTo.superUser("/vehicles/" + props.reg_2, true),
|
||||
})
|
||||
: null,
|
||||
props.reg_3 && SessionUser.canAccessSuperUser()
|
||||
? buildMenuAction("vehicle-reg-3", {
|
||||
icon: "fas fa-car",
|
||||
label: t("admin.pos.settings_wheel.view_vehicle_new_tab", { reg: props.reg_3 }),
|
||||
clickAction: () => SessionUser.functions.redirectTo.superUser("/vehicles/" + props.reg_3, true),
|
||||
})
|
||||
: null,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2751,7 +2477,7 @@ const syncDesktopFlyoutPosition = () => {
|
||||
{{ t("admin.pos.attachments_office_preview_unavailable") }}
|
||||
</span>
|
||||
<span v-else-if="activeAttachmentPreviewKind === 'text'" class="action-settings-wheel-attachment-panel__text">
|
||||
{{ formatAttachmentText(activeAttachment) }}
|
||||
{{ activeAttachment.content?.other }}
|
||||
</span>
|
||||
<span v-else class="action-settings-wheel-attachment-panel__text">
|
||||
{{ t("admin.pos.attachments_no_preview") }}
|
||||
@@ -3141,7 +2867,6 @@ const syncDesktopFlyoutPosition = () => {
|
||||
text-align: center;
|
||||
color: #4a5568;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.action-settings-wheel-attachment-panel__link {
|
||||
|
||||
@@ -6,10 +6,6 @@ const props = defineProps({
|
||||
icon: String,
|
||||
label: String,
|
||||
disabled: Boolean,
|
||||
testId: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
template: String // The style of the button (default, danger, success, warning, info, light)
|
||||
});
|
||||
const emit = defineEmits(['selected']);
|
||||
@@ -135,7 +131,6 @@ const getLabelColor = () => {
|
||||
@click.stop.prevent="click"
|
||||
:class="{'is-disabled': isDisabled()}"
|
||||
:disabled="isDisabled()"
|
||||
:data-testid="props.testId || undefined"
|
||||
>
|
||||
<span class="icon">
|
||||
<i :class="getIcon() + ' ' + getIconColor()"></i>
|
||||
|
||||
@@ -415,7 +415,7 @@ const handleShortcutSelection = (event) => {
|
||||
:disabled="props.isDisabled || props.isReadonly"
|
||||
@change="handleShortcutSelection"
|
||||
>
|
||||
<option value="" disabled>Vælg periode</option>
|
||||
<option value="" disabled>Vaelg periode</option>
|
||||
<option v-for="shortcut in shortcuts" :key="shortcut.label" :value="shortcut.label">
|
||||
{{ shortcut.label }}
|
||||
</option>
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
<script setup>
|
||||
import { watch } from 'vue';
|
||||
import { useRouter } from "vue-router";
|
||||
import {
|
||||
clearActivePosOrderContext,
|
||||
getCurrentStep,
|
||||
setDepartment,
|
||||
getOrderId,
|
||||
setStep,
|
||||
setOrderId,
|
||||
searchAndSelectCustomer,
|
||||
loadOrderItems,
|
||||
} from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import { getCurrentStep, setDepartment, getOrderId, setStep, setOrderId, searchAndSelectCustomer, loadOrderItems } from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import PosDepartmentStep1 from "@/components/displays/department/pos/steps/PosDepartmentStep1.vue";
|
||||
import PosDepartmentStep2 from "@/components/displays/department/pos/steps/PosDepartmentStep2.vue";
|
||||
import PosDepartmentStep3 from "@/components/displays/department/pos/steps/PosDepartmentStep3.vue";
|
||||
@@ -50,33 +41,20 @@ const debugVehicles = () => {
|
||||
}
|
||||
);
|
||||
};
|
||||
setDepartment();
|
||||
|
||||
const routeStateHandlers = {
|
||||
watch(() => router.currentRoute.value.params.departmentId, (nextDepartmentId) => {
|
||||
if (nextDepartmentId) {
|
||||
setDepartment(nextDepartmentId);
|
||||
}
|
||||
});
|
||||
|
||||
applyPosRouteSearch(window.location.search, {
|
||||
setOrderId,
|
||||
loadOrderItems,
|
||||
setStep,
|
||||
searchAndSelectCustomer,
|
||||
clearActivePosOrderContext,
|
||||
resetMobilePos: () => pos.reset.pos(),
|
||||
};
|
||||
|
||||
const getRouteSearch = (route) => {
|
||||
const fullPath = route?.fullPath || "";
|
||||
const queryIndex = fullPath.indexOf("?");
|
||||
return queryIndex >= 0 ? fullPath.slice(queryIndex) : "";
|
||||
};
|
||||
|
||||
const applyCurrentPosRoute = () => {
|
||||
const currentRoute = router.currentRoute.value;
|
||||
if (currentRoute.params.departmentId) {
|
||||
setDepartment(currentRoute.params.departmentId);
|
||||
} else {
|
||||
setDepartment();
|
||||
}
|
||||
applyPosRouteSearch(getRouteSearch(currentRoute), routeStateHandlers);
|
||||
};
|
||||
|
||||
watch(() => router.currentRoute.value.fullPath, applyCurrentPosRoute, { immediate: true });
|
||||
});
|
||||
|
||||
/** Define the createOrder function */
|
||||
</script>
|
||||
|
||||
@@ -1260,12 +1260,7 @@ const formatCashierName = (order) => {
|
||||
v-bind:user_id="order.user_id"
|
||||
v-bind:order_id="order.id"
|
||||
v-bind:invoice_collection_id="order.invoice_collection_id"
|
||||
v-bind:customer_number="order.customer_id"
|
||||
v-bind:department_id="order.department_id"
|
||||
v-bind:reg_1="order.reg_1"
|
||||
v-bind:reg_2="order.reg_2"
|
||||
v-bind:reg_3="order.reg_3"
|
||||
v-bind:order_booking_id="order.booking_id"
|
||||
:refreshFunction="loadList"
|
||||
@deleted="loadList()"
|
||||
@flag-created="emitFlagCreated"
|
||||
@@ -1506,12 +1501,7 @@ const formatCashierName = (order) => {
|
||||
v-bind:user_id="order.user_id"
|
||||
v-bind:order_id="order.id"
|
||||
v-bind:invoice_collection_id="order.invoice_collection_id"
|
||||
v-bind:customer_number="order.customer_id"
|
||||
v-bind:department_id="order.department_id"
|
||||
v-bind:reg_1="order.reg_1"
|
||||
v-bind:reg_2="order.reg_2"
|
||||
v-bind:reg_3="order.reg_3"
|
||||
v-bind:order_booking_id="order.booking_id"
|
||||
:refreshFunction="loadList"
|
||||
@deleted="loadList()"
|
||||
@flag-created="emitFlagCreated"
|
||||
@@ -2002,7 +1992,6 @@ const formatCashierName = (order) => {
|
||||
v-bind:user_id="selectedOrderForActionsMenu.user_id"
|
||||
v-bind:order_id="selectedOrderForActionsMenu.id"
|
||||
v-bind:invoice_collection_id="selectedOrderForActionsMenu.invoice_collection_id"
|
||||
v-bind:customer_number="selectedOrderForActionsMenu.customer_id"
|
||||
v-bind:reg_1="selectedOrderForActionsMenu.reg_1"
|
||||
v-bind:reg_2="selectedOrderForActionsMenu.reg_2"
|
||||
v-bind:reg_3="selectedOrderForActionsMenu.reg_3"
|
||||
|
||||
@@ -297,7 +297,7 @@ const bookingSelectionObjects = computed(() => {
|
||||
const duplicateDetailsObjects = computed(() => {
|
||||
return duplicateOrders.value.map((order) => ({
|
||||
id: Number(order.id),
|
||||
label: `${t("admin.pos.order")} #${order.id} - ${formatDuplicateOrderDate(order.created_at)}`,
|
||||
label: `${t("common.order")} #${order.id} - ${formatDuplicateOrderDate(order.created_at)}`,
|
||||
content: getDuplicateOrderContent(order),
|
||||
buttons: [
|
||||
{
|
||||
|
||||
+5
-60
@@ -14,7 +14,7 @@ type attachment = {
|
||||
image: string | null;
|
||||
document: string | null;
|
||||
relation: string | null;
|
||||
other: unknown;
|
||||
other: string | null;
|
||||
src: string | null; // For document preview (e.g., PDF URL) // THIS IS NEVER STORED, JUST FOR PREVIEW PURPOSES
|
||||
};
|
||||
created_at: string;
|
||||
@@ -22,8 +22,6 @@ type attachment = {
|
||||
deleted_at: string | null;
|
||||
}
|
||||
|
||||
const SELF_SERVE_WASH_ATTACHMENT_TYPE = 'SELF_SERVE_WASH';
|
||||
|
||||
const getAttachmentContent = (attachmentEntry: attachment) => {
|
||||
if (!attachmentEntry.content) {
|
||||
return {
|
||||
@@ -45,19 +43,7 @@ const getAttachmentContent = (attachmentEntry: attachment) => {
|
||||
};
|
||||
|
||||
const getAttachmentOtherText = (attachmentEntry: attachment) => {
|
||||
const other = getAttachmentContent(attachmentEntry).other;
|
||||
if (typeof other === 'string') {
|
||||
return other;
|
||||
}
|
||||
|
||||
if (isSelfServeWashAttachment(attachmentEntry)) {
|
||||
const customerNumber = getSelfServeWashPayload(attachmentEntry)?.customer_number;
|
||||
return customerNumber
|
||||
? `${t('admin.pos.settings_wheel.self_serve_wash_attachment')} #${customerNumber}`
|
||||
: t('admin.pos.settings_wheel.self_serve_wash_attachment');
|
||||
}
|
||||
|
||||
return other && typeof other === 'object' ? JSON.stringify(other) : '';
|
||||
return getAttachmentContent(attachmentEntry).other || '';
|
||||
};
|
||||
const props = defineProps({
|
||||
attachments: {
|
||||
@@ -93,26 +79,6 @@ const determineAttachmentType = (attachment: attachment): 'image' | 'document' |
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
const getSelfServeWashPayload = (attachmentEntry: attachment): Record<string, any> | null => {
|
||||
const other = getAttachmentContent(attachmentEntry).other;
|
||||
return other && typeof other === 'object' && (other as Record<string, any>).type === SELF_SERVE_WASH_ATTACHMENT_TYPE
|
||||
? other as Record<string, any>
|
||||
: null;
|
||||
};
|
||||
|
||||
const isSelfServeWashAttachment = (attachmentEntry: attachment): boolean => {
|
||||
return getSelfServeWashPayload(attachmentEntry) !== null;
|
||||
};
|
||||
|
||||
const formatSelfServeDriver = (payload: Record<string, any>): string => {
|
||||
return payload.subuser?.name || payload.subuser?.username || (payload.subuser_id ? `#${payload.subuser_id}` : '-');
|
||||
};
|
||||
|
||||
const formatElapsedMinutes = (seconds: unknown): string => {
|
||||
const parsed = Number(seconds);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? `${Math.ceil(parsed / 60)} min` : '-';
|
||||
};
|
||||
|
||||
const getAttachmentTypeIcon = (attachment: attachment): string => {
|
||||
const type = determineAttachmentType(attachment);
|
||||
switch (type) {
|
||||
@@ -343,35 +309,14 @@ const onClickAttachWashCertificate = () => {
|
||||
</template>
|
||||
<!-- OTHER PREVIEW -->
|
||||
<template v-else-if="determineAttachmentType(attachment) === 'other' && getAttachmentContent(attachment).other">
|
||||
<template v-if="isSelfServeWashAttachment(attachment)">
|
||||
<div class="content is-size-7">
|
||||
<p class="has-text-weight-semibold">{{ t('admin.pos.settings_wheel.self_serve_wash_attachment') }}</p>
|
||||
<p>
|
||||
<strong>{{ t('admin.pos.settings_wheel.self_serve_customer') }}:</strong>
|
||||
#{{ getSelfServeWashPayload(attachment)?.customer_number || '-' }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ t('admin.pos.settings_wheel.self_serve_driver') }}:</strong>
|
||||
{{ formatSelfServeDriver(getSelfServeWashPayload(attachment) || {}) }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ t('pos.license_plate') }}:</strong>
|
||||
{{ getSelfServeWashPayload(attachment)?.license_plate || '-' }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ t('admin.pos.settings_wheel.self_serve_elapsed') }}:</strong>
|
||||
{{ formatElapsedMinutes(getSelfServeWashPayload(attachment)?.elapsed_wash_time_seconds) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<!-- If the other type is a URL, you can create a link -->
|
||||
<template v-else-if="typeof getAttachmentContent(attachment).other === 'string' && getAttachmentContent(attachment).other.startsWith('http')">
|
||||
<a :href="String(getAttachmentContent(attachment).other)" target="_blank" rel="noopener noreferrer">
|
||||
<template v-if="getAttachmentContent(attachment).other.startsWith('http')">
|
||||
<a :href="getAttachmentContent(attachment).other" target="_blank" rel="noopener noreferrer">
|
||||
{{ getAttachmentContent(attachment).other }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>{{ getAttachmentOtherText(attachment) }}</span>
|
||||
<span>{{ getAttachmentContent(attachment).other }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<!-- NO PREVIEW -->
|
||||
|
||||
+21
-14
@@ -1,27 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
reset_all_values,
|
||||
customer_name,
|
||||
nextStep,
|
||||
searchAndSelectCustomer,
|
||||
isCustomerSelected,
|
||||
order_id,
|
||||
getStoredPosOrderId,
|
||||
customer_id,
|
||||
step,
|
||||
reg_1,
|
||||
reg_2,
|
||||
reg_3,
|
||||
reference,
|
||||
order_notes,
|
||||
setDepartment,
|
||||
getDepartment,
|
||||
} from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
|
||||
import { resetPos } from "../objects/PosDepartmentStepMobileFlow.vue";
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
|
||||
const toPositiveInteger = (value: unknown) => {
|
||||
const parsedValue = Number.parseInt(String(value ?? ""), 10);
|
||||
return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null;
|
||||
};
|
||||
|
||||
const onClickClearAll = async () => {
|
||||
const storedOrderId = getStoredPosOrderId();
|
||||
const currentOrderId = toPositiveInteger(order_id.value);
|
||||
const activeDraftOrderId = storedOrderId && currentOrderId === storedOrderId ? storedOrderId : null;
|
||||
|
||||
// 1. Delete only the active mobile draft order, never a historical route-loaded order.
|
||||
if (activeDraftOrderId) {
|
||||
// 1. Delete the order (If any)
|
||||
// Check localstorage for pos_order_id
|
||||
if (localStorage.getItem("pos_order_id")) {
|
||||
order_id.value = parseInt(localStorage.getItem("pos_order_id") || "0");
|
||||
}
|
||||
if (order_id.value) {
|
||||
try {
|
||||
order_id.value = activeDraftOrderId;
|
||||
const deleted = await SessionUser.objects.orders.functions.deleteWithConfirmation(activeDraftOrderId);
|
||||
const deleted = await SessionUser.objects.orders.functions.deleteWithConfirmation(order_id.value);
|
||||
if (!deleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
+1
-7
@@ -9,9 +9,6 @@ const product = props.product;
|
||||
const note = ref(product.notes || "");
|
||||
watch(note, (newNote) => {
|
||||
product.notes = newNote;
|
||||
if (props) {
|
||||
props.validationMessage = "";
|
||||
}
|
||||
// If the note is empty, remove it from the product
|
||||
if (newNote === "") {
|
||||
delete product.notes;
|
||||
@@ -31,9 +28,6 @@ watch(note, (newNote) => {
|
||||
placeholder="Indtast note"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="props?.validationMessage" class="help is-danger mt-2">
|
||||
{{ props.validationMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -131,4 +125,4 @@ input.is-searched {
|
||||
flex-grow: 0;
|
||||
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
+1
-9
@@ -788,15 +788,7 @@ const promptForNotesIfRequired = (product: PosProduct, callback: (notes: string)
|
||||
label: "Bekræft",
|
||||
description: "Bekræft noten og fortsæt",
|
||||
onClick: () => {
|
||||
const activePopup = popups.get();
|
||||
const note = String(activePopup?.props?.product?.notes || "").trim();
|
||||
if (!note) {
|
||||
if (activePopup?.props) {
|
||||
activePopup.props.validationMessage = "Note er påkrævet for dette produkt";
|
||||
}
|
||||
return;
|
||||
}
|
||||
callback(note);
|
||||
callback(popups.get()?.props?.product?.notes || "");
|
||||
clearPopup();
|
||||
},
|
||||
color: "primary",
|
||||
|
||||
@@ -229,7 +229,7 @@ const clearAnswers = async () => {
|
||||
|
||||
const confirmation = await Swal.fire({
|
||||
title: "Ryd besvarelser?",
|
||||
text: `Registreringsnummer ${normalizedReg.value} på bane ${selectedLaneId.value} bliver ryddet.`,
|
||||
text: `Registreringsnummer ${normalizedReg.value} pa bane ${selectedLaneId.value} bliver ryddet.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Ja, ryd besvarelser",
|
||||
@@ -284,7 +284,7 @@ watch(dynamicImageUrl, () => {
|
||||
<div class="modal-background" @click="closeModal"></div>
|
||||
<div class="modal-card" style="width: 95%; max-width: 1200px;">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Forhåndsvisning af selvvask</p>
|
||||
<p class="modal-card-title">Self-serve preview</p>
|
||||
<button class="delete" aria-label="close" @click="closeModal"></button>
|
||||
</header>
|
||||
|
||||
@@ -301,7 +301,7 @@ watch(dynamicImageUrl, () => {
|
||||
<label class="label">Bane</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="selectedLaneId" data-testid="self-serve-try-lane">
|
||||
<option :value="null">Vælg bane</option>
|
||||
<option :value="null">Vaelg bane</option>
|
||||
<option v-for="entry in availableLanes" :key="entry.id" :value="entry.id">
|
||||
{{ entry.name }} (ID: {{ entry.id }})
|
||||
</option>
|
||||
@@ -313,7 +313,7 @@ watch(dynamicImageUrl, () => {
|
||||
<input class="input" :value="selectedLaneId" type="text" disabled>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<label class="label">Køretøjstype</label>
|
||||
<label class="label">Koretojstype</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="selectedVehicleTypeId" data-testid="self-serve-try-vehicle-type">
|
||||
<option :value="null">Auto (fra registreringsnummer)</option>
|
||||
@@ -325,7 +325,7 @@ watch(dynamicImageUrl, () => {
|
||||
</div>
|
||||
<div class="column is-3 is-flex is-align-items-flex-end">
|
||||
<button class="button is-link is-fullwidth" data-testid="self-serve-try-refresh" :class="{ 'is-loading': loading }" @click="refresh">
|
||||
Hent forhåndsvisning
|
||||
Hent preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -335,10 +335,10 @@ watch(dynamicImageUrl, () => {
|
||||
Tilladt: {{ allowed ? "Ja" : "Nej" }}
|
||||
</span>
|
||||
<span class="tag" :class="machineAvailable ? 'is-success' : 'is-light'">
|
||||
Maskine tilgængelig: {{ machineAvailable ? "Ja" : "Nej" }}
|
||||
Maskine tilgaengelig: {{ machineAvailable ? "Ja" : "Nej" }}
|
||||
</span>
|
||||
<span class="tag" :class="allDisplayQuestionsAnswered ? 'is-success' : 'is-warning'">
|
||||
Alle synlige spørgsmål besvaret: {{ allDisplayQuestionsAnswered ? "Ja" : "Nej" }}
|
||||
Alle synlige sporgsmal besvaret: {{ allDisplayQuestionsAnswered ? "Ja" : "Nej" }}
|
||||
</span>
|
||||
<span v-if="session" class="tag is-info">
|
||||
Session: {{ session.status }} (#{{ session.id }})
|
||||
@@ -347,10 +347,10 @@ watch(dynamicImageUrl, () => {
|
||||
Maskintype: {{ machineType.name }}
|
||||
</span>
|
||||
<span v-if="lane" class="tag is-light">
|
||||
Bane: {{ lane.name || lane.id }}
|
||||
Lane: {{ lane.name || lane.id }}
|
||||
</span>
|
||||
<span v-if="configVersionId" class="tag is-dark">
|
||||
Konfigurationsversion: #{{ configVersionId }}
|
||||
Config version: #{{ configVersionId }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -364,10 +364,10 @@ watch(dynamicImageUrl, () => {
|
||||
|
||||
<div class="column is-6">
|
||||
<div class="box" style="height: 100%">
|
||||
<h4 class="title is-5">Spørgsmål</h4>
|
||||
<h4 class="title is-5">Sporgsmal</h4>
|
||||
|
||||
<div v-if="displayQuestions.length === 0" class="notification is-success is-light">
|
||||
<p>Ingen synlige spørgsmål for denne forhåndsvisning.</p>
|
||||
<p>Ingen synlige sporgsmal for denne preview.</p>
|
||||
</div>
|
||||
|
||||
<SelfServeQuestionCards
|
||||
@@ -375,18 +375,18 @@ watch(dynamicImageUrl, () => {
|
||||
:answers="answers"
|
||||
@answer-question="submitAnswer"
|
||||
/>
|
||||
<p v-if="questions.length === 0" class="has-text-centered is-italic">Ingen spørgsmål fundet.</p>
|
||||
<p v-if="questions.length === 0" class="has-text-centered is-italic">Ingen sporgsmal fundet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6">
|
||||
<div class="box" style="height: 100%">
|
||||
<h4 class="title is-5">Opgaver og session</h4>
|
||||
<h4 class="title is-5">Tasks og session</h4>
|
||||
|
||||
<div v-if="displayedDynamicImageUrl" class="mb-4">
|
||||
<img
|
||||
:src="displayedDynamicImageUrl"
|
||||
alt="Forhåndsvisning af maskinstatus"
|
||||
alt="Machine status preview"
|
||||
data-testid="self-serve-try-dynamic-image"
|
||||
style="max-width: 100%; height: auto; border-radius: 4px; display: block; margin-left: auto; margin-right: auto;"
|
||||
@error="onDynamicImageError"
|
||||
@@ -401,11 +401,11 @@ watch(dynamicImageUrl, () => {
|
||||
@download-attachment="downloadAttachment"
|
||||
/>
|
||||
|
||||
<p v-if="activeTasks.length === 0" class="is-italic">Ingen aktive opgaver.</p>
|
||||
<p v-if="activeTasks.length === 0" class="is-italic">Ingen aktive tasks.</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5 class="subtitle is-6">Seneste hændelser</h5>
|
||||
<h5 class="subtitle is-6">Seneste haendelser</h5>
|
||||
<ul>
|
||||
<li v-for="event in events" :key="event.id" class="mb-2">
|
||||
<strong>{{ event.type }}</strong>
|
||||
@@ -416,16 +416,16 @@ watch(dynamicImageUrl, () => {
|
||||
|
||||
<hr />
|
||||
|
||||
<h5 class="subtitle is-6">Evalueringsspor</h5>
|
||||
<h5 class="subtitle is-6">Evaluation trace</h5>
|
||||
<div v-if="evaluationTrace" class="content is-small">
|
||||
<pre>{{ JSON.stringify(evaluationTrace, null, 2) }}</pre>
|
||||
</div>
|
||||
<p v-else class="is-italic">Ingen sporingsdata returneret.</p>
|
||||
<p v-else class="is-italic">Ingen trace-data returneret.</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="is-flex is-align-items-center is-justify-content-space-between mb-2">
|
||||
<h5 class="subtitle is-6 mb-0">Besvarede spørgsmål</h5>
|
||||
<h5 class="subtitle is-6 mb-0">Besvarede sporgsmal</h5>
|
||||
<button
|
||||
class="button is-small is-light"
|
||||
data-testid="self-serve-try-clear-answers"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed, inject, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import * as paginatedListModule from "@/components/pagination/paginatedList.vue";
|
||||
const paginatedList = inject(paginatedListModule.PaginatedListKey, paginatedListModule);
|
||||
const { search, metaSearch, isLoading, loadList } = paginatedList;
|
||||
@@ -13,15 +13,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const canExport = computed(() => typeof paginatedList?.exportToExcel === "function");
|
||||
const canExport = computed(() => typeof paginatedList?.exportToExcel === 'function');
|
||||
const isExporting = computed(() => Boolean(paginatedList?.isExporting?.value));
|
||||
const resolvedSearchPlaceholder = computed(() => props.searchPlaceholder || t("global.search_placeholder"));
|
||||
const isActionDropdownOpen = ref(false);
|
||||
const actionDropdownRef = ref<HTMLElement | null>(null);
|
||||
|
||||
@@ -66,11 +61,11 @@ const handleDocumentClick = (event: MouseEvent) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", handleDocumentClick);
|
||||
document.addEventListener('click', handleDocumentClick);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("click", handleDocumentClick);
|
||||
document.removeEventListener('click', handleDocumentClick);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -79,12 +74,12 @@ onBeforeUnmount(() => {
|
||||
<div class="columns is-vcentered is-multiline pagination-general-search-reload">
|
||||
<div v-if="!props.hideSearch" class="column pagination-general-search-reload__search-column">
|
||||
<input
|
||||
data-testid="pagination-search-input"
|
||||
class="input"
|
||||
type="text"
|
||||
:placeholder="resolvedSearchPlaceholder"
|
||||
v-model="metaSearch"
|
||||
@input="search($event.target.value)"
|
||||
data-testid="pagination-search-input"
|
||||
class="input"
|
||||
type="text"
|
||||
:placeholder="$t('global.search_placeholder')"
|
||||
v-model="metaSearch"
|
||||
@input="search($event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-narrow pagination-general-search-reload__buttons-column" v-if="$slots.buttons">
|
||||
@@ -139,7 +134,7 @@ onBeforeUnmount(() => {
|
||||
@click.prevent="handleExportExcel"
|
||||
>
|
||||
<i class="fas fa-file-excel" aria-hidden="true"></i>
|
||||
<span>{{ t("pagination.download_excel") }}</span>
|
||||
<span>{{ t('pagination.download_excel') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<script setup>
|
||||
import { ref, inject } from "vue";
|
||||
import { ref, inject } from 'vue';
|
||||
import * as paginatedListModule from "@/components/pagination/paginatedList.vue";
|
||||
|
||||
const paginatedList = inject(paginatedListModule.PaginatedListKey, paginatedListModule);
|
||||
const { isLoading, loadList, metaCurrentPage, metaItemsPerPage, metaTotalItems, setMetaItemsPerPage, setPage } =
|
||||
paginatedList;
|
||||
const {
|
||||
isLoading,
|
||||
loadList,
|
||||
metaCurrentPage,
|
||||
metaItemsPerPage,
|
||||
metaTotalItems,
|
||||
setMetaItemsPerPage,
|
||||
setPage,
|
||||
} = paginatedList;
|
||||
|
||||
import PaginationDisplayGeneralSearchReload from "@/components/displays/pagination/PaginationDisplayGeneralSearchReload.vue";
|
||||
import PaginationDisplayGeneralSearchReload
|
||||
from "@/components/displays/pagination/PaginationDisplayGeneralSearchReload.vue";
|
||||
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||
|
||||
@@ -23,10 +31,6 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
hidePagination: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -44,7 +48,6 @@ const isSmall = ref(window.innerWidth < 1024);
|
||||
<PaginationDisplayGeneralSearchReload
|
||||
v-if="!props.hideSearch || $slots.buttons"
|
||||
:hide-search="props.hideSearch"
|
||||
:search-placeholder="props.searchPlaceholder"
|
||||
>
|
||||
<template #buttons="{ loadList }" v-if="$slots.buttons">
|
||||
<slot name="buttons" :loadList="loadList"></slot>
|
||||
@@ -63,7 +66,7 @@ const isSmall = ref(window.innerWidth < 1024);
|
||||
<template #paginationColumns>
|
||||
<slot name="paginationDisplayFiltersElement"></slot>
|
||||
<slot name="leftPaginationColumns"></slot>
|
||||
<div class="column is-auto-fill my-3" v-if="!isSmall" />
|
||||
<div class="column is-auto-fill my-3" v-if="!isSmall"/>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12 p-1 m-0"></div>
|
||||
<slot name="rightPaginationColumns"></slot>
|
||||
@@ -86,4 +89,5 @@ const isSmall = ref(window.innerWidth < 1024);
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
+46
-29
@@ -1,30 +1,30 @@
|
||||
<script setup>
|
||||
import { provide, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import TableLabeledPagination from "@/components/displays/pagination/TableLabeledPagination.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 CustomerComplaintsTable from "@/components/displays/superuser/tables/customerComplaintsTable.vue";
|
||||
import { PaginatedListKey, usePaginatedList } from "@/components/pagination/paginatedList.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
|
||||
const props = defineProps({
|
||||
hideSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
autoLoad: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const props = defineProps(["hideSearch", "autoLoad"]);
|
||||
const { t } = useI18n();
|
||||
|
||||
const paginatedList = usePaginatedList();
|
||||
provide(PaginatedListKey, paginatedList);
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
list,
|
||||
loadList,
|
||||
metaCurrentPage,
|
||||
metaItemsPerPage,
|
||||
metaTotalItems,
|
||||
setEndpoint,
|
||||
setMetaItemsPerPage,
|
||||
setPage,
|
||||
search,
|
||||
setFilter,
|
||||
setOrder,
|
||||
hideSearchField,
|
||||
@@ -54,11 +54,6 @@ const onDepartmentFilterChange = (value) => {
|
||||
setFilter("department_id", parsedDepartmentId, true);
|
||||
};
|
||||
|
||||
const onOrderDirectionChange = (value) => {
|
||||
setOrder("created_at", value);
|
||||
loadList();
|
||||
};
|
||||
|
||||
setEndpoint("/departments/daily-reports/complaints", false);
|
||||
setOrder("created_at", "desc");
|
||||
|
||||
@@ -74,15 +69,22 @@ if (props.autoLoad) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableLabeledPagination
|
||||
:label="t('superuser.pages.complaints.title')"
|
||||
:hide-search="hideSearchField"
|
||||
>
|
||||
<template #description>
|
||||
<p class="is-size-6 mb-4">{{ t('superuser.pages.complaints.subtitle') }}</p>
|
||||
</template>
|
||||
<input
|
||||
v-if="!hideSearchField"
|
||||
class="input"
|
||||
data-testid="superuser-complaints-search"
|
||||
type="text"
|
||||
:placeholder="t('superuser.pages.complaints.search_placeholder')"
|
||||
@input="search($event.target.value)"
|
||||
/>
|
||||
|
||||
<template #paginationDisplayFiltersElement>
|
||||
<PaginationDisplay
|
||||
:metaItemsPerPage="metaItemsPerPage"
|
||||
:loadFunction="loadList"
|
||||
:isLoading="isLoading"
|
||||
:setMetaItemsPerPage="setMetaItemsPerPage"
|
||||
>
|
||||
<template #paginationColumns>
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label is-small">{{ t('superuser.pages.complaints.department_filter') }}</label>
|
||||
<div class="control">
|
||||
@@ -110,7 +112,7 @@ if (props.autoLoad) {
|
||||
<div class="select">
|
||||
<select
|
||||
data-testid="superuser-complaints-order-direction"
|
||||
@change="onOrderDirectionChange($event.target.value)"
|
||||
@change="setOrder('created_at', $event.target.value); loadList();"
|
||||
>
|
||||
<option value="desc" selected>{{ t('pagination.descending') }}</option>
|
||||
<option value="asc">{{ t('pagination.ascending') }}</option>
|
||||
@@ -119,11 +121,26 @@ if (props.autoLoad) {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</PaginationDisplay>
|
||||
|
||||
<template #default>
|
||||
<CustomerComplaintsTable :objects="list" />
|
||||
</template>
|
||||
</TableLabeledPagination>
|
||||
<CustomerComplaintsTable :objects="list" />
|
||||
|
||||
<PaginationNavigation
|
||||
:currentPage="metaCurrentPage"
|
||||
:totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)"
|
||||
:loadFunction="loadList"
|
||||
:setPage="setPage"
|
||||
:isLoading="isLoading"
|
||||
/>
|
||||
|
||||
<LoadButtonWhileAwait
|
||||
class="is-dark"
|
||||
:isLoading="isLoading"
|
||||
:loadFunction="loadList"
|
||||
icon="fas fa-sync-alt"
|
||||
>
|
||||
{{ t('pagination.reload') }}
|
||||
</LoadButtonWhileAwait>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+46
-20
@@ -1,27 +1,45 @@
|
||||
<script setup>
|
||||
let props = defineProps(["setCustomerFilter", "hideSearch", "autoLoad"]);
|
||||
|
||||
import { useRouter } from "vue-router";
|
||||
import {
|
||||
usePaginatedList,
|
||||
PaginatedListKey
|
||||
} from "@/components/pagination/paginatedList.vue";
|
||||
import { provide } from "vue";
|
||||
import CustomersTable from "@/components/displays/superuser/tables/customersTable.vue";
|
||||
import TableLabeledPagination from "@/components/displays/pagination/TableLabeledPagination.vue";
|
||||
import { PaginatedListKey, usePaginatedList } from "@/components/pagination/paginatedList.vue";
|
||||
|
||||
const props = defineProps({
|
||||
hideSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
autoLoad: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const paginatedList = usePaginatedList();
|
||||
provide(PaginatedListKey, paginatedList);
|
||||
const {
|
||||
isLoaded,
|
||||
isLoading,
|
||||
list,
|
||||
loadList,
|
||||
metaCurrentPage,
|
||||
metaItemsPerPage,
|
||||
metaTotalItems,
|
||||
setEndpoint,
|
||||
setMetaItemsPerPage,
|
||||
setPage,
|
||||
metaSearch,
|
||||
search,
|
||||
setFilter,
|
||||
setOrder,
|
||||
hideSearchField,
|
||||
setHideSearchField,
|
||||
} = paginatedList;
|
||||
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||
|
||||
const { list, loadList, setEndpoint, hideSearchField, setHideSearchField } = paginatedList;
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||
import CustomersTable from "@/components/displays/superuser/tables/customersTable.vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter();
|
||||
setEndpoint("/customers", false);
|
||||
|
||||
// Hide the search field, if the hideSearch prop is set
|
||||
if (props.hideSearch) {
|
||||
setHideSearchField(true);
|
||||
}
|
||||
@@ -29,12 +47,20 @@ if (props.hideSearch) {
|
||||
if (props.autoLoad) {
|
||||
loadList();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableLabeledPagination :label="$t('customers.title')" :hide-search="hideSearchField">
|
||||
<CustomersTable :objects="list" />
|
||||
</TableLabeledPagination>
|
||||
<input @input="search($event.target.value)" class="input" type="text" :placeholder="$t('global.search_customers')" v-if="!hideSearchField"/>
|
||||
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||
<template #paginationColumns>
|
||||
</template>
|
||||
</PaginationDisplay>
|
||||
<CustomersTable :objects="list" />
|
||||
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">{{ t('pagination.reload') }}</LoadButtonWhileAwait>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
+33
-7
@@ -3,7 +3,9 @@ import { computed, provide, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
import DepartmentsTable from "@/components/displays/superuser/tables/departmentsTable.vue";
|
||||
import TableLabeledPagination from "@/components/displays/pagination/TableLabeledPagination.vue";
|
||||
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||
import PaginationDisplayGeneralSearchReload from "@/components/displays/pagination/PaginationDisplayGeneralSearchReload.vue";
|
||||
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||
import { PaginatedListKey, usePaginatedList } from "@/components/pagination/paginatedList.vue";
|
||||
|
||||
const props = defineProps(["setCustomerFilter", "hideSearch", "autoLoad"]);
|
||||
@@ -12,9 +14,15 @@ const paginatedList = usePaginatedList();
|
||||
provide(PaginatedListKey, paginatedList);
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
list,
|
||||
loadList,
|
||||
metaCurrentPage,
|
||||
metaItemsPerPage,
|
||||
metaTotalItems,
|
||||
setEndpoint,
|
||||
setMetaItemsPerPage,
|
||||
setPage,
|
||||
setFilter,
|
||||
setOrder,
|
||||
hideSearchField,
|
||||
@@ -46,11 +54,14 @@ if (props.autoLoad) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableLabeledPagination
|
||||
:label="t('common.departments')"
|
||||
:hide-search="hideSearchField"
|
||||
<PaginationDisplayGeneralSearchReload v-if="!hideSearchField" />
|
||||
<PaginationDisplay
|
||||
:metaItemsPerPage="metaItemsPerPage"
|
||||
:loadFunction="loadList"
|
||||
:isLoading="isLoading"
|
||||
:setMetaItemsPerPage="setMetaItemsPerPage"
|
||||
>
|
||||
<template #paginationDisplayFiltersElement>
|
||||
<template #paginationColumns>
|
||||
<div class="column is-narrow my-3 department-status-filter">
|
||||
<label class="label is-small" for="department-archive-filter">{{
|
||||
t("common.status")
|
||||
@@ -70,8 +81,23 @@ if (props.autoLoad) {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<DepartmentsTable :objects="sortedList" />
|
||||
</TableLabeledPagination>
|
||||
</PaginationDisplay>
|
||||
<div v-if="hideSearchField" class="mb-3">
|
||||
<button class="button is-dark" :class="{ 'is-loading': isLoading }" :disabled="isLoading" @click="loadList">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-sync-alt" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>{{ t("pagination.reload") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<DepartmentsTable :objects="sortedList" />
|
||||
<PaginationNavigation
|
||||
:currentPage="metaCurrentPage"
|
||||
:totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)"
|
||||
:loadFunction="loadList"
|
||||
:setPage="setPage"
|
||||
:isLoading="isLoading"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+3
-20
@@ -7,24 +7,7 @@ import PaginationDisplay from "@/components/displays/pagination/PaginationDispla
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import { PaginatedListKey, usePaginatedList } from "@/components/pagination/paginatedList.vue";
|
||||
|
||||
const props = defineProps({
|
||||
hideSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
autoLoad: {
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
endpoint: {
|
||||
type: String,
|
||||
default: "/subusers",
|
||||
},
|
||||
showCustomer: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const props = defineProps(["hideSearch", "autoLoad"]);
|
||||
const { t } = useI18n();
|
||||
|
||||
const paginatedList = usePaginatedList();
|
||||
@@ -47,7 +30,7 @@ const {
|
||||
setAdditionalQueryParameters,
|
||||
} = paginatedList;
|
||||
|
||||
setEndpoint(props.endpoint, false);
|
||||
setEndpoint("/subusers", false);
|
||||
setAdditionalQueryParameters({ include_non_enabled: true });
|
||||
setOrder("created_at", "desc");
|
||||
|
||||
@@ -99,7 +82,7 @@ if (props.autoLoad) {
|
||||
</template>
|
||||
</PaginationDisplay>
|
||||
|
||||
<SubusersTable :objects="list" :show-customer="showCustomer" />
|
||||
<SubusersTable :objects="list" />
|
||||
|
||||
<PaginationNavigation
|
||||
:currentPage="metaCurrentPage"
|
||||
|
||||
@@ -1,21 +1,45 @@
|
||||
<script setup>
|
||||
import { provide } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
import TableLabeledPagination from "@/components/displays/pagination/TableLabeledPagination.vue";
|
||||
import UsersTable from "@/components/displays/superuser/tables/usersTable.vue";
|
||||
import { PaginatedListKey, usePaginatedList } from "@/components/pagination/paginatedList.vue";
|
||||
|
||||
const props = defineProps(["setCustomerFilter", "hideSearch", "autoLoad"]);
|
||||
let props = defineProps(["setCustomerFilter", "hideSearch", "autoLoad"]);
|
||||
|
||||
import { useRouter } from "vue-router";
|
||||
import {
|
||||
usePaginatedList,
|
||||
PaginatedListKey
|
||||
} from "@/components/pagination/paginatedList.vue";
|
||||
import { provide } from "vue";
|
||||
const paginatedList = usePaginatedList();
|
||||
provide(PaginatedListKey, paginatedList);
|
||||
const { list, loadList, setEndpoint, setFilter, setOrder, hideSearchField, setHideSearchField } = paginatedList;
|
||||
const {
|
||||
isLoaded,
|
||||
isLoading,
|
||||
list,
|
||||
loadList,
|
||||
metaCurrentPage,
|
||||
metaItemsPerPage,
|
||||
metaTotalItems,
|
||||
setEndpoint,
|
||||
setMetaItemsPerPage,
|
||||
setPage,
|
||||
metaSearch,
|
||||
search,
|
||||
setFilter,
|
||||
setOrder,
|
||||
hideSearchField,
|
||||
setHideSearchField,
|
||||
} = paginatedList;
|
||||
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter();
|
||||
setEndpoint("/users", false);
|
||||
setFilter("customer_number", 0, false);
|
||||
setOrder("created_at", "desc");
|
||||
|
||||
// Hide the search field, if the hideSearch prop is set
|
||||
if (props.hideSearch) {
|
||||
@@ -25,35 +49,32 @@ if (props.hideSearch) {
|
||||
if (props.autoLoad) {
|
||||
loadList();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableLabeledPagination
|
||||
:label="t('superuser.pages.employees.title')"
|
||||
:hide-search="hideSearchField"
|
||||
:search-placeholder="t('global.search_user')"
|
||||
>
|
||||
<template #paginationDisplayFiltersElement>
|
||||
<input @input="search($event.target.value)" class="input" type="text" :placeholder="$t('global.search_user')" v-if="!hideSearchField"/>
|
||||
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||
<template #paginationColumns>
|
||||
<!-- Sort by created_at -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label is-small">{{ t("pagination.order_direction") }}</label>
|
||||
<label class="label is-small">{{ t('pagination.order_direction') }}</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select
|
||||
@change="
|
||||
setOrder('created_at', $event.target.value);
|
||||
loadList();
|
||||
"
|
||||
>
|
||||
<option value="asc">{{ t("pagination.ascending") }}</option>
|
||||
<option value="desc" selected>{{ t("pagination.descending") }}</option>
|
||||
<select @change="setOrder('created_at', $event.target.value); loadList();">
|
||||
<option value="asc">{{ t('pagination.ascending') }}</option>
|
||||
<option value="desc" selected>{{ t('pagination.descending') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<UsersTable :objects="list" />
|
||||
</TableLabeledPagination>
|
||||
</PaginationDisplay>
|
||||
<UsersTable :objects="list" />
|
||||
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">{{ t('pagination.reload') }}</LoadButtonWhileAwait>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
+117
-226
@@ -1,21 +1,25 @@
|
||||
<script setup>
|
||||
import { list, loadList, setEndpoint, setFilter, setOrder } from "@/components/pagination/paginatedList.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import {
|
||||
list,
|
||||
loadList,
|
||||
setEndpoint,
|
||||
setFilter,
|
||||
setOrder
|
||||
} from "@/components/pagination/paginatedList.vue";
|
||||
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import TableLabeledPagination from "@/components/displays/pagination/TableLabeledPagination.vue";
|
||||
import OrderBookingsTable from "@/views/dashboards/userDashboard/bookings/displays/tables/OrderBookingsTable.vue";
|
||||
import { departments, getDepartments } from "@/components/pagination/departmentTabs.vue";
|
||||
import { Colors } from "@/ThemeConfig.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
/**
|
||||
* Router
|
||||
*/
|
||||
const router = useRouter();
|
||||
const isUserRoute = computed(() => router.currentRoute.value.path.startsWith("/user"));
|
||||
const isAdminRoute = computed(() => router.currentRoute.value.path.startsWith("/admin"));
|
||||
/**
|
||||
* Props
|
||||
*/
|
||||
@@ -23,40 +27,41 @@ const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
required: false,
|
||||
required: false
|
||||
},
|
||||
});
|
||||
const orderIdFilter = ref("*");
|
||||
})
|
||||
const orderIdFilter = ref('*');
|
||||
const onOrderIdFilterChange = (event) => {
|
||||
const val = event.target.value;
|
||||
orderIdFilter.value = val;
|
||||
// Apply filter: '*' clears, 'is null' shows without order, 'not null' shows with order
|
||||
setFilter("order_id", val);
|
||||
setFilter('order_id', val);
|
||||
|
||||
};
|
||||
|
||||
// Department filter
|
||||
const departmentFilter = ref("*");
|
||||
const departmentFilter = ref('*');
|
||||
const onDepartmentFilterChange = (event) => {
|
||||
const val = event.target.value;
|
||||
departmentFilter.value = val;
|
||||
setFilter("department", val);
|
||||
setFilter('department', val);
|
||||
};
|
||||
|
||||
// Only today filter
|
||||
const onlyTodayFilter = ref("*");
|
||||
const onlyTodayFilter = ref('*');
|
||||
const onOnlyTodayFilterChange = (event, autoLoadList = true) => {
|
||||
const val = event.target.value;
|
||||
onlyTodayFilter.value = val;
|
||||
if (val === "*") {
|
||||
setFilter("datetime", val, false); // Clear filter
|
||||
setFilter("datetime-date_from", null, false);
|
||||
setFilter("datetime-date_to", null, false);
|
||||
if (val === '*') {
|
||||
setFilter('datetime', val, false); // Clear filter
|
||||
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', new Date(startOfDay).toISOString(), false);
|
||||
setFilter('datetime-date_to', new Date(endOfDay).toISOString(), false);
|
||||
}
|
||||
if (autoLoadList) {
|
||||
loadList();
|
||||
@@ -64,38 +69,34 @@ const onOnlyTodayFilterChange = (event, autoLoadList = true) => {
|
||||
};
|
||||
|
||||
// Version selector + helpers
|
||||
const versionSelector = ref("new");
|
||||
const versionSelector = ref('new');
|
||||
const resetVersionToNew = () => {
|
||||
versionSelector.value = "new";
|
||||
versionSelector.value = 'new';
|
||||
};
|
||||
const showLegacyOrderBookingsPortal = () => {
|
||||
const departmentId = SessionUser.functions.getDepartmentIdFromUrl();
|
||||
if (!departmentId) {
|
||||
SessionUser.functions.redirectTo.user("/bookings-legacy", true);
|
||||
SessionUser.functions.redirectTo.user('/bookings-legacy', true);
|
||||
return;
|
||||
}
|
||||
SessionUser.functions.redirectTo.department(
|
||||
SessionUser.functions.getDepartmentIdFromUrl(),
|
||||
"modules/bookings-legacy",
|
||||
true
|
||||
);
|
||||
SessionUser.functions.redirectTo.department(SessionUser.functions.getDepartmentIdFromUrl(), 'modules/bookings-legacy', true);
|
||||
};
|
||||
|
||||
// Filters
|
||||
onMounted(() => {
|
||||
setEndpoint(SessionUser.objects.order_bookings.meta.endpoint, false);
|
||||
setOrder("datetime", "desc", false);
|
||||
setOrder('datetime', 'desc', false);
|
||||
// Apply initial filters from props
|
||||
for (const [key, value] of Object.entries(props.filters)) {
|
||||
let setFilterKey = true;
|
||||
// Also set the filter controls if applicable
|
||||
if (key === "order_id") {
|
||||
if (key === 'order_id') {
|
||||
orderIdFilter.value = value;
|
||||
} else if (key === "department") {
|
||||
} else if (key === 'department') {
|
||||
departmentFilter.value = value;
|
||||
} else if (key === "only_today" && value === true) {
|
||||
} 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: new Date().toISOString().split('T')[0] } }, false);
|
||||
}
|
||||
if (setFilterKey) {
|
||||
setFilter(key, value, false);
|
||||
@@ -105,218 +106,108 @@ onMounted(() => {
|
||||
// Load departments for filter options
|
||||
getDepartments().catch(() => {});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="order-bookings-pagination">
|
||||
<TableLabeledPagination :label="t('pagination.bookings_overview')" class="order-bookings-pagination__table">
|
||||
<template #paginationDisplayFiltersElement>
|
||||
<!-- Status filter -->
|
||||
<div class="column is-narrow order-bookings-pagination__filter">
|
||||
<div class="field mb-0">
|
||||
<label class="label is-small">{{ t("common.status") }}</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select :value="orderIdFilter" @change="onOrderIdFilterChange">
|
||||
<option value="*">{{ t("common.all") }}</option>
|
||||
<option value="is null">{{ t("pagination.not_completed") }}</option>
|
||||
<option value="not null">{{ t("pagination.completed") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<TableLabeledPagination :label="t('pagination.bookings_overview')">
|
||||
<template #paginationDisplayFiltersElement>
|
||||
<!-- Status filter -->
|
||||
<div class="column is-narrow">
|
||||
<div class="field mb-0">
|
||||
<label class="label is-small">{{ t('common.status') }}</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select :value="orderIdFilter" @change="onOrderIdFilterChange">
|
||||
<option value="*">{{ t('common.all') }}</option>
|
||||
<option value="is null">{{ t('pagination.not_completed') }}</option>
|
||||
<option value="not null">{{ t('pagination.completed') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Department filter -->
|
||||
<div class="column is-narrow order-bookings-pagination__filter">
|
||||
<div class="field mb-0">
|
||||
<label class="label is-small">{{ t("pagination.department") }}</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select :value="departmentFilter" @change="onDepartmentFilterChange">
|
||||
<option value="*">{{ t("common.all") }}</option>
|
||||
<option v-for="department in departments" :key="department.id" :value="department.id">
|
||||
{{ department.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Department filter -->
|
||||
<div class="column is-narrow">
|
||||
<div class="field mb-0">
|
||||
<label class="label is-small">{{ t('pagination.department') }}</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select :value="departmentFilter" @change="onDepartmentFilterChange">
|
||||
<option value="*">{{ t('common.all') }}</option>
|
||||
<option v-for="department in departments" :key="department.id" :value="department.id">{{ department.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Version selector -->
|
||||
<div class="column is-narrow order-bookings-pagination__filter">
|
||||
<div class="field mb-0">
|
||||
<label class="label is-small">{{ t("pagination.version") }}</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select
|
||||
@change="
|
||||
showLegacyOrderBookingsPortal();
|
||||
resetVersionToNew();
|
||||
"
|
||||
v-model="versionSelector"
|
||||
>
|
||||
<option value="new">{{ t("common.new") }}</option>
|
||||
<option value="legacy">{{ t("pagination.old") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Version selector -->
|
||||
<div class="column is-narrow">
|
||||
<div class="field mb-0">
|
||||
<label class="label is-small">{{ t('pagination.version') }}</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select @change="showLegacyOrderBookingsPortal(); resetVersionToNew()" v-model="versionSelector">
|
||||
<option value="new">{{ t('common.new') }}</option>
|
||||
<option value="legacy">{{ t('pagination.old') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Only today filter -->
|
||||
<div class="column is-narrow order-bookings-pagination__filter" v-if="!isUserRoute">
|
||||
<div class="field mb-0">
|
||||
<label class="label is-small">{{ t("pagination.only_today") }}</label>
|
||||
<div class="control">
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Only today filter -->
|
||||
<div class="column is-narrow">
|
||||
<div class="field mb-0">
|
||||
<label class="label is-small">{{ t('pagination.only_today') }}</label>
|
||||
<div class="control">
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #leftPaginationColumns> </template>
|
||||
<template #rightPaginationColumns>
|
||||
<!-- Toggles and actions depending on route -->
|
||||
<!-- User: Only today switch + New booking button -->
|
||||
<div class="column is-narrow order-bookings-pagination__actions" v-if="isUserRoute">
|
||||
<div class="order-bookings-pagination__actions-grid">
|
||||
<div class="order-bookings-pagination__today-action">
|
||||
<label class="label is-small">{{ t("pagination.show_only_today") }}</label>
|
||||
<div class="field mb-0">
|
||||
<input
|
||||
id="today"
|
||||
type="checkbox"
|
||||
class="switch is-rounded"
|
||||
@change="
|
||||
(event) => {
|
||||
onOnlyTodayFilterChange({
|
||||
target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' },
|
||||
});
|
||||
}
|
||||
"
|
||||
:class="{ 'is-link': onlyTodayFilter !== '*' }"
|
||||
:checked="onlyTodayFilter !== '*'"
|
||||
/>
|
||||
<label for="today"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-bookings-pagination__new-booking-action">
|
||||
<label class="label is-small order-bookings-pagination__desktop-spacer"> </label>
|
||||
<button
|
||||
class="button is-link button-same-width"
|
||||
data-testid="user-bookings-new-booking"
|
||||
@click="SessionUser.functions.redirectTo.external('/user/bookings/new', true)"
|
||||
>
|
||||
{{ t("pagination.new_booking") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #leftPaginationColumns>
|
||||
</template>
|
||||
<template #rightPaginationColumns>
|
||||
<!-- Toggles and actions depending on route -->
|
||||
<!-- User: Only today switch + New booking button -->
|
||||
<div class="column is-narrow" v-if="router.currentRoute.value.path.startsWith('/user')">
|
||||
<label class="label is-small">{{ t('pagination.show_only_today') }}</label>
|
||||
<div class="field">
|
||||
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { onOnlyTodayFilterChange({ target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' } }) }" :class="{ 'is-link': onlyTodayFilter !== '*' }" :checked="onlyTodayFilter !== '*'" />
|
||||
<label for="today"></label>
|
||||
</div>
|
||||
<!-- Admin: pending + only today combined switch -->
|
||||
<div class="column is-narrow" v-if="isAdminRoute">
|
||||
<label class="label is-small">{{ t("pagination.show_only_todays_pending") }}</label>
|
||||
<div class="field">
|
||||
<input
|
||||
id="today-pending"
|
||||
type="checkbox"
|
||||
class="switch is-rounded"
|
||||
@change="
|
||||
(event) => {
|
||||
onOnlyTodayFilterChange(
|
||||
{ target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' } },
|
||||
false
|
||||
);
|
||||
onOrderIdFilterChange({ target: { value: event.target.checked ? 'is null' : '*' } });
|
||||
}
|
||||
"
|
||||
:class="{ 'is-link': onlyTodayFilter !== '*' && orderIdFilter === 'is null' }"
|
||||
:checked="onlyTodayFilter !== '*' && orderIdFilter === 'is null'"
|
||||
/>
|
||||
<label for="today-pending"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-narrow is-float-right" v-if="router.currentRoute.value.path.startsWith('/user')">
|
||||
<label class="label is-small"> </label>
|
||||
<button
|
||||
class="button is-link button-same-width"
|
||||
@click="SessionUser.functions.redirectTo.external('/user/bookings/new', true)"
|
||||
>
|
||||
{{ t('pagination.new_booking') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Admin: pending + only today combined switch -->
|
||||
<div class="column is-narrow" v-if="router.currentRoute.value.path.startsWith('/admin')">
|
||||
<label class="label is-small">{{ t('pagination.show_only_todays_pending') }}</label>
|
||||
<div class="field">
|
||||
<input id="today-pending" type="checkbox" class="switch is-rounded" @change="(event) => { onOnlyTodayFilterChange({ target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' } }, false); onOrderIdFilterChange({ target: { value: event.target.checked ? 'is null' : '*' } }); }" :class="{ 'is-link': onlyTodayFilter !== '*' && orderIdFilter === 'is null' }" :checked="onlyTodayFilter !== '*' && orderIdFilter === 'is null'" />
|
||||
<label for="today-pending"></label>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<OrderBookingsTable :objects="list" />
|
||||
</template>
|
||||
</TableLabeledPagination>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<OrderBookingsTable :objects="list" />
|
||||
</template>
|
||||
</TableLabeledPagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.order-bookings-pagination__filter .select,
|
||||
.order-bookings-pagination__filter select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.order-bookings-pagination__actions-grid {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.order-bookings-pagination__today-action .field {
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.order-bookings-pagination__new-booking-action .button {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.order-bookings-pagination__table :deep([data-testid="table-labeled-pagination-filters"] > .columns) {
|
||||
align-items: flex-start;
|
||||
column-gap: 0.75rem;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
row-gap: 0.85rem;
|
||||
}
|
||||
|
||||
.order-bookings-pagination__table :deep([data-testid="table-labeled-pagination-filters"] > .columns > .column),
|
||||
.order-bookings-pagination__filter,
|
||||
.order-bookings-pagination__actions {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.order-bookings-pagination__filter {
|
||||
flex: 1 1 calc(50% - 0.375rem);
|
||||
max-width: calc(50% - 0.375rem);
|
||||
}
|
||||
|
||||
.order-bookings-pagination__table
|
||||
:deep([data-testid="table-labeled-pagination-filters"] > .columns > .columns.is-multiline) {
|
||||
flex: 1 1 100%;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.order-bookings-pagination__actions {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.order-bookings-pagination__actions-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.order-bookings-pagination__new-booking-action .button {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.order-bookings-pagination__desktop-spacer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -2,16 +2,13 @@
|
||||
import { computed } from "vue";
|
||||
import { BButton, BField } from "buefy";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
steps: Array<any>;
|
||||
currentStep: number;
|
||||
showActions?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showActions: true,
|
||||
}
|
||||
);
|
||||
const props = withDefaults(defineProps<{
|
||||
steps: Array<any>;
|
||||
currentStep: number;
|
||||
showActions?: boolean;
|
||||
}>(), {
|
||||
showActions: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:currentStep", value: number): void;
|
||||
@@ -48,6 +45,9 @@ const goNext = () => {
|
||||
<div class="guided-instructions" data-testid="self-serve-guided-instructions">
|
||||
<div class="guided-instructions__header">
|
||||
<h3 class="title is-6">{{ $t("self_wash.follow_steps") }}</h3>
|
||||
<span class="guided-instructions__counter">
|
||||
{{ normalizedStepIndex + 1 }} / {{ steps.length }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<section
|
||||
@@ -55,18 +55,8 @@ const goNext = () => {
|
||||
class="guided-instructions__step"
|
||||
:data-testid="`self-serve-guided-step-${normalizedStepIndex}`"
|
||||
>
|
||||
<b-field>
|
||||
<template #label>
|
||||
<span class="guided-instructions__step-label" data-testid="self-serve-guided-step-label">
|
||||
<span class="guided-instructions__step-title" data-testid="self-serve-guided-step-title">
|
||||
{{ normalizedStepIndex + 1 }}. {{ activeStep.title }}
|
||||
</span>
|
||||
<span class="guided-instructions__counter" data-testid="self-serve-guided-counter">
|
||||
{{ normalizedStepIndex + 1 }}/{{ steps.length }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<div class="guided-instructions__content" data-testid="self-serve-guided-content">
|
||||
<b-field :label="`${normalizedStepIndex + 1}. ${activeStep.title}`">
|
||||
<div class="guided-instructions__content">
|
||||
<p v-if="activeStep.content" class="guided-instructions__paragraph">{{ activeStep.content }}</p>
|
||||
<template v-for="(brush, brushIndex) in activeStep.brushes || []" :key="brushIndex">
|
||||
<p class="guided-instructions__paragraph">
|
||||
@@ -115,28 +105,14 @@ const goNext = () => {
|
||||
.guided-instructions__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.guided-instructions__step-label {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.guided-instructions__step-title {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.guided-instructions__counter {
|
||||
flex: 0 0 auto;
|
||||
color: #566074;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { BIcon, BMessage, BRadioButton } from "buefy";
|
||||
import { BField, BIcon, BMessage, BRadioButton } from "buefy";
|
||||
|
||||
defineProps<{
|
||||
lanes: Array<any>;
|
||||
@@ -19,23 +19,14 @@ const emit = defineEmits<{
|
||||
<template>
|
||||
<div data-testid="self-serve-lane-step">
|
||||
<h1 class="title has-text-centered">{{ $t("self_wash.start_wash") }}</h1>
|
||||
<section class="self-serve-choice-group" data-testid="self-serve-lane-choice-group">
|
||||
<h2 class="self-serve-choice-label">{{ $t("self_wash.wash_lane") }}</h2>
|
||||
<div
|
||||
class="self-serve-choice-grid"
|
||||
:class="{ 'self-serve-choice-grid--centered': lanes.length >= 2 }"
|
||||
data-testid="self-serve-lane-options"
|
||||
>
|
||||
<b-field :label="$t('self_wash.wash_lane')">
|
||||
<div class="columns is-multiline is-mobile" :class="{ 'is-centered': lanes.length >= 2 }">
|
||||
<template v-for="lane in lanes" :key="lane.id">
|
||||
<div class="self-serve-choice-grid__item">
|
||||
<div class="column is-half-mobile is-one-third-tablet is-one-quarter-desktop">
|
||||
<b-radio-button
|
||||
class="self-serve-choice-card"
|
||||
:model-value="selectedLaneId"
|
||||
:native-value="lane.id"
|
||||
type="is-link"
|
||||
:disabled="!isLaneAvailable(lane)"
|
||||
:title="!isLaneAvailable(lane) ? $t('self_wash.lane_unavailable') : null"
|
||||
:aria-label="!isLaneAvailable(lane) ? $t('self_wash.lane_unavailable') : null"
|
||||
:data-testid="`self-serve-lane-option-${lane.id}`"
|
||||
@update:model-value="emit('update:selectedLaneId', lane.id)"
|
||||
@input="emit('update:selectedLaneId', lane.id)"
|
||||
@@ -45,7 +36,7 @@ const emit = defineEmits<{
|
||||
<span v-if="!isLaneAvailable(lane)">
|
||||
<small>
|
||||
<b-icon icon="times-circle" type="is-danger" pack="fas" class="mr-1" />
|
||||
{{ $t("self_wash.lane_unavailable") }}
|
||||
{{ $t("self_wash.occupied") }}
|
||||
</small>
|
||||
</span>
|
||||
<span v-else>
|
||||
@@ -58,18 +49,18 @@ const emit = defineEmits<{
|
||||
</b-radio-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="lanes.length === 0" class="column is-12">
|
||||
<b-message type="is-warning" aria-close-label="Luk besked">
|
||||
Ingen vaskebaner tilgaengelige for selvvask i <b>{{ departmentName || "denne" }}</b> afdeling.
|
||||
</b-message>
|
||||
</div>
|
||||
</div>
|
||||
<b-message v-if="lanes.length === 0" type="is-warning" aria-close-label="Luk besked">
|
||||
Ingen vaskebaner tilgængelige for selvvask i <b>{{ departmentName || "denne" }}</b> afdeling.
|
||||
</b-message>
|
||||
</section>
|
||||
</b-field>
|
||||
|
||||
<section class="self-serve-choice-group" data-testid="self-serve-wash-type-group">
|
||||
<h2 class="self-serve-choice-label">Maskine eller manuel vask</h2>
|
||||
<div class="self-serve-choice-grid self-serve-choice-grid--centered" data-testid="self-serve-wash-type-options">
|
||||
<div class="self-serve-choice-grid__item">
|
||||
<b-field label="Maskine eller manuel vask">
|
||||
<div class="columns is-mobile is-centered is-multiline">
|
||||
<div class="column is-half-mobile is-one-third-tablet is-one-quarter-desktop">
|
||||
<b-radio-button
|
||||
class="self-serve-choice-card"
|
||||
:model-value="washType"
|
||||
native-value="Manual"
|
||||
type="is-link"
|
||||
@@ -81,20 +72,17 @@ const emit = defineEmits<{
|
||||
<span>Manuel<br /></span>
|
||||
<small>
|
||||
<b-icon icon="check-circle" type="is-success" pack="fas" class="mr-1" />
|
||||
Tilgængelig
|
||||
Tilgaengelig
|
||||
</small>
|
||||
</span>
|
||||
</b-radio-button>
|
||||
</div>
|
||||
<div class="self-serve-choice-grid__item">
|
||||
<div class="column is-half-mobile is-one-third-tablet is-one-quarter-desktop">
|
||||
<b-radio-button
|
||||
class="self-serve-choice-card"
|
||||
:model-value="washType"
|
||||
native-value="Machine"
|
||||
type="is-link"
|
||||
:disabled="!isMachineAvailable(selectedLaneId)"
|
||||
:title="!isMachineAvailable(selectedLaneId) ? $t('self_wash.machine_unavailable_for_lane') : null"
|
||||
:aria-label="!isMachineAvailable(selectedLaneId) ? $t('self_wash.machine_unavailable_for_lane') : null"
|
||||
data-testid="self-serve-wash-type-machine"
|
||||
@update:model-value="emit('update:washType', 'Machine')"
|
||||
@input="emit('update:washType', 'Machine')"
|
||||
@@ -117,86 +105,6 @@ const emit = defineEmits<{
|
||||
</b-radio-button>
|
||||
</div>
|
||||
</div>
|
||||
<b-message
|
||||
v-if="selectedLaneId && !isMachineAvailable(selectedLaneId)"
|
||||
type="is-warning"
|
||||
has-icon
|
||||
:closable="false"
|
||||
data-testid="self-serve-machine-unavailable-guidance"
|
||||
>
|
||||
{{ $t("self_wash.machine_unavailable_for_lane") }}
|
||||
</b-message>
|
||||
</section>
|
||||
</b-field>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.self-serve-choice-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.self-serve-choice-label {
|
||||
color: #303440;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.self-serve-choice-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin-bottom: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.self-serve-choice-grid--centered {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.self-serve-choice-grid__item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.self-serve-choice-card {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
min-height: 5rem;
|
||||
padding: 0.75rem 0.5rem;
|
||||
white-space: normal;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.self-serve-choice-card :deep(.button) {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
min-height: 5rem;
|
||||
padding: 0.75rem 0.5rem;
|
||||
white-space: normal;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.self-serve-choice-card :deep(.button),
|
||||
.self-serve-choice-card span,
|
||||
.self-serve-choice-card :deep(.button span) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.self-serve-choice-card small,
|
||||
.self-serve-choice-card :deep(.button small) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
.self-serve-choice-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(11rem, 15rem));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,7 +38,7 @@ const emit = defineEmits<{
|
||||
<div class="card-footer-item">
|
||||
<button
|
||||
class="button is-fullwidth"
|
||||
:class="[answers[question.id] === false ? 'is-danger' : 'is-light']"
|
||||
:class="[answers[question.id] === false ? 'is-danger is-light' : 'is-light']"
|
||||
:data-testid="`self-serve-question-${question.id}-no`"
|
||||
@click="emit('answer-question', question.id, false)"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { BButton, BIcon } from "buefy";
|
||||
import { BButton, BIcon, BTooltip } from "buefy";
|
||||
import SelfServeQuestionCards from "@/components/displays/selfServe/SelfServeQuestionCards.vue";
|
||||
|
||||
defineProps<{
|
||||
@@ -7,9 +7,18 @@ defineProps<{
|
||||
visibleQuestions: Array<any>;
|
||||
answers: Record<number, boolean | undefined>;
|
||||
editAnswers: boolean;
|
||||
showDebug: boolean;
|
||||
conditions: Array<any>;
|
||||
rules: Array<any>;
|
||||
evaluateCondition: (conditionId: number) => boolean;
|
||||
evaluateRule: (rule: any, visited?: Set<number>) => boolean;
|
||||
isQuestionVisible: (question: any) => boolean;
|
||||
getConditionById: (conditionId: number) => any;
|
||||
getRuleTypeLabel: (type: string) => string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "toggle-debug"): void;
|
||||
(e: "toggle-edit"): void;
|
||||
(e: "answer-question", questionId: number, value: boolean): void;
|
||||
}>();
|
||||
@@ -26,8 +35,27 @@ const emitAnswerQuestion = (questionId: number, value: boolean) => {
|
||||
<p>{{ $t("self_wash.loading_data") }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="notification is-info is-light py-2 px-3 mb-4"
|
||||
data-testid="self-serve-questions-inline-loading"
|
||||
>
|
||||
<b-icon pack="fas" icon="spinner" custom-class="fa-pulse" size="is-small" />
|
||||
<span class="ml-2">{{ $t("self_wash.loading_data") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="is-flex is-justify-content-center is-align-items-center mb-4">
|
||||
<h1 class="title mb-0">{{ $t("self_wash.answer_questions") }}</h1>
|
||||
<b-button
|
||||
size="is-small"
|
||||
icon-left="bug"
|
||||
type="is-ghost"
|
||||
class="ml-2"
|
||||
data-testid="self-serve-toggle-debug"
|
||||
@click="emit('toggle-debug')"
|
||||
>
|
||||
Debug
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="editAnswers"
|
||||
size="is-small"
|
||||
@@ -41,16 +69,75 @@ const emitAnswerQuestion = (questionId: number, value: boolean) => {
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<div class="self-serve-questions-status-slot" aria-live="polite">
|
||||
<div
|
||||
class="notification is-info is-light py-2 px-3 mb-0"
|
||||
:class="{ 'is-invisible': !isLoading }"
|
||||
data-testid="self-serve-questions-inline-loading"
|
||||
:aria-hidden="!isLoading"
|
||||
>
|
||||
<b-icon pack="fas" icon="spinner" custom-class="fa-pulse" size="is-small" />
|
||||
<span class="ml-2">{{ $t("self_wash.loading_data") }}</span>
|
||||
<div v-if="showDebug" class="box mb-4 has-background-light" data-testid="self-serve-debug-panel">
|
||||
<h5 class="subtitle is-5">Debug: Betingelser</h5>
|
||||
<div class="tags">
|
||||
<b-tooltip
|
||||
v-for="condition in conditions"
|
||||
:key="condition.id"
|
||||
position="is-top"
|
||||
multilined
|
||||
type="is-dark"
|
||||
>
|
||||
<template #content>
|
||||
<div class="has-text-left">
|
||||
<p v-if="condition.description" class="mb-2"><i>{{ condition.description }}</i></p>
|
||||
<p class="is-size-7 has-text-weight-bold mb-1">Regler:</p>
|
||||
<div
|
||||
v-for="rule in rules.filter(entry => parseInt(entry.condition_id as any) === parseInt(condition.id))"
|
||||
:key="rule.id"
|
||||
class="is-size-7"
|
||||
>
|
||||
<span class="icon is-small">
|
||||
<i :class="evaluateRule(rule, new Set([parseInt(condition.id)])) ? 'fas fa-check has-text-success' : 'fas fa-times has-text-danger'" />
|
||||
</span>
|
||||
<span class="ml-1">[{{ getRuleTypeLabel(rule.type) }}] {{ rule.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<span class="tag" :class="evaluateCondition(condition.id) ? 'is-success' : 'is-light'">
|
||||
<span class="icon is-small mr-1">
|
||||
<i :class="evaluateCondition(condition.id) ? 'fas fa-check-circle' : 'fas fa-times-circle'" />
|
||||
</span>
|
||||
{{ condition.name }}
|
||||
</span>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5 class="subtitle is-5">Debug: Alle mulige sporgsmal (synlighed)</h5>
|
||||
<ul>
|
||||
<li v-for="question in visibleQuestions" :key="question.id" class="is-size-7">
|
||||
<span class="icon is-small">
|
||||
<i :class="isQuestionVisible(question) ? 'fas fa-eye has-text-success' : 'fas fa-eye-slash has-text-grey-light'" />
|
||||
</span>
|
||||
{{ question.question }}
|
||||
<b-tooltip v-if="question.condition_id" position="is-top" multilined type="is-dark">
|
||||
<template #content>
|
||||
<div v-if="getConditionById(question.condition_id)" class="has-text-left">
|
||||
<p v-if="getConditionById(question.condition_id).description" class="mb-2">
|
||||
<i>{{ getConditionById(question.condition_id).description }}</i>
|
||||
</p>
|
||||
<p class="is-size-7 has-text-weight-bold mb-1">Regler:</p>
|
||||
<div
|
||||
v-for="rule in rules.filter(entry => parseInt(entry.condition_id as any) === parseInt(question.condition_id))"
|
||||
:key="rule.id"
|
||||
class="is-size-7"
|
||||
>
|
||||
<span class="icon is-small">
|
||||
<i :class="evaluateRule(rule, new Set([parseInt(question.condition_id)])) ? 'fas fa-check has-text-success' : 'fas fa-times has-text-danger'" />
|
||||
</span>
|
||||
<span class="ml-1">[{{ getRuleTypeLabel(rule.type) }}] {{ rule.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<span class="has-text-grey is-clickable">
|
||||
(Hvis: {{ getConditionById(question.condition_id)?.name || question.condition_id }})
|
||||
</span>
|
||||
</b-tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<SelfServeQuestionCards
|
||||
@@ -61,20 +148,3 @@ const emitAnswerQuestion = (questionId: number, value: boolean) => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.self-serve-questions-status-slot {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
|
||||
.self-serve-questions-status-slot .notification {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.self-serve-questions-status-slot .notification.is-invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { BCheckbox, BField, BIcon } from "buefy";
|
||||
import { getSelfServeTaskDynamicImagePresentation } from "@/services/selfServeDynamicImage.js";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
tasks: Array<any>;
|
||||
@@ -75,24 +74,20 @@ const getProgramWheelSelection = (task: any) => {
|
||||
if (!taskUsesProgramPicker(task)) {
|
||||
return null;
|
||||
}
|
||||
const rawValue = task?.dynamic_images_vehicle_type ?? task?.dynamic_image_vehicle_type ?? task?.dynamicImagesVehicleType;
|
||||
if (rawValue === null || rawValue === undefined || rawValue === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getSelfServeTaskDynamicImagePresentation(task).thumbPosition;
|
||||
const parsed = Number.parseInt(String(rawValue), 10);
|
||||
return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
|
||||
};
|
||||
|
||||
const formatTaskTitle = (task: any) => {
|
||||
const title = String(task?.task || "");
|
||||
const programWheelSelection = getProgramWheelSelection(task);
|
||||
|
||||
if (programWheelSelection === null) {
|
||||
return title;
|
||||
}
|
||||
|
||||
const programTitle = title.match(/^(.*?\bprogram)(\b.*)$/i);
|
||||
if (programTitle) {
|
||||
return `${programTitle[1]} #${programWheelSelection}${programTitle[2]}`;
|
||||
}
|
||||
|
||||
return `${title} #${programWheelSelection}`;
|
||||
return programWheelSelection === null ? title : `#${programWheelSelection} ${title}`;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ const onDynamicImageError = () => {
|
||||
v-if="dynamicImageUrl"
|
||||
class="self-serve-dynamic-image-frame mb-4"
|
||||
:class="{ 'is-loading': isDynamicImageLoading }"
|
||||
data-testid="self-serve-dynamic-image-frame"
|
||||
>
|
||||
<b-skeleton
|
||||
v-if="isDynamicImageLoading"
|
||||
@@ -99,18 +98,16 @@ const onDynamicImageError = () => {
|
||||
<style scoped>
|
||||
.self-serve-dynamic-image-frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
.self-serve-dynamic-image-frame {
|
||||
max-width: 640px;
|
||||
}
|
||||
.self-serve-dynamic-image-frame.is-loading {
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
min-height: 180px;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.self-serve-dynamic-image-skeleton {
|
||||
@@ -121,11 +118,12 @@ const onDynamicImageError = () => {
|
||||
}
|
||||
|
||||
.self-serve-dynamic-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ const props = defineProps<{
|
||||
availableProductIds: number[];
|
||||
vehicleTypes: Array<any>;
|
||||
vehicleStepError?: string | null;
|
||||
vehicleStepGuidance?: string | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -110,15 +109,6 @@ const emitRegistration = (value: unknown) => {
|
||||
>
|
||||
{{ props.vehicleStepError }}
|
||||
</b-message>
|
||||
<b-message
|
||||
v-else-if="props.vehicleStepGuidance"
|
||||
type="is-info"
|
||||
has-icon
|
||||
:closable="false"
|
||||
data-testid="self-serve-vehicle-step-guidance"
|
||||
>
|
||||
{{ props.vehicleStepGuidance }}
|
||||
</b-message>
|
||||
<b-field :label="$t('self_wash.select_your_vehicle')">
|
||||
<template v-if="availableProductIds.length > 0">
|
||||
<SelfServeVehicleTypeSelector
|
||||
|
||||
@@ -29,7 +29,7 @@ const getLoadingVehicleTypes = (count: number): VehicleTypeTemplate[] => {
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
loadingTypes.push({
|
||||
id: index,
|
||||
name: "Indlæser...",
|
||||
name: "Indlaeser...",
|
||||
price: 0,
|
||||
loading: true,
|
||||
});
|
||||
@@ -146,7 +146,7 @@ watch(() => props.selectedVehicleTypeId, (newId) => {
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
Vælg en køretøjstype ved at trykke på et af ikonerne ovenfor.
|
||||
Vaelg venligst din koretojstype ved at klikke pa ikonet ovenfor.
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -8,20 +8,14 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showCustomer: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { loadList } = usePaginatedListInstance();
|
||||
|
||||
const canEditPermissions = () =>
|
||||
props.showCustomer ? SessionUser.canAccessSuperUser() : SessionUser.canAccessUser() || SessionUser.hasPermission("SUBUSERS_EDIT");
|
||||
const canDisableAccess = () =>
|
||||
props.showCustomer ? SessionUser.canAccessSuperUser() : SessionUser.canAccessUser() || SessionUser.hasPermission("SUBUSERS_DELETE");
|
||||
const canEditPermissions = () => SessionUser.canAccessUser() || SessionUser.hasPermission("SUBUSERS_EDIT");
|
||||
const canDisableAccess = () => SessionUser.canAccessUser() || SessionUser.hasPermission("SUBUSERS_DELETE");
|
||||
const canResendInvite = (subuser) =>
|
||||
(props.showCustomer ? SessionUser.canAccessSuperUser() : SessionUser.canAccessUser() || SessionUser.hasPermission("SUBUSERS_EDIT"))
|
||||
(SessionUser.canAccessUser() || SessionUser.hasPermission("SUBUSERS_EDIT"))
|
||||
&& Boolean(subuser?.can_resend_invite ?? subuser?.setup_required);
|
||||
|
||||
const formatDateTime = (dateString) => {
|
||||
@@ -56,16 +50,6 @@ const formatEmail = (subuser) => {
|
||||
return subuser?.setup_required ? "E-mail oplyses ved accept" : "-";
|
||||
};
|
||||
|
||||
const formatCustomer = (subuser) => {
|
||||
if (!props.showCustomer) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const number = subuser?.customer_number ?? "-";
|
||||
const name = subuser?.customer_name || "Ukendt kunde";
|
||||
return `${number} - ${name}`;
|
||||
};
|
||||
|
||||
const permissionSummary = (subuser) =>
|
||||
SessionUser.objects.subusers.functions.permissionSummary(subuser?.grant_permissions || []);
|
||||
|
||||
@@ -177,7 +161,7 @@ const onToggleEnabled = async (subuser, enabled) => {
|
||||
};
|
||||
|
||||
const onResendInvite = async (subuser) => {
|
||||
await SessionUser.objects.subusers.functions.resendInvite(subuser, refreshList, { superuser: props.showCustomer });
|
||||
await SessionUser.objects.subusers.functions.resendInvite(subuser, refreshList);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -187,7 +171,6 @@ const onResendInvite = async (subuser) => {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th v-if="showCustomer">Kunde</th>
|
||||
<th>Chauffør</th>
|
||||
<th>Kontakt</th>
|
||||
<th>Adgang</th>
|
||||
@@ -200,17 +183,12 @@ const onResendInvite = async (subuser) => {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="props.objects.length === 0">
|
||||
<td :colspan="showCustomer ? 10 : 9" class="has-text-centered has-text-grey py-6">Ingen chauffører fundet.</td>
|
||||
<td colspan="9" class="has-text-centered has-text-grey py-6">Ingen chauffører fundet.</td>
|
||||
</tr>
|
||||
|
||||
<tr v-for="subuser in props.objects" :key="`${subuser.id}-${subuser.grant_id || 'none'}`">
|
||||
<tr v-for="subuser in props.objects" :key="subuser.id">
|
||||
<td>{{ subuser.id }}</td>
|
||||
|
||||
<td v-if="showCustomer">
|
||||
<div class="has-text-weight-semibold">{{ formatCustomer(subuser) }}</div>
|
||||
<div class="is-size-7 has-text-grey">Grant #{{ subuser.grant_id }}</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="has-text-weight-semibold">{{ subuser.name || "-" }}</div>
|
||||
<div class="is-size-7 has-text-grey" :data-testid="`subuser-username-${subuser.id}`">
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup>
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { getDepartmentDailyReportComplaintCategoryLabel } from "@/services/departmentDailyReportComplaintCategories.js";
|
||||
@@ -94,24 +92,29 @@ const formatCategory = (value) => (
|
||||
<td class="complaint-description">{{ complaint.description }}</td>
|
||||
<td>{{ formatCreatedBy(complaint) }}</td>
|
||||
<td class="has-text-right">
|
||||
<div class="buttons is-right is-justify-content-flex-end" :data-testid="`superuser-complaint-actions-${complaint.id}`">
|
||||
<ActionSettingsWheelButton>
|
||||
<template #actions>
|
||||
<ActionSettingsWheelItem
|
||||
icon="fas fa-pen"
|
||||
:label="$t('global.edit')"
|
||||
:test-id="`superuser-complaint-edit-${complaint.id}`"
|
||||
:click-action="() => SessionUser.objects.department_daily_report_complaints.functions.showEditForm(complaint, loadList)"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
icon="fas fa-trash"
|
||||
:label="$t('global.delete')"
|
||||
template="danger"
|
||||
:test-id="`superuser-complaint-delete-${complaint.id}`"
|
||||
:click-action="() => SessionUser.objects.department_daily_report_complaints.functions.showDeleteConfirmationModal(complaint.id, loadList)"
|
||||
/>
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
<div class="buttons is-right is-justify-content-flex-end">
|
||||
<button
|
||||
class="button is-small is-dark"
|
||||
type="button"
|
||||
:data-testid="`superuser-complaint-edit-${complaint.id}`"
|
||||
@click="SessionUser.objects.department_daily_report_complaints.functions.showEditForm(complaint, loadList)"
|
||||
>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-pen"></i>
|
||||
</span>
|
||||
<span>{{ $t('global.edit') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="button is-small is-danger"
|
||||
type="button"
|
||||
:data-testid="`superuser-complaint-delete-${complaint.id}`"
|
||||
@click="SessionUser.objects.department_daily_report_complaints.functions.showDeleteConfirmationModal(complaint.id, loadList)"
|
||||
>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-trash"></i>
|
||||
</span>
|
||||
<span>{{ $t('global.delete') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -102,16 +102,26 @@ const parseBalance = (user) => {
|
||||
{{ parseBalance(user) }}</td>
|
||||
<td>
|
||||
<div class="buttons is-float-right">
|
||||
<ActionSettingsWheelButton icon="fas fa-exclamation-triangle">
|
||||
<template #actions>
|
||||
<ActionSettingsWheelItem
|
||||
label="Kundekonto er lukket i E-conomic"
|
||||
icon="fas fa-exclamation-triangle"
|
||||
template="warning"
|
||||
:click-action="showCustomerBarred"
|
||||
/>
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
<!-- Disabled customer actions -->
|
||||
<div class="dropdown is-right is-hoverable">
|
||||
<div class="dropdown-trigger">
|
||||
<button class="button is-small is-danger is-inverted" aria-haspopup="true" aria-controls="dropdown-menu" @click="showCustomerBarred">
|
||||
<span class="icon">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<a class="dropdown-item has-text-warning" @click="showCustomerBarred">
|
||||
<span class="icon">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</span>
|
||||
<span class="ml-1">Kundekonto er lukket i E-conomic</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -133,4 +143,4 @@ const parseBalance = (user) => {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,17 +1,15 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||
import { showEditDepartmentForm } from "@/components/forms/superUser/editDepartmentForm.vue";
|
||||
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
|
||||
const props = defineProps(["objects"]);
|
||||
const { t } = useI18n();
|
||||
import { ref } from "vue";
|
||||
import { departments, getDepartments, isLoading, getDepartmentName } from "@/components/pagination/departmentTabs.vue";
|
||||
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
|
||||
const { loadList, loadSwitch, metaCurrentPage, metaItemsPerPage, setList } = usePaginatedListInstance();
|
||||
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||
|
||||
// Get the departments (If the departments are not already loaded)
|
||||
if (departments.value.length === 0) {
|
||||
getDepartments();
|
||||
}
|
||||
|
||||
const draggingIndex = ref(null);
|
||||
const dragOverIndex = ref(null);
|
||||
@@ -70,6 +68,18 @@ const onDrop = async (event, newIndex) => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseCustomerName = (user) => {
|
||||
if (user.customer_name) {
|
||||
return user.customer_name;
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
};
|
||||
import { showEditDepartmentForm } from "@/components/forms/superUser/editDepartmentForm.vue";
|
||||
|
||||
import { showEditUserForm } from "@/components/forms/superUser/editUserForm.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
|
||||
const redirect = (path) => {
|
||||
window.location = path;
|
||||
};
|
||||
@@ -126,7 +136,7 @@ const toggleArchived = async (department) => {
|
||||
<th>{{ SessionUser.objects.departments.columns.archived.label }}</th>
|
||||
<th>{{ SessionUser.objects.departments.columns.latitude.label }}</th>
|
||||
<th>{{ SessionUser.objects.departments.columns.longitude.label }}</th>
|
||||
<th class="has-text-right">{{ $t("tables.actions") }}</th>
|
||||
<th>{{ $t("tables.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -202,41 +212,34 @@ const toggleArchived = async (department) => {
|
||||
column="longitude"
|
||||
:edit-function="SessionUser.objects.departments.showEditObjectFieldForm"
|
||||
/>
|
||||
<td class="has-text-right">
|
||||
<div
|
||||
class="buttons is-right is-justify-content-flex-end"
|
||||
:data-testid="`superuser-department-actions-${department.id}`"
|
||||
>
|
||||
<ActionSettingsWheelButton>
|
||||
<template #actions>
|
||||
<ActionSettingsWheelItem
|
||||
icon="fas fa-edit"
|
||||
:label="t('global.edit')"
|
||||
:test-id="`superuser-department-edit-${department.id}`"
|
||||
:click-action="
|
||||
() =>
|
||||
showEditDepartmentForm(
|
||||
department.id,
|
||||
department.name,
|
||||
department.description,
|
||||
department.economic_department_id
|
||||
)
|
||||
"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
icon="fas fa-external-link-alt"
|
||||
:label="t('global.open')"
|
||||
:test-id="`superuser-department-open-${department.id}`"
|
||||
:click-action="() => redirect('/admin/' + department.id)"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
icon="fas fa-cog"
|
||||
:label="t('global.settings')"
|
||||
:test-id="`superuser-department-settings-${department.id}`"
|
||||
:click-action="() => redirect('/superuser/departments/' + department.id)"
|
||||
/>
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="button is-small"
|
||||
@click="
|
||||
showEditDepartmentForm(
|
||||
department.id,
|
||||
department.name,
|
||||
department.description,
|
||||
department.economic_department_id
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="icon">
|
||||
<i class="fas fa-edit"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button class="button is-small" @click="redirect('/admin/' + department.id)">
|
||||
<!-- External link icon -->
|
||||
<span class="icon">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button class="button is-small is-dark" @click="redirect('/superuser/departments/' + department.id)">
|
||||
<span class="icon">
|
||||
<i class="fas fa-cog"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
<script setup>
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps(['objects']);
|
||||
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// Get the departments (If the departments are not already loaded)
|
||||
if (departments.value.length === 0) {
|
||||
getDepartments();
|
||||
}
|
||||
|
||||
|
||||
|
||||
import { showEditUserForm } from "@/components/forms/superUser/editUserForm.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||
|
||||
const redirect = (path) => {
|
||||
window.location = path;
|
||||
}
|
||||
|
||||
|
||||
defineProps({
|
||||
objects: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const editUser = (user) => {
|
||||
showEditUserForm(user.id, user.customer_number, user.display_name, user.group_id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="table is-fullwidth" data-testid="superuser-users-table">
|
||||
<thead>
|
||||
<table class="table is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t("objects.columns.id") }}</th>
|
||||
<th>{{ $t("tables.users.name") }}</th>
|
||||
<th>{{ $t("tables.users.role") }}</th>
|
||||
<th class="has-text-right">{{ $t("tables.actions") }}</th>
|
||||
<th>{{ $t('objects.columns.id') }}</th>
|
||||
<th>{{ $t('tables.users.name') }}</th>
|
||||
<th>{{ $t('tables.users.role') }}</th>
|
||||
<th class="has-text-right">{{ $t('tables.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in objects" :key="user.id" :data-testid="`superuser-users-row-${user.id}`">
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in objects" :key="user.id">
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.display_name }}</td>
|
||||
<td>{{ user.group_id }}</td>
|
||||
@@ -34,27 +46,25 @@ const editUser = (user) => {
|
||||
<div class="buttons is-float-right">
|
||||
<!-- Settings wheel -->
|
||||
<ActionSettingsWheelButton
|
||||
:customer_number="user.customer_number"
|
||||
:user_id="user.id"
|
||||
:data-testid="`superuser-user-actions-${user.id}`"
|
||||
:customer_number="user.customer_number"
|
||||
:user_id="user.id"
|
||||
>
|
||||
<template #actions>
|
||||
<ActionSettingsWheelItem
|
||||
:click-action="() => editUser(user)"
|
||||
icon="fas fa-user-edit"
|
||||
:label="$t('global.edit')"
|
||||
:test-id="`superuser-user-edit-${user.id}`"
|
||||
@click="showEditUserForm(user.id, user.customer_number, user.display_name, user.group_id)"
|
||||
icon="fas fa-user-edit"
|
||||
:label="$t('global.edit')"
|
||||
/>
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="objects.length === 0">
|
||||
<td colspan="4">{{ $t("global.no_data") }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup>
|
||||
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
import { departments, getDepartments } from "@/components/pagination/departmentTabs.vue";
|
||||
import { departments, getDepartments} from "@/components/pagination/departmentTabs.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
@@ -27,15 +27,16 @@ const props = defineProps({
|
||||
reloadList: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: null,
|
||||
default: null
|
||||
},
|
||||
add_other_customer_id: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const reload = () => {
|
||||
// Load the list of vehicles
|
||||
if (props.reloadList) {
|
||||
@@ -58,9 +59,13 @@ if (departments.value.length === 0) {
|
||||
const onClickListAddons = (vehicleId) => {
|
||||
// Redirect to the product addons page
|
||||
console.log("Fetching product addons for vehicle: " + vehicleId);
|
||||
SessionUser.request("/vehicles/addons/available", "GET", {
|
||||
id: vehicleId,
|
||||
});
|
||||
SessionUser.request(
|
||||
'/vehicles/addons/available',
|
||||
'GET',
|
||||
{
|
||||
id: vehicleId
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
const vehicleAddons = ref(null);
|
||||
@@ -71,9 +76,13 @@ const getVehicleAvailableAddons = (vehicleId, forceReload = false) => {
|
||||
// Get the vehicle addons from the server
|
||||
if (vehicleAddons.value === null) {
|
||||
vehicleAddons.value = [];
|
||||
SessionUser.request("/vehicles/addons/available", "GET", {
|
||||
id: vehicleId,
|
||||
}).then((response) => {
|
||||
SessionUser.request(
|
||||
'/vehicles/addons/available',
|
||||
'GET',
|
||||
{
|
||||
id: vehicleId
|
||||
},
|
||||
).then((response) => {
|
||||
console.log("Vehicle addons: ", response.data.data);
|
||||
vehicleAddons.value = response.data.data;
|
||||
});
|
||||
@@ -84,10 +93,14 @@ const getVehicleAvailableAddons = (vehicleId, forceReload = false) => {
|
||||
|
||||
const toggleVehicleAddon = (vehicleId, addonId) => {
|
||||
// Toggle the vehicle addon
|
||||
SessionUser.request("/vehicles/addons/toggle", "POST", {
|
||||
vehicle_id: vehicleId,
|
||||
addon_id: addonId,
|
||||
}).then(() => {
|
||||
SessionUser.request(
|
||||
'/vehicles/addons/toggle',
|
||||
'POST',
|
||||
{
|
||||
vehicle_id: vehicleId,
|
||||
addon_id: addonId
|
||||
},
|
||||
).then(() => {
|
||||
reload();
|
||||
});
|
||||
};
|
||||
@@ -95,9 +108,9 @@ const toggleVehicleAddon = (vehicleId, addonId) => {
|
||||
const getVehicleAddonToggleIcon = (vehicleAddon) => {
|
||||
// Check if the vehicle addon is applied
|
||||
if (isVehicleAddonApplied(vehicleAddon)) {
|
||||
return "fas fa-minus";
|
||||
return 'fas fa-minus';
|
||||
} else {
|
||||
return "fas fa-plus";
|
||||
return 'fas fa-plus';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -124,142 +137,140 @@ const getProductOptionsLabel = (vehicle) => {
|
||||
<div class="table-container" data-testid="user-vehicles-table-container">
|
||||
<table class="table is-fullwidth" data-testid="user-vehicles-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-if="!props.compact">{{ $t("objects.columns.id") }}</th>
|
||||
<th v-if="!props.compact">{{ $t("objects.columns.customer_id") }}</th>
|
||||
<th>{{ $t("objects.bookings.columns.reg_1") }}</th>
|
||||
<th>{{ $t("vehicles.type") }}</th>
|
||||
<th>{{ $t("objects.vehicles.columns.wash_subscription") }}</th>
|
||||
<th v-if="!props.compact">{{ SessionUser.objects.product_options.meta.title }}</th>
|
||||
<th v-if="!props.compact">{{ $t("common.reference") }}</th>
|
||||
<th v-if="!props.compact"></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th v-if="!props.compact">{{ $t('objects.columns.id') }}</th>
|
||||
<th v-if="!props.compact">{{ $t('objects.columns.customer_id') }}</th>
|
||||
<th>{{ $t('objects.bookings.columns.reg_1') }}</th>
|
||||
<th>{{ $t('vehicles.type') }}</th>
|
||||
<th>{{ $t('objects.vehicles.columns.wash_subscription') }}</th>
|
||||
<th v-if="!props.compact">{{ SessionUser.objects.product_options.meta.title }}</th>
|
||||
<th v-if="!props.compact">{{ $t('common.reference') }}</th>
|
||||
<th v-if="!props.compact"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="object in props.vehicles" :key="object.id">
|
||||
<!-- ID -->
|
||||
<EditableTableColumn v-if="!props.compact" :object="object" :loadList="reload" column="id" />
|
||||
<!-- Customer ID -->
|
||||
<EditableTableColumn v-if="!props.compact" :object="object" :loadList="reload" column="customer_id" />
|
||||
<!-- Reg -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="reg"
|
||||
/>
|
||||
<!-- Type -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="type"
|
||||
:parse-function="
|
||||
(value) => {
|
||||
return SessionUser.objects.products.functions.getProductName(value, 'Ukendt');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<!-- Wash Subscription -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="wash_subscription"
|
||||
:parse-function="
|
||||
(value) => {
|
||||
return value ? t('common.yes') : t('common.no');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<!-- Product Options, if the wash subscription is set to true -->
|
||||
<td v-if="!props.compact">
|
||||
<template v-if="object.wash_subscription">
|
||||
<!-- Enabled subscription -->
|
||||
<ActionSettingsWheelButton
|
||||
:label="getProductOptionsLabel(object)"
|
||||
:icon="SessionUser.objects.product_options.meta.icon"
|
||||
@mouseenter="getVehicleAvailableAddons(object.id, true)"
|
||||
>
|
||||
<template #actions>
|
||||
<!-- List addons -->
|
||||
<template v-for="vehicleAddon in vehicleAddons" :key="vehicleAddon.id">
|
||||
<ActionSettingsWheelItem
|
||||
:label="
|
||||
(isVehicleAddonApplied(vehicleAddon)
|
||||
? SessionUser.objects.global.language.remove
|
||||
: SessionUser.objects.global.language.add) +
|
||||
' ' +
|
||||
vehicleAddon.name
|
||||
"
|
||||
:icon="getVehicleAddonToggleIcon(vehicleAddon)"
|
||||
:click-action="() => toggleVehicleAddon(object.id, vehicleAddon.id)"
|
||||
:template="isVehicleAddonApplied(vehicleAddon) ? 'danger' : 'default'"
|
||||
/>
|
||||
</template>
|
||||
<!-- If there are no addons, show a message -->
|
||||
<ActionSettingsWheelItem
|
||||
v-if="vehicleAddons ? vehicleAddons.length === 0 : true"
|
||||
:label="$t('global.no_data')"
|
||||
icon="fas fa-list"
|
||||
/>
|
||||
<!-- Addons -->
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
<tr v-for="object in props.vehicles" :key="object.id">
|
||||
<!-- ID -->
|
||||
<EditableTableColumn
|
||||
v-if="!props.compact"
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
column="id"
|
||||
/>
|
||||
<!-- Customer ID -->
|
||||
<EditableTableColumn
|
||||
v-if="!props.compact"
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
column="customer_id"
|
||||
/>
|
||||
<!-- Reg -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="reg"
|
||||
/>
|
||||
<!-- Type -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="type"
|
||||
:parse-function="(value) => {
|
||||
return SessionUser.objects.products.functions.getProductName(value, 'Ukendt');
|
||||
}"
|
||||
/>
|
||||
<!-- Wash Subscription -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="wash_subscription"
|
||||
:parse-function="(value) => {
|
||||
return value ? t('common.yes') : t('common.no');
|
||||
}"
|
||||
/>
|
||||
<!-- Product Options, if the wash subscription is set to true -->
|
||||
<td v-if="!props.compact">
|
||||
<template v-if="object.wash_subscription">
|
||||
<!-- Enabled subscription -->
|
||||
<ActionSettingsWheelButton
|
||||
:label="getProductOptionsLabel(object)"
|
||||
:icon="SessionUser.objects.product_options.meta.icon"
|
||||
@mouseenter="getVehicleAvailableAddons(object.id, true)"
|
||||
>
|
||||
<template #actions>
|
||||
<!-- List addons -->
|
||||
<template v-for="vehicleAddon in vehicleAddons" :key="vehicleAddon.id">
|
||||
<ActionSettingsWheelItem
|
||||
:label="(isVehicleAddonApplied(vehicleAddon) ? SessionUser.objects.global.language.remove : SessionUser.objects.global.language.add) + ' ' + vehicleAddon.name"
|
||||
:icon="getVehicleAddonToggleIcon(vehicleAddon)"
|
||||
:click-action="() => toggleVehicleAddon(object.id, vehicleAddon.id)"
|
||||
:template="isVehicleAddonApplied(vehicleAddon) ? 'danger' : 'default'"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Disabled subscription -->
|
||||
{{ $t("global.no_data") }}
|
||||
</template>
|
||||
</td>
|
||||
<!-- Reference to the vehicle -->
|
||||
<EditableTableColumn
|
||||
v-if="!props.compact"
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="reference"
|
||||
/>
|
||||
<!-- Actions -->
|
||||
<td>
|
||||
<!-- Actions stay grouped behind the wheel menu. -->
|
||||
<ActionSettingsWheelButton
|
||||
v-if="!props.compact"
|
||||
:user_id="object.user_id"
|
||||
:reg_1="object.reg"
|
||||
:displayActionsDirectly="false"
|
||||
>
|
||||
<template #actions>
|
||||
<!-- View (Redirect to the vehicle page) -->
|
||||
<ActionSettingsWheelItem
|
||||
:label="$t('global.manage') + ' ' + $t('objects.vehicles.single')"
|
||||
icon="fas fa-eye"
|
||||
:click-action="() => redirectUserVehiclePage(object.id)"
|
||||
/>
|
||||
<!-- Delete -->
|
||||
<ActionSettingsWheelItem
|
||||
:label="$t('global.delete') + ' ' + $t('objects.vehicles.single')"
|
||||
icon="fas fa-trash"
|
||||
:template="'danger'"
|
||||
:click-action="
|
||||
() => SessionUser.objects.vehicles.functions.showDeleteObjectForm(object.id, () => reload())
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- If there are no addons, show a message -->
|
||||
<ActionSettingsWheelItem
|
||||
v-if="vehicleAddons ? vehicleAddons.length === 0 : true"
|
||||
:label="$t('global.no_data')"
|
||||
icon="fas fa-list"
|
||||
/>
|
||||
<!-- Addons -->
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Disabled subscription -->
|
||||
{{ $t('global.no_data') }}
|
||||
</template>
|
||||
</td>
|
||||
<!-- Reference to the vehicle -->
|
||||
<EditableTableColumn
|
||||
v-if="!props.compact"
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="reference"
|
||||
/>
|
||||
<!-- Actions -->
|
||||
<td>
|
||||
<!-- Actions -->
|
||||
<ActionSettingsWheelButton
|
||||
v-if="!props.compact"
|
||||
:user_id="object.user_id"
|
||||
:reg_1="object.reg"
|
||||
:displayActionsDirectly="true"
|
||||
>
|
||||
<template #actions>
|
||||
<!-- View (Redirect to the vehicle page) -->
|
||||
<ActionSettingsWheelItem
|
||||
:label="$t('global.manage') + ' ' + $t('objects.vehicles.single')"
|
||||
icon="fas fa-eye"
|
||||
:click-action="() => redirectUserVehiclePage(object.id)"
|
||||
/>
|
||||
<!-- Delete -->
|
||||
<ActionSettingsWheelItem
|
||||
:label="$t('global.delete') + ' ' + $t('objects.vehicles.single')"
|
||||
icon="fas fa-trash"
|
||||
:template="'danger'"
|
||||
:click-action="() => SessionUser.objects.vehicles.functions.showDeleteObjectForm(object.id, () => reload())"
|
||||
/>
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="10">
|
||||
{{ $t("tables.showing") }} {{ props.vehicles.length }} {{ $t("objects.vehicles.multiple") }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="10">{{ $t('tables.showing') }} {{ props.vehicles.length }} {{ $t('objects.vehicles.multiple') }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { parseError, getError, addError, clearErrors } from "@/components/reques
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { isPasskeySupported, authenticateWithPasskey } from "@/services/PasskeyAuthService.js";
|
||||
import { getSubuserPasswordPolicyError } from "@/services/subuserPasswordPolicy.js";
|
||||
import TwoFactorVerify from "@/components/forms/auth/TwoFactorVerify.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -40,12 +39,6 @@ const twoFactorToken = ref('');
|
||||
|
||||
const login = async () => {
|
||||
clearErrors();
|
||||
const passwordPolicyError = getSubuserPasswordPolicyError(password.value);
|
||||
if (passwordPolicyError) {
|
||||
addError(passwordPolicyError, 'auth');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let requestBody = { password: password.value };
|
||||
|
||||
|
||||
@@ -441,23 +441,16 @@ const addProductWithAddonsToOrder = async (product_id) => {
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Tilføj',
|
||||
showLoaderOnConfirm: true,
|
||||
inputValidator: (note) => {
|
||||
if (!String(note || '').trim()) {
|
||||
return 'Note er påkrævet for dette produkt';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
preConfirm: (note) => {
|
||||
const normalizedNote = String(note || '').trim();
|
||||
const confirmedOrderId = getValidOrderId();
|
||||
if (!confirmedOrderId) {
|
||||
Swal.showValidationMessage('Order ID is required');
|
||||
return false;
|
||||
}
|
||||
// Show the fake create order item
|
||||
showFakeCreateOrderItem(product_id, 1, 0, 0, normalizedNote);
|
||||
showFakeCreateOrderItem(product_id, 1, 0, 0, note);
|
||||
// Create the order item
|
||||
return createOrderItem(confirmedOrderId, product_id, 1, 0, normalizedNote).then(async (result) => {
|
||||
return createOrderItem(confirmedOrderId, product_id, 1, 0, note).then(async (result) => {
|
||||
let order_item_id = result.data.data.id;
|
||||
// Add the addons to the order
|
||||
await addAddonsToOrderMiddleware(product_id, 1, order_item_id, confirmedOrderId).then(() => {
|
||||
|
||||
@@ -217,44 +217,6 @@ const getVehicleReferenceValue = () => {
|
||||
return String(vehicleObject.value?.reference ?? "").trim();
|
||||
};
|
||||
|
||||
const getVehicleStateKey = (vehicle) => {
|
||||
if (!vehicle) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
vehicle?.id ?? "",
|
||||
normalizePlateValue(vehicle?.reg),
|
||||
normalizeCustomerNumber(vehicle?.customer_id) ?? "",
|
||||
vehicle?.type ?? "",
|
||||
vehicle?.booking_id ?? "",
|
||||
String(vehicle?.reference ?? "").trim(),
|
||||
].join("|");
|
||||
};
|
||||
|
||||
const getBookingStateKey = (booking) => {
|
||||
if (!booking) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
booking?.id ?? "",
|
||||
getBookingReg1Value(booking),
|
||||
getBookingReg2Value(booking),
|
||||
getBookingCustomerNumber(booking) ?? "",
|
||||
String(booking?.reference_number ?? booking?.reference ?? "").trim(),
|
||||
String(booking?.po ?? "").trim(),
|
||||
].join("|");
|
||||
};
|
||||
|
||||
const getBookingMatchesStateKey = (matches) => {
|
||||
if (!Array.isArray(matches) || matches.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return matches.map((booking) => getBookingStateKey(booking)).join("||");
|
||||
};
|
||||
|
||||
const setVehicleObject = (emittedVehicleObject, options = {}) => {
|
||||
const normalizedOptions = {
|
||||
preserveManualReference: true,
|
||||
@@ -262,26 +224,17 @@ const setVehicleObject = (emittedVehicleObject, options = {}) => {
|
||||
...options,
|
||||
};
|
||||
|
||||
const emittedVehiclePlate = normalizePlateValue(emittedVehicleObject?.reg || reg_1.value);
|
||||
vehicleObject.value = emittedVehicleObject;
|
||||
|
||||
const emittedVehiclePlate = normalizePlateValue(vehicleObject.value?.reg || reg_1.value);
|
||||
if (
|
||||
emittedVehiclePlate &&
|
||||
skippedDesktopBookingVehiclePlate.value === emittedVehiclePlate &&
|
||||
!isBookingMarkedVehicle(emittedVehicleObject)
|
||||
!isBookingMarkedVehicle(vehicleObject.value)
|
||||
) {
|
||||
skippedDesktopBookingVehiclePlate.value = "";
|
||||
}
|
||||
|
||||
const currentVehicleKey = getVehicleStateKey(vehicleObject.value);
|
||||
const nextVehicleKey = getVehicleStateKey(emittedVehicleObject);
|
||||
if (currentVehicleKey === nextVehicleKey) {
|
||||
if (normalizedOptions.nextSelectionSource) {
|
||||
setSelectionSource(normalizedOptions.nextSelectionSource);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
vehicleObject.value = emittedVehicleObject;
|
||||
|
||||
const matchedVehicleCustomerNumber = normalizeCustomerNumber(vehicleObject.value?.customer_id);
|
||||
const selectedCustomerNumber = normalizeCustomerNumber(customer_id.value);
|
||||
|
||||
@@ -321,10 +274,6 @@ const setBookingObject = (emittedBookingObject, options = {}) => {
|
||||
...options,
|
||||
};
|
||||
|
||||
if (getBookingStateKey(bookingObject.value) === getBookingStateKey(emittedBookingObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bookingObject.value = emittedBookingObject;
|
||||
|
||||
if (emittedBookingObject && emittedBookingObject.reference_number) {
|
||||
@@ -335,12 +284,7 @@ const setBookingObject = (emittedBookingObject, options = {}) => {
|
||||
};
|
||||
|
||||
const setBookingMatches = (emittedBookingMatches) => {
|
||||
const nextBookingMatches = Array.isArray(emittedBookingMatches) ? emittedBookingMatches : [];
|
||||
if (getBookingMatchesStateKey(bookingMatches.value) === getBookingMatchesStateKey(nextBookingMatches)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bookingMatches.value = nextBookingMatches;
|
||||
bookingMatches.value = Array.isArray(emittedBookingMatches) ? emittedBookingMatches : [];
|
||||
};
|
||||
|
||||
const mergeBookingMatchDetails = (booking) => {
|
||||
|
||||
@@ -416,21 +416,6 @@ const shouldPreserveCustomerSelection = (plateValue) => {
|
||||
};
|
||||
|
||||
let pendingVehicleCustomerLookup = null;
|
||||
const lastAutoSyncedVehicleKey = ref("");
|
||||
|
||||
const getVehicleAutoSyncKey = (vehicle, plateOverride = null) => {
|
||||
if (!vehicle) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
resolveBookingPlate(vehicle, plateOverride),
|
||||
vehicle?.id ?? "",
|
||||
normalizeCustomerNumber(vehicle?.customer_id) ?? "",
|
||||
normalizeCustomerNumber(customer_id.value) ?? "",
|
||||
].join("|");
|
||||
};
|
||||
|
||||
const syncMatchedVehicleSelection = (vehicle, options = {}) => {
|
||||
const normalizedOptions = {
|
||||
plateOverride: null,
|
||||
@@ -605,7 +590,6 @@ watch(reg_1, (newValue) => {
|
||||
// Define the search ID for this input change
|
||||
const search_id = register_new_search();
|
||||
console.log("reg_1 changed:", newValue);
|
||||
lastAutoSyncedVehicleKey.value = "";
|
||||
const shouldKeepCustomerSelection = shouldPreserveCustomerSelection(newValue);
|
||||
selectedDropdownItem.value = -1;
|
||||
// Check if the new value is empty, if so, clear the vehicles_matching array
|
||||
@@ -863,17 +847,11 @@ watch(vehicles_matching, (newValue) => {
|
||||
const currentValue = reg_1.value;
|
||||
const vehicle = newValue.find((vehicle) => vehicle.reg === currentValue);
|
||||
if (vehicle) {
|
||||
const nextAutoSyncKey = getVehicleAutoSyncKey(vehicle, currentValue);
|
||||
if (lastAutoSyncedVehicleKey.value !== nextAutoSyncKey) {
|
||||
lastAutoSyncedVehicleKey.value = nextAutoSyncKey;
|
||||
syncMatchedVehicleSelection(vehicle);
|
||||
}
|
||||
syncMatchedVehicleSelection(vehicle);
|
||||
} else if (getBookingMatchesForSelection(null, currentValue).length > 0) {
|
||||
lastAutoSyncedVehicleKey.value = "";
|
||||
clearCustomerConflict();
|
||||
emitVehicleObject(null, currentValue);
|
||||
} else {
|
||||
lastAutoSyncedVehicleKey.value = "";
|
||||
clearCustomerConflict();
|
||||
}
|
||||
// Check if the selectedDropdownItem index is valid
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, reactive, ref, watch } from "vue";
|
||||
import { useMediaQuery } from "@vueuse/core";
|
||||
import { computed, nextTick, reactive, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
releaseRuntimeState,
|
||||
} from "@/services/releaseTimeline.js";
|
||||
import { submitErrorReport } from "@/services/errorReports.js";
|
||||
import { errorReportLaunchRequestId } from "@/services/errorReportLauncher.js";
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
@@ -23,9 +21,6 @@ const FALLBACK_LABELS = {
|
||||
"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.submit": "Submit report",
|
||||
"error_report.submitted": "Error report submitted.",
|
||||
@@ -51,12 +46,7 @@ const form = reactive({
|
||||
data_collection_accepted: false,
|
||||
});
|
||||
|
||||
const isMobileReportPlacement = useMediaQuery("(max-width: 768px)");
|
||||
const isDesktopNavigationReportPlacement = useMediaQuery("(min-width: 1024px)");
|
||||
const isAuthenticated = computed(() => SessionUser.isInitiated() && SessionUser.authenticated.value);
|
||||
const shouldShowFloatingButton = computed(
|
||||
() => !isMobileReportPlacement.value && !isDesktopNavigationReportPlacement.value
|
||||
);
|
||||
const isValid = computed(() => (
|
||||
form.before_error.trim().length > 0
|
||||
&& form.expected.trim().length > 0
|
||||
@@ -106,12 +96,6 @@ const open = () => {
|
||||
void loadHtml2Canvas().catch(() => {});
|
||||
};
|
||||
|
||||
watch(errorReportLaunchRequestId, () => {
|
||||
if (isAuthenticated.value) {
|
||||
open();
|
||||
}
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
if (isSubmitting.value) {
|
||||
return;
|
||||
@@ -316,7 +300,6 @@ const submit = async () => {
|
||||
<template>
|
||||
<div v-if="isAuthenticated" data-error-report-exclude>
|
||||
<button
|
||||
v-if="shouldShowFloatingButton"
|
||||
class="button is-danger error-report-button"
|
||||
type="button"
|
||||
data-testid="error-report-button"
|
||||
@@ -334,7 +317,7 @@ const submit = async () => {
|
||||
<h2 class="title is-4">{{ tr("error_report.title") }}</h2>
|
||||
<p class="subtitle is-6">{{ tr("error_report.subtitle") }}</p>
|
||||
</div>
|
||||
<button class="delete" type="button" :aria-label="t('common.close')" :disabled="isSubmitting" @click="close"></button>
|
||||
<button class="delete" type="button" aria-label="close" :disabled="isSubmitting" @click="close"></button>
|
||||
</header>
|
||||
|
||||
<div v-if="submitted" class="notification is-success is-light" data-testid="error-report-submitted">
|
||||
@@ -349,35 +332,17 @@ const submit = async () => {
|
||||
|
||||
<label class="field">
|
||||
<span class="label">{{ tr("error_report.before_error") }}</span>
|
||||
<textarea
|
||||
v-model="form.before_error"
|
||||
class="textarea"
|
||||
maxlength="4000"
|
||||
:placeholder="tr('error_report.before_error_placeholder')"
|
||||
required
|
||||
></textarea>
|
||||
<textarea v-model="form.before_error" class="textarea" maxlength="4000" required></textarea>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">{{ tr("error_report.expected") }}</span>
|
||||
<textarea
|
||||
v-model="form.expected"
|
||||
class="textarea"
|
||||
maxlength="4000"
|
||||
:placeholder="tr('error_report.expected_placeholder')"
|
||||
required
|
||||
></textarea>
|
||||
<textarea v-model="form.expected" class="textarea" maxlength="4000" required></textarea>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">{{ tr("error_report.actual") }}</span>
|
||||
<textarea
|
||||
v-model="form.actual"
|
||||
class="textarea"
|
||||
maxlength="4000"
|
||||
:placeholder="tr('error_report.actual_placeholder')"
|
||||
required
|
||||
></textarea>
|
||||
<textarea v-model="form.actual" class="textarea" maxlength="4000" required></textarea>
|
||||
</label>
|
||||
|
||||
<label class="checkbox error-report-consent">
|
||||
|
||||
@@ -86,13 +86,6 @@ const menu_items = ref([
|
||||
children: [],
|
||||
hidden: false
|
||||
},
|
||||
{
|
||||
label: 'Chauffører',
|
||||
value: '/subusers',
|
||||
icon: 'fas fa-id-card',
|
||||
children: [],
|
||||
hidden: false
|
||||
},
|
||||
{
|
||||
label: SessionUser.objects.roles.meta.title,
|
||||
value: SessionUser.objects.roles.meta.endpoint,
|
||||
|
||||
@@ -332,7 +332,6 @@ const items = computed<NavigationItemProps[]>(() => [
|
||||
type: "category",
|
||||
children: [
|
||||
{ label: t("superuser.nav.employees"), to: "/superuser/users" },
|
||||
{ label: "Chauffører", to: "/superuser/subusers" },
|
||||
{ label: t("superuser.nav.customers"), to: "/superuser/customers" },
|
||||
{ label: t("superuser.nav.complaints"), to: "/superuser/complaints" },
|
||||
{ label: t("superuser.nav.roles"), to: "/superuser/roles" },
|
||||
|
||||
@@ -3,80 +3,38 @@
|
||||
import { useRouter } from 'vue-router';
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import Swal from "sweetalert2";
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { pingApiServer } from "@/services/apiHealth.js";
|
||||
|
||||
const props = defineProps({
|
||||
hasPermission: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
import { ref } from 'vue';
|
||||
defineProps(['hasPermission']);
|
||||
const router = useRouter();
|
||||
|
||||
const showDebugInfo = ref(false);
|
||||
const isSessionInitiated = computed(() => SessionUser.isInitiated());
|
||||
let sessionTimeout = null;
|
||||
|
||||
const clearSessionTimeout = () => {
|
||||
if (sessionTimeout) {
|
||||
clearTimeout(sessionTimeout);
|
||||
sessionTimeout = null;
|
||||
}
|
||||
};
|
||||
|
||||
const redirectToConnectivityIssue = () => {
|
||||
if (typeof window === "undefined" || window.location.pathname === "/connectivity-issue") {
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = "/connectivity-issue";
|
||||
};
|
||||
|
||||
const handleSlowSessionBootstrap = async () => {
|
||||
// If the user is not initiated, and it takes more than 5 seconds, show an error message
|
||||
setTimeout(() => {
|
||||
if (!SessionUser.isInitiated()) {
|
||||
const pingResult = await pingApiServer();
|
||||
if (!pingResult.ok) {
|
||||
redirectToConnectivityIssue();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await Swal.fire({
|
||||
Swal.fire({
|
||||
title: 'Error',
|
||||
text: 'The user session could not be initiated.',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'Clear session, and try again',
|
||||
showCancelButton: true,
|
||||
cancelButtonText: 'Retry',
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
}).then(() => {
|
||||
SessionUser.auth.forceClearSession();
|
||||
SessionUser.functions.redirectTo.pages.login();
|
||||
return;
|
||||
}
|
||||
|
||||
await SessionUser.initiateOnAppStart({ force: true });
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
sessionTimeout = setTimeout(() => {
|
||||
void handleSlowSessionBootstrap();
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
onBeforeUnmount(clearSessionTimeout);
|
||||
}, 5000);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!isSessionInitiated">
|
||||
<div v-if="!SessionUser.isInitiated()">
|
||||
<div class="pageloader is-active is-dark">
|
||||
<span class="title">Bekræfter bruger...</span>
|
||||
</div>
|
||||
</div>
|
||||
<slot v-else-if="props.hasPermission" />
|
||||
<slot v-if="hasPermission" />
|
||||
<div v-else>
|
||||
<div class="my-6">
|
||||
<h2 class="title is-2">403 Forbidden</h2>
|
||||
@@ -118,4 +76,4 @@ onBeforeUnmount(clearSessionTimeout);
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,12 +1,11 @@
|
||||
<script>
|
||||
import { getCurrentScope, inject, onScopeDispose, provide, ref } from 'vue';
|
||||
import { ref, inject, provide } from 'vue';
|
||||
import axios from "axios";
|
||||
import {API_URL} from "@/config.js";
|
||||
import { parseError, clearErrors } from "@/components/request/HandleGlobalError.vue";
|
||||
import { exportRowsToExcel } from "@/services/TableExcelExportService.js";
|
||||
|
||||
export const PaginatedListKey = Symbol('PaginatedList');
|
||||
const SEARCH_DEBOUNCE_MS = 250;
|
||||
|
||||
/**
|
||||
* usePaginatedList composable
|
||||
@@ -34,9 +33,6 @@ export function usePaginatedList() {
|
||||
|
||||
/** The latest search */
|
||||
const latestSearch = ref(null);
|
||||
const latestRequestId = ref(0);
|
||||
let activeRequestController = null;
|
||||
let searchDebounceTimeout = null;
|
||||
|
||||
/** Additional query parameters */
|
||||
const additionalQueryParameters = ref({});
|
||||
@@ -128,58 +124,24 @@ export function usePaginatedList() {
|
||||
return search === latestSearch.value;
|
||||
};
|
||||
|
||||
const clearPendingSearch = () => {
|
||||
if (searchDebounceTimeout) {
|
||||
clearTimeout(searchDebounceTimeout);
|
||||
searchDebounceTimeout = null;
|
||||
}
|
||||
};
|
||||
|
||||
const abortActiveRequest = () => {
|
||||
activeRequestController?.abort();
|
||||
activeRequestController = null;
|
||||
};
|
||||
|
||||
if (getCurrentScope()) {
|
||||
onScopeDispose(() => {
|
||||
clearPendingSearch();
|
||||
abortActiveRequest();
|
||||
});
|
||||
}
|
||||
|
||||
const isCanceledRequest = (error) => {
|
||||
return error?.code === "ERR_CANCELED"
|
||||
|| error?.name === "CanceledError"
|
||||
|| error?.name === "AbortError"
|
||||
|| (typeof axios.isCancel === "function" && axios.isCancel(error));
|
||||
};
|
||||
|
||||
/** The paginated get request */
|
||||
const paginatedGetRequest = async () => {
|
||||
// Set the latest search
|
||||
latestSearch.value = metaSearch.value;
|
||||
const tmp_search = metaSearch.value;
|
||||
const requestId = latestRequestId.value + 1;
|
||||
latestRequestId.value = requestId;
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
loadSwitch(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
abortActiveRequest();
|
||||
const requestController = typeof AbortController !== "undefined" ? new AbortController() : null;
|
||||
activeRequestController = requestController;
|
||||
|
||||
try {
|
||||
const response = await axios.get(API_URL + endpoint.value, {
|
||||
params: buildRequestParams(),
|
||||
headers: buildRequestHeaders(token),
|
||||
...(requestController ? { signal: requestController.signal } : {}),
|
||||
headers: buildRequestHeaders(token)
|
||||
});
|
||||
|
||||
// Check if the search is the latest search
|
||||
if (!isLatestSearch(tmp_search) || requestId !== latestRequestId.value) {
|
||||
if (!isLatestSearch(tmp_search)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -190,30 +152,22 @@ export function usePaginatedList() {
|
||||
response?.data?.meta?.pagination?.total || 0
|
||||
);
|
||||
meta.value = response?.data?.meta || null;
|
||||
loadSwitch(false);
|
||||
setLastUpdated();
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (!isCanceledRequest(error)) {
|
||||
parseError(error, 'paginatedGetRequest');
|
||||
console.log(error);
|
||||
}
|
||||
parseError(error, 'paginatedGetRequest');
|
||||
console.log(error);
|
||||
loadSwitch(false);
|
||||
return null;
|
||||
} finally {
|
||||
if (activeRequestController === requestController) {
|
||||
activeRequestController = null;
|
||||
}
|
||||
if (requestId === latestRequestId.value) {
|
||||
loadSwitch(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** The load list function */
|
||||
const loadList = () => {
|
||||
clearPendingSearch();
|
||||
loadSwitch(true);
|
||||
// clear the list
|
||||
return paginatedGetRequest();
|
||||
paginatedGetRequest();
|
||||
};
|
||||
|
||||
/** Set the page */
|
||||
@@ -300,17 +254,11 @@ export function usePaginatedList() {
|
||||
const search = (searchValue, autoLoad = true) => {
|
||||
// If the search is *, remove the search
|
||||
metaSearch.value = searchValue === '*' ? null : searchValue;
|
||||
latestSearch.value = metaSearch.value;
|
||||
// Reset the page to 1
|
||||
setPage(1);
|
||||
// Load the list
|
||||
if (autoLoad) {
|
||||
clearPendingSearch();
|
||||
loadSwitch(true);
|
||||
searchDebounceTimeout = setTimeout(() => {
|
||||
searchDebounceTimeout = null;
|
||||
paginatedGetRequest();
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
loadList();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
releaseChannelKey,
|
||||
releaseChannelOptions,
|
||||
} from "@/services/releaseChannelAvailability.js";
|
||||
import ReleaseFrontendVersionBadge from "@/components/release/ReleaseFrontendVersionBadge.vue";
|
||||
import { releaseRuntimeState } from "@/services/releaseTimeline.js";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -158,10 +157,7 @@ const selectOption = (option) => {
|
||||
data-testid="release-channel-selector"
|
||||
>
|
||||
<div class="release-channel-selector__header">
|
||||
<div class="release-channel-selector__header-main">
|
||||
<p>{{ selectorTitle }}</p>
|
||||
<ReleaseFrontendVersionBadge v-if="isSidebar" />
|
||||
</div>
|
||||
<p>{{ selectorTitle }}</p>
|
||||
<span v-if="selectorSubtitle">{{ selectorSubtitle }}</span>
|
||||
</div>
|
||||
|
||||
@@ -243,14 +239,6 @@ const selectOption = (option) => {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.release-channel-selector__header-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.release-channel-selector__header p {
|
||||
margin: 0;
|
||||
color: #10243f;
|
||||
|
||||
@@ -1,42 +1,21 @@
|
||||
<script setup>
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import ReleaseChannelSelector from "@/components/release/ReleaseChannelSelector.vue";
|
||||
import ReleaseUpdateWidget from "@/components/release/ReleaseUpdateWidget.vue";
|
||||
import {
|
||||
clearSelectedReleaseChannel,
|
||||
releaseChannelSelectorVisible,
|
||||
switchSelectedReleaseChannel,
|
||||
} from "@/services/releaseChannelAvailability.js";
|
||||
import {
|
||||
isReleaseSourceOverrideAvailable,
|
||||
RELEASE_SOURCE_MODES,
|
||||
setReleaseSourceOverride,
|
||||
} from "@/services/releaseBootstrap.js";
|
||||
import { inspectReleaseRuntimeForUpdate } from "@/services/releaseUpdate.js";
|
||||
import { releaseRuntimeState } from "@/services/releaseTimeline.js";
|
||||
|
||||
const switchingSlug = ref("");
|
||||
const switchError = ref("");
|
||||
const { t, te } = useI18n({ useScope: "global" });
|
||||
const reloadWindow = inject("releaseSourceReload", () => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
const tr = (key, fallback) => {
|
||||
const path = `configuration.release_manager.channel_selector.${key}`;
|
||||
return te(path) ? t(path) : fallback;
|
||||
};
|
||||
|
||||
const showUseLocalFrontend = computed(
|
||||
() =>
|
||||
releaseChannelSelectorVisible.value &&
|
||||
isReleaseSourceOverrideAvailable() &&
|
||||
String(releaseRuntimeState.source || "").toLowerCase() !== RELEASE_SOURCE_MODES.LOCAL
|
||||
);
|
||||
|
||||
const switchChannel = async (option) => {
|
||||
if (switchingSlug.value) {
|
||||
return;
|
||||
@@ -46,84 +25,29 @@ const switchChannel = async (option) => {
|
||||
switchError.value = "";
|
||||
try {
|
||||
await switchSelectedReleaseChannel(option.channel, SessionUser.refreshReleaseRuntime);
|
||||
void inspectReleaseRuntimeForUpdate(releaseRuntimeState, { autoDownload: true });
|
||||
} catch (error) {
|
||||
switchError.value = tr("switch_error", "Release channel could not be switched. The previous channel is still active.");
|
||||
} finally {
|
||||
switchingSlug.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const useLocalFrontend = () => {
|
||||
if (switchingSlug.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReleaseSourceOverride(RELEASE_SOURCE_MODES.LOCAL);
|
||||
clearSelectedReleaseChannel();
|
||||
reloadWindow();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="release-channel-sidebar-selector">
|
||||
<div v-if="releaseChannelSelectorVisible" class="release-channel-sidebar-selector">
|
||||
<ReleaseChannelSelector
|
||||
v-if="releaseChannelSelectorVisible"
|
||||
variant="sidebar"
|
||||
:switching-slug="switchingSlug"
|
||||
:disabled="Boolean(switchingSlug)"
|
||||
@select="switchChannel"
|
||||
/>
|
||||
<button
|
||||
v-if="showUseLocalFrontend"
|
||||
type="button"
|
||||
class="release-channel-sidebar-selector__local-button"
|
||||
data-testid="release-channel-use-local-frontend"
|
||||
:disabled="Boolean(switchingSlug)"
|
||||
@click="useLocalFrontend"
|
||||
>
|
||||
<i class="fas fa-code" aria-hidden="true"></i>
|
||||
<span>{{ tr("use_local_frontend", "Use local frontend") }}</span>
|
||||
</button>
|
||||
<p v-if="switchError" class="release-channel-sidebar-selector__error" role="alert">
|
||||
{{ switchError }}
|
||||
</p>
|
||||
<ReleaseUpdateWidget />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.release-channel-sidebar-selector__local-button {
|
||||
width: calc(100% - 32px);
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin: -2px 16px 14px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #b8c7d9;
|
||||
border-radius: 5px;
|
||||
background: #f8fbff;
|
||||
color: #153554;
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.release-channel-sidebar-selector__local-button:hover:not(:disabled) {
|
||||
border-color: #6f8fb4;
|
||||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.release-channel-sidebar-selector__local-button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.release-channel-sidebar-selector__error {
|
||||
margin: 0 16px 14px;
|
||||
color: #b42318;
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
isSameReleaseCommit,
|
||||
releaseUpdateState,
|
||||
shortReleaseCommit,
|
||||
} from "@/services/releaseUpdate.js";
|
||||
import { releaseRuntimeState } from "@/services/releaseTimeline.js";
|
||||
|
||||
const { locale } = useI18n({ useScope: "global" });
|
||||
const textValue = (value) => String(value ?? "").trim();
|
||||
const githubAccess = (version) =>
|
||||
version?.metadata?.github_access && typeof version.metadata.github_access === "object"
|
||||
? version.metadata.github_access
|
||||
: {};
|
||||
const githubCommit = (version) => {
|
||||
const access = githubAccess(version);
|
||||
return version?.commit && typeof version.commit === "object"
|
||||
? version.commit
|
||||
: access.commit || access.latest_commit || null;
|
||||
};
|
||||
const versionTimestamp = (version) => {
|
||||
const commit = githubCommit(version);
|
||||
return (
|
||||
textValue(version?.deployed_at) ||
|
||||
textValue(version?.released_at) ||
|
||||
textValue(version?.promoted_at) ||
|
||||
textValue(version?.created_at) ||
|
||||
textValue(version?.commit_authored_at) ||
|
||||
textValue(commit?.authored_at) ||
|
||||
textValue(githubAccess(version)?.commit_authored_at)
|
||||
);
|
||||
};
|
||||
const formatReleaseTime = (value) => {
|
||||
const timestamp = textValue(value);
|
||||
if (!timestamp) {
|
||||
return "";
|
||||
}
|
||||
const parsed = new Date(timestamp);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return timestamp;
|
||||
}
|
||||
return new Intl.DateTimeFormat(locale.value || undefined, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
}).format(parsed);
|
||||
};
|
||||
|
||||
const currentText = computed(() => shortReleaseCommit(releaseUpdateState.currentCommit));
|
||||
const latestText = computed(() => shortReleaseCommit(releaseUpdateState.latestCommit));
|
||||
const currentReleaseTime = computed(() =>
|
||||
formatReleaseTime(versionTimestamp(releaseRuntimeState.versions?.frontend || null))
|
||||
);
|
||||
const hasLatestUpdate = computed(
|
||||
() =>
|
||||
Boolean(releaseUpdateState.latestCommit) &&
|
||||
!isSameReleaseCommit(releaseUpdateState.currentCommit, releaseUpdateState.latestCommit)
|
||||
);
|
||||
const title = computed(() => {
|
||||
const releaseTime = currentReleaseTime.value;
|
||||
if (!hasLatestUpdate.value) {
|
||||
return releaseTime ? `Frontend ${currentText.value} released ${releaseTime}` : `Frontend ${currentText.value}`;
|
||||
}
|
||||
return releaseTime
|
||||
? `Frontend ${currentText.value} released ${releaseTime} to ${latestText.value}`
|
||||
: `Frontend ${currentText.value} to ${latestText.value}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="release-frontend-version-badge"
|
||||
:class="{ 'release-frontend-version-badge--update': hasLatestUpdate }"
|
||||
:title="title"
|
||||
data-testid="release-frontend-version-badge"
|
||||
>
|
||||
<span class="release-frontend-version-badge__commit">{{ currentText }}</span>
|
||||
<template v-if="hasLatestUpdate">
|
||||
<span class="release-frontend-version-badge__arrow">-></span>
|
||||
<span class="release-frontend-version-badge__commit">{{ latestText }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.release-frontend-version-badge {
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.release-frontend-version-badge__commit {
|
||||
padding: 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.release-frontend-version-badge--update {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.release-frontend-version-badge__arrow {
|
||||
color: #64748b;
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const emit = defineEmits(["retry"]);
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
operation: {
|
||||
type: Object,
|
||||
default: null,
|
||||
@@ -18,32 +16,6 @@ const statusClass = (status) => {
|
||||
if (["running", "queued", "pending"].includes(normalized)) return "is-info";
|
||||
return "is-light";
|
||||
};
|
||||
|
||||
const shortCommit = (value) => {
|
||||
const text = String(value || "").trim();
|
||||
return text.length > 12 ? text.slice(0, 12) : text;
|
||||
};
|
||||
|
||||
const operationContext = computed(() => props.operation?.context || {});
|
||||
const releaseGateContext = computed(() => operationContext.value.release_gate || {});
|
||||
const autoSyncEvent = computed(
|
||||
() => operationContext.value.auto_sync_event || props.operation?.steps?.find((step) => step.context?.auto_sync_event)?.context?.auto_sync_event || null
|
||||
);
|
||||
const operationMeta = computed(() => {
|
||||
const context = operationContext.value;
|
||||
const gate = releaseGateContext.value;
|
||||
const event = autoSyncEvent.value || {};
|
||||
const rows = [
|
||||
["Source", context.source || event.source || (gate.auto_sync ? "release_gate" : "")],
|
||||
["App", props.operation?.app || context.app || gate.app || event.app],
|
||||
["Commit", shortCommit(context.commit_sha || gate.expected_commit || event.commit_sha)],
|
||||
["Auto-sync", event.status || (gate.auto_sync ? "requested" : "")],
|
||||
];
|
||||
|
||||
return rows
|
||||
.filter(([, value]) => String(value || "").trim() !== "")
|
||||
.map(([label, value]) => ({ label, value }));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -55,13 +27,6 @@ const operationMeta = computed(() => {
|
||||
<span>{{ operation?.warning_step_count || 0 }} warnings</span>
|
||||
</div>
|
||||
|
||||
<div v-if="operationMeta.length" class="release-operation-timeline__meta" data-testid="release-operation-meta">
|
||||
<span v-for="item in operationMeta" :key="item.label">
|
||||
<strong>{{ item.label }}</strong>
|
||||
<span>{{ item.value }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ol class="release-operation-timeline__steps">
|
||||
<li
|
||||
v-for="step in operation?.steps || []"
|
||||
@@ -107,7 +72,6 @@ const operationMeta = computed(() => {
|
||||
}
|
||||
|
||||
.release-operation-timeline__status,
|
||||
.release-operation-timeline__meta,
|
||||
.release-operation-timeline__step-head,
|
||||
.release-operation-timeline__diagnostic,
|
||||
.release-operation-timeline__solution,
|
||||
@@ -122,21 +86,6 @@ const operationMeta = computed(() => {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.release-operation-timeline__meta {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
padding: 0.55rem 0.65rem;
|
||||
}
|
||||
|
||||
.release-operation-timeline__meta span {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.release-operation-timeline__steps {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, watch } from "vue";
|
||||
import { releaseRuntimeState } from "@/services/releaseTimeline.js";
|
||||
import {
|
||||
checkReleaseUpdateNow,
|
||||
downloadReleaseCandidate,
|
||||
inspectReleaseRuntimeForUpdate,
|
||||
installReadyReleaseUpdate,
|
||||
isSameReleaseCommit,
|
||||
releaseUpdateState,
|
||||
shortReleaseCommit,
|
||||
startReleaseUpdateAutoCheck,
|
||||
} from "@/services/releaseUpdate.js";
|
||||
|
||||
let stopAutoCheck = null;
|
||||
|
||||
const updateAvailable = computed(
|
||||
() =>
|
||||
Boolean(releaseUpdateState.latestCommit) &&
|
||||
!isSameReleaseCommit(releaseUpdateState.currentCommit, releaseUpdateState.latestCommit)
|
||||
);
|
||||
const isVisible = computed(
|
||||
() => updateAvailable.value && ["downloading", "ready", "failed"].includes(releaseUpdateState.status)
|
||||
);
|
||||
const progress = computed(() => Math.max(0, Math.min(100, Number(releaseUpdateState.progress || 0))));
|
||||
const progressStyle = computed(() => ({ width: `${progress.value}%` }));
|
||||
const latestCommitText = computed(() => shortReleaseCommit(releaseUpdateState.latestCommit));
|
||||
const currentCommitText = computed(() => shortReleaseCommit(releaseUpdateState.currentCommit));
|
||||
const statusLabel = computed(() => {
|
||||
if (releaseUpdateState.status === "ready") {
|
||||
return "Ready";
|
||||
}
|
||||
if (releaseUpdateState.status === "failed") {
|
||||
return "Failed";
|
||||
}
|
||||
return "Downloading";
|
||||
});
|
||||
const runtimeSignature = computed(() =>
|
||||
JSON.stringify({
|
||||
channel: releaseRuntimeState.channel?.slug || "",
|
||||
commit: releaseRuntimeState.versions?.frontend?.commit_sha || releaseRuntimeState.versions?.frontend?.commit || "",
|
||||
frontendBaseUrl: releaseRuntimeState.frontendBaseUrl || "",
|
||||
configured: releaseRuntimeState.availability?.configured !== false,
|
||||
})
|
||||
);
|
||||
const hasRuntimeCandidate = () =>
|
||||
Boolean(releaseRuntimeState.channel || releaseRuntimeState.frontendBaseUrl || releaseRuntimeState.versions?.frontend);
|
||||
|
||||
const inspectCurrentRuntime = () => {
|
||||
if (!hasRuntimeCandidate()) {
|
||||
return;
|
||||
}
|
||||
void inspectReleaseRuntimeForUpdate(releaseRuntimeState, { autoDownload: true });
|
||||
};
|
||||
|
||||
const retryDownload = () => {
|
||||
if (releaseUpdateState.candidate) {
|
||||
void downloadReleaseCandidate(releaseUpdateState.candidate);
|
||||
return;
|
||||
}
|
||||
void checkReleaseUpdateNow();
|
||||
};
|
||||
|
||||
const installUpdate = () => {
|
||||
void installReadyReleaseUpdate();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
stopAutoCheck = startReleaseUpdateAutoCheck();
|
||||
inspectCurrentRuntime();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopAutoCheck?.();
|
||||
stopAutoCheck = null;
|
||||
});
|
||||
|
||||
watch(runtimeSignature, inspectCurrentRuntime, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="isVisible" class="release-update-widget" data-testid="release-update-widget">
|
||||
<div class="release-update-widget__topline">
|
||||
<span class="release-update-widget__label">{{ statusLabel }}</span>
|
||||
<span class="release-update-widget__commits">{{ currentCommitText }} -> {{ latestCommitText }}</span>
|
||||
</div>
|
||||
|
||||
<template v-if="releaseUpdateState.status === 'ready'">
|
||||
<button
|
||||
type="button"
|
||||
class="release-update-widget__install"
|
||||
data-testid="release-update-install"
|
||||
@click="installUpdate"
|
||||
>
|
||||
Install
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="releaseUpdateState.status === 'failed'">
|
||||
<div class="release-update-widget__failed">
|
||||
<span>{{ releaseUpdateState.error || "Download failed." }}</span>
|
||||
<button type="button" data-testid="release-update-retry" @click="retryDownload">Retry</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
class="release-update-widget__progress"
|
||||
role="progressbar"
|
||||
:aria-valuenow="progress"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
data-testid="release-update-progress"
|
||||
>
|
||||
<span :style="progressStyle"></span>
|
||||
</div>
|
||||
<div class="release-update-widget__progress-meta">
|
||||
<span>{{ progress }}%</span>
|
||||
<span v-if="releaseUpdateState.totalAssets">
|
||||
{{ releaseUpdateState.downloadedAssets }}/{{ releaseUpdateState.totalAssets }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.release-update-widget {
|
||||
margin: -2px 16px 14px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #d7e2ef;
|
||||
border-radius: 5px;
|
||||
background: #f8fbff;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.release-update-widget__topline,
|
||||
.release-update-widget__progress-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.release-update-widget__label {
|
||||
color: #10243f;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.release-update-widget__commits {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #475569;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.release-update-widget__progress {
|
||||
height: 7px;
|
||||
margin-top: 8px;
|
||||
border-radius: 999px;
|
||||
background: #dbe5f0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.release-update-widget__progress span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: #153554;
|
||||
transition: width 0.18s ease;
|
||||
}
|
||||
|
||||
.release-update-widget__progress-meta {
|
||||
margin-top: 5px;
|
||||
color: #64748b;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.release-update-widget__install {
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
margin-top: 8px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: #153554;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.release-update-widget__install:hover {
|
||||
background: #10243f;
|
||||
}
|
||||
|
||||
.release-update-widget__failed {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.release-update-widget__failed span {
|
||||
color: #b42318;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.release-update-widget__failed button {
|
||||
min-height: 28px;
|
||||
border: 1px solid #f4a3a3;
|
||||
border-radius: 4px;
|
||||
background: #fff5f5;
|
||||
color: #9f1c1c;
|
||||
cursor: pointer;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.release-update-widget__progress span {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -49,7 +49,6 @@ import { SubuserGrants } from "@/components/session/token/SessionUser/Objects/Su
|
||||
import { Subusers } from "@/components/session/token/SessionUser/Objects/Subusers.vue";
|
||||
import { configureReleaseRuntime } from "@/services/releaseTimeline.js";
|
||||
import { fetchReleaseRuntime } from "@/services/releaseBootstrap.js";
|
||||
import { pingApiServer } from "@/services/apiHealth.js";
|
||||
import { normalizeSessionPayload } from "@/services/sessionPayload.js";
|
||||
import {
|
||||
isReleaseChannelApiAvailabilityError,
|
||||
@@ -234,92 +233,6 @@ const resetSessionState = () => {
|
||||
configureReleaseRuntime({});
|
||||
};
|
||||
|
||||
let sessionBootstrapPromise = null;
|
||||
|
||||
const extractErrorMessage = (error) => {
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error?.response?.data?.data?.message) {
|
||||
return error.response.data.data.message;
|
||||
}
|
||||
|
||||
if (error?.response?.data?.message) {
|
||||
return error.response.data.message;
|
||||
}
|
||||
|
||||
if (typeof error?.response?.data === "string") {
|
||||
return error.response.data;
|
||||
}
|
||||
|
||||
if (error?.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const isInvalidSessionError = (error) => {
|
||||
const status = Number.parseInt(String(error?.response?.status ?? ""), 10);
|
||||
const message = extractErrorMessage(error).toLowerCase();
|
||||
|
||||
if (status === 401 || status === 419) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (![400, 403].includes(status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
message.includes("invalid session") ||
|
||||
message.includes("invalid token") ||
|
||||
message.includes("unauthenticated") ||
|
||||
message.includes("unauthorized")
|
||||
);
|
||||
};
|
||||
|
||||
const redirectToConnectivityIssue = () => {
|
||||
if (typeof window === "undefined" || window.location.pathname === "/connectivity-issue") {
|
||||
return false;
|
||||
}
|
||||
|
||||
window.location.href = "/connectivity-issue";
|
||||
return true;
|
||||
};
|
||||
|
||||
export const handleSessionBootstrapFailure = async (error, { title, text } = {}) => {
|
||||
if (isInvalidSessionError(error)) {
|
||||
const pingResult = await pingApiServer();
|
||||
if (!pingResult.ok) {
|
||||
console.warn("Preserving stored session because API ping failed during invalid-session handling.", pingResult);
|
||||
const redirected = redirectToConnectivityIssue();
|
||||
return {
|
||||
cleared: false,
|
||||
apiReachable: false,
|
||||
ping: pingResult,
|
||||
redirectedTo: redirected ? "/connectivity-issue" : null,
|
||||
};
|
||||
}
|
||||
|
||||
forceClearSession();
|
||||
SessionUser.functions.redirectTo.pages.login();
|
||||
return { cleared: true, apiReachable: true, ping: pingResult, redirectedTo: "/login" };
|
||||
}
|
||||
|
||||
parseError(error, "auth");
|
||||
console.error(error);
|
||||
await Swal.fire({
|
||||
title,
|
||||
text,
|
||||
icon: "error",
|
||||
confirmButtonText: "Ok",
|
||||
});
|
||||
|
||||
return { cleared: false, apiReachable: null, ping: null, redirectedTo: null };
|
||||
};
|
||||
|
||||
export const refreshSessionData = async () => {
|
||||
if (!hydrateSessionFromStorage()) {
|
||||
resetSessionState();
|
||||
@@ -337,30 +250,12 @@ export const refreshSessionData = async () => {
|
||||
* Initiate the user session on app start
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const initiateOnAppStart = async ({ force = false } = {}) => {
|
||||
if (sessionBootstrapPromise && !force) {
|
||||
return await sessionBootstrapPromise;
|
||||
}
|
||||
|
||||
const bootstrapPromise = (async () => {
|
||||
if (!hydrateSessionFromStorage()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const initiateOnAppStart = async () => {
|
||||
if (hydrateSessionFromStorage()) {
|
||||
if (SessionUser.isSubuser.value) {
|
||||
return await getSubuserSessionData();
|
||||
}
|
||||
|
||||
return await getSessionData();
|
||||
})();
|
||||
|
||||
sessionBootstrapPromise = bootstrapPromise;
|
||||
|
||||
try {
|
||||
return await bootstrapPromise;
|
||||
} finally {
|
||||
if (sessionBootstrapPromise === bootstrapPromise) {
|
||||
sessionBootstrapPromise = null;
|
||||
await getSubuserSessionData();
|
||||
} else {
|
||||
await getSessionData();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -401,7 +296,7 @@ export const authenticateUser = async (customer_number, password) => {
|
||||
* @param {number} [credentials.phone] - Phone number (4-15 digits)
|
||||
* @param {number} [credentials.subuser_id] - Subuser ID
|
||||
* @param {string} [credentials.username] - Username
|
||||
* @param {string} credentials.password - Password (8-255 characters, uppercase, lowercase, and number)
|
||||
* @param {string} credentials.password - Password (8-255 characters)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const authenticateSubuser = async (credentials) => {
|
||||
@@ -477,16 +372,23 @@ export const getSubuserSessionData = async () => {
|
||||
await refreshReleaseRuntime();
|
||||
SessionUser.initiated.value = true;
|
||||
})
|
||||
.catch(async (error) => {
|
||||
.catch((error) => {
|
||||
if (isReleaseChannelApiAvailabilityError(error)) {
|
||||
console.warn("Selected release channel API is unavailable during session bootstrap.", error);
|
||||
markReleaseChannelApiUnavailable();
|
||||
return;
|
||||
}
|
||||
|
||||
return await handleSessionBootstrapFailure(error, {
|
||||
parseError(error, "auth");
|
||||
console.error(error);
|
||||
Swal.fire({
|
||||
title: "Fejl ved hentning af subbrugerdata",
|
||||
text: "Der opstod en fejl ved hentning af dine brugerdata. Prøv at logge ind igen.",
|
||||
icon: "error",
|
||||
confirmButtonText: "Ok",
|
||||
}).then(() => {
|
||||
SessionUser.auth.forceClearSession();
|
||||
SessionUser.functions.redirectTo.pages.login();
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -546,16 +448,23 @@ export const getSessionData = async () => {
|
||||
}
|
||||
SessionUser.initiated.value = true;
|
||||
})
|
||||
.catch(async (error) => {
|
||||
.catch((error) => {
|
||||
if (isReleaseChannelApiAvailabilityError(error)) {
|
||||
console.warn("Selected release channel API is unavailable during session bootstrap.", error);
|
||||
markReleaseChannelApiUnavailable();
|
||||
return;
|
||||
}
|
||||
|
||||
return await handleSessionBootstrapFailure(error, {
|
||||
parseError(error, "auth");
|
||||
console.error(error);
|
||||
Swal.fire({
|
||||
title: "Fejl ved hentning af brugerdata",
|
||||
text: "Der opstod en fejl ved hentning af dine brugerdata. Prøv at logge ind igen.",
|
||||
icon: "error",
|
||||
confirmButtonText: "Ok",
|
||||
}).then(() => {
|
||||
SessionUser.auth.forceClearSession();
|
||||
SessionUser.functions.redirectTo.pages.login();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
+6
-6
@@ -49,7 +49,7 @@ const buildDepartmentOptions = (departments, selectedDepartmentId) => {
|
||||
const buildComplaintCategoryOptions = (selectedCategory) => {
|
||||
const normalizedSelectedCategory = String(selectedCategory ?? "").trim();
|
||||
const options = [
|
||||
`<option value="">${escapeHtml("Vælg kategori")}</option>`,
|
||||
`<option value="">${escapeHtml("Vaelg kategori")}</option>`,
|
||||
...DEPARTMENT_DAILY_REPORT_COMPLAINT_CATEGORY_OPTIONS.map((category) => {
|
||||
const selected = category.value === normalizedSelectedCategory ? " selected" : "";
|
||||
return `<option value="${escapeHtml(category.value)}"${selected}>${escapeHtml(category.label)}</option>`;
|
||||
@@ -236,7 +236,7 @@ const resolveComplaintCustomerNumberFromLookup = (lookupState) => {
|
||||
return lookupState.selectedCustomer.customer_number;
|
||||
}
|
||||
|
||||
throw new Error("Vælg en kunde fra listen eller ryd feltet.");
|
||||
throw new Error("Vaelg en kunde fra listen eller ryd feltet.");
|
||||
};
|
||||
|
||||
const setupComplaintCustomerLookupField = (lookupState) => {
|
||||
@@ -521,28 +521,28 @@ export const DepartmentDailyReportComplaints = {
|
||||
10
|
||||
);
|
||||
if (!Number.isFinite(departmentId) || departmentId <= 0) {
|
||||
throw new Error("Afdeling er påkrævet.");
|
||||
throw new Error("Afdeling er paakraevet.");
|
||||
}
|
||||
|
||||
const washDate = (
|
||||
document.getElementById("superuser-complaint-wash-date")?.value || ""
|
||||
).trim();
|
||||
if (washDate === "") {
|
||||
throw new Error("Dato for vask er påkrævet.");
|
||||
throw new Error("Dato for vask er paakraevet.");
|
||||
}
|
||||
|
||||
const category = (
|
||||
document.getElementById("superuser-complaint-category")?.value || ""
|
||||
).trim();
|
||||
if (!isDepartmentDailyReportComplaintCategory(category)) {
|
||||
throw new Error("Kategori er påkrævet.");
|
||||
throw new Error("Kategori er paakraevet.");
|
||||
}
|
||||
|
||||
const description = (
|
||||
document.getElementById("superuser-complaint-description")?.value || ""
|
||||
).trim();
|
||||
if (description === "") {
|
||||
throw new Error("Beskrivelse er påkrævet.");
|
||||
throw new Error("Beskrivelse er paakraevet.");
|
||||
}
|
||||
|
||||
const customerNumber = resolveComplaintCustomerNumberFromLookup(customerLookupState);
|
||||
|
||||
@@ -20,67 +20,6 @@
|
||||
return normalizedRelayId;
|
||||
};
|
||||
|
||||
const OPERATIONAL_STATUSES = ["AVAILABLE", "OCCUPIED", "RESERVED"];
|
||||
const SELF_SERVE_CONFIGURATION_FIELDS = [
|
||||
{ field: "relay_in_id", label: "Indgangsrelæ" },
|
||||
{ field: "relay_out_id", label: "Udgangsrelæ" },
|
||||
{ field: "relay_machine_id", label: "Maskinrelæ" },
|
||||
{ field: "relay_machine_program_picker_id", label: "Programvælgerrelæ" },
|
||||
{ field: "relay_machine_cleaner_id", label: "Vaskerelæ" },
|
||||
{ field: "dynamic_image_id", label: "Maskinstatusbillede" },
|
||||
{ field: "machine_type_id", label: "Maskintype" },
|
||||
];
|
||||
|
||||
const hasConfiguredValue = (value) => {
|
||||
if (value === null || value === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const normalizedValue = value.trim().toLowerCase();
|
||||
return normalizedValue !== "" && normalizedValue !== "0" && normalizedValue !== "null";
|
||||
}
|
||||
|
||||
if (typeof value === "number") {
|
||||
return value > 0;
|
||||
}
|
||||
|
||||
return Boolean(value);
|
||||
};
|
||||
|
||||
const getSelfServeConfigurationWarnings = (object) => {
|
||||
if (Array.isArray(object?.dognvask_configuration_warnings)) {
|
||||
return object.dognvask_configuration_warnings;
|
||||
}
|
||||
|
||||
return SELF_SERVE_CONFIGURATION_FIELDS
|
||||
.filter(({ field }) => !hasConfiguredValue(object?.[field]))
|
||||
.map(({ field, label }) => ({
|
||||
field,
|
||||
label,
|
||||
message: `${label} mangler`,
|
||||
}));
|
||||
};
|
||||
|
||||
const isSelfServeConfigured = (object) => {
|
||||
if (typeof object?.dognvask_configured === "boolean") {
|
||||
return object.dognvask_configured;
|
||||
}
|
||||
if (typeof object?.selfserve_configured === "boolean") {
|
||||
return object.selfserve_configured;
|
||||
}
|
||||
|
||||
return getSelfServeConfigurationWarnings(object).length === 0;
|
||||
};
|
||||
|
||||
const isMachineStatusEnabled = (object) => {
|
||||
if (typeof object?.machine_status_enabled === "boolean") {
|
||||
return object.machine_status_enabled;
|
||||
}
|
||||
|
||||
return OPERATIONAL_STATUSES.includes(String(object?.status ?? "").toUpperCase());
|
||||
};
|
||||
|
||||
/**
|
||||
* The Department Lanes object
|
||||
*/
|
||||
@@ -337,20 +276,6 @@
|
||||
},
|
||||
functions: {
|
||||
getDepartmentName: getDepartmentName,
|
||||
getStatusToggles: async (departmentId) => {
|
||||
return SessionUser.request('/department/lanes/status-toggles', 'GET', {
|
||||
department_id: parseInt(departmentId)
|
||||
});
|
||||
},
|
||||
setMachineStatusEnabled: async (laneId, enabled) => {
|
||||
return SessionUser.request('/modules/self-serve/lane/status', 'PUT', {
|
||||
lane_id: parseInt(laneId),
|
||||
enabled: Boolean(enabled),
|
||||
});
|
||||
},
|
||||
isMachineStatusEnabled,
|
||||
isSelfServeConfigured,
|
||||
getSelfServeConfigurationWarnings,
|
||||
isOperational: (object) => {
|
||||
return object.status === "AVAILABLE" || object.status === "OCCUPIED" || object.status === "RESERVED";
|
||||
},
|
||||
|
||||
@@ -356,20 +356,6 @@ export const OrderBookings = {
|
||||
{ safety_seal: normalizeCompletionSafetySeal(safetySeal) },
|
||||
onAfterComplete
|
||||
);
|
||||
},
|
||||
resendBookingConfirmation: async (id) => {
|
||||
return SessionUser.request(
|
||||
OrderBookings.meta.endpoint + "/booking-confirmation/resend",
|
||||
"POST",
|
||||
{ id: parseInt(id, 10) }
|
||||
).then((response) => response.data.data);
|
||||
},
|
||||
resendBookingCompletionConfirmation: async (id) => {
|
||||
return SessionUser.request(
|
||||
OrderBookings.meta.endpoint + "/completion-confirmation/resend",
|
||||
"POST",
|
||||
{ id: parseInt(id, 10) }
|
||||
).then((response) => response.data.data);
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -924,13 +924,6 @@ const assignDraftOrderCustomer = async ({
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
resendWashCertificate: async (id) => {
|
||||
return SessionUser.request(
|
||||
Orders.meta.endpoint + "/wash-certificate/resend",
|
||||
"POST",
|
||||
{ id: parseInt(id, 10) }
|
||||
).then((response) => response.data.data);
|
||||
},
|
||||
showAttachWashCertificateForm(order_id, onAfterSubmit = null) {
|
||||
const normalizedOrderId = Number.parseInt(order_id, 10);
|
||||
if (!Number.isInteger(normalizedOrderId) || normalizedOrderId <= 0) {
|
||||
|
||||
@@ -9,16 +9,8 @@ const escapeHtml = (value) => String(value ?? "")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
const buildInviteFormHtml = (defaults = {}, options = {}) => `
|
||||
const buildInviteFormHtml = (defaults = {}) => `
|
||||
<div class="subuser-form">
|
||||
${options.includeCustomerNumber ? `
|
||||
<div class="field">
|
||||
<label class="label">Kundenummer</label>
|
||||
<div class="control">
|
||||
<input id="subuser-form-customer-number" class="input" type="number" inputmode="numeric" value="${escapeHtml(defaults.customer_number || defaults.customerNumber || "")}" placeholder="Kundens kundenummer" />
|
||||
</div>
|
||||
</div>
|
||||
` : ""}
|
||||
<div class="field">
|
||||
<label class="label">Navn</label>
|
||||
<div class="control">
|
||||
@@ -49,17 +41,11 @@ const buildInviteFormHtml = (defaults = {}, options = {}) => `
|
||||
</div>
|
||||
`;
|
||||
|
||||
const readInviteFormValues = (options = {}) => {
|
||||
const customerNumberRaw = document.getElementById("subuser-form-customer-number")?.value?.trim() || "";
|
||||
const readInviteFormValues = () => {
|
||||
const name = document.getElementById("subuser-form-name")?.value?.trim() || "";
|
||||
const phoneCountryCodeRaw = document.getElementById("subuser-form-phone-country-code")?.value?.trim() || "";
|
||||
const phoneRaw = document.getElementById("subuser-form-phone")?.value?.trim() || "";
|
||||
|
||||
if (options.includeCustomerNumber && !/^\d+$/.test(customerNumberRaw)) {
|
||||
Swal.showValidationMessage("Kundenummer skal udfyldes.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name.length < 3) {
|
||||
Swal.showValidationMessage("Navn skal være mindst 3 tegn.");
|
||||
return null;
|
||||
@@ -76,7 +62,6 @@ const readInviteFormValues = (options = {}) => {
|
||||
}
|
||||
|
||||
return {
|
||||
...(options.includeCustomerNumber ? { customer_number: Number.parseInt(customerNumberRaw, 10) } : {}),
|
||||
name,
|
||||
phone_country_code: Number.parseInt(phoneCountryCodeRaw, 10),
|
||||
phone: Number.parseInt(phoneRaw, 10),
|
||||
@@ -106,17 +91,17 @@ const showInviteFeedback = async (response) => {
|
||||
});
|
||||
};
|
||||
|
||||
const submitInviteForm = async ({ title, confirmButtonText, defaults = {}, endpoint, method, payloadBuilder, includeCustomerNumber = false }) => {
|
||||
const submitInviteForm = async ({ title, confirmButtonText, defaults = {}, endpoint, method, payloadBuilder }) => {
|
||||
const result = await Swal.fire({
|
||||
title,
|
||||
html: buildInviteFormHtml(defaults, { includeCustomerNumber }),
|
||||
html: buildInviteFormHtml(defaults),
|
||||
width: 640,
|
||||
focusConfirm: false,
|
||||
showCancelButton: true,
|
||||
confirmButtonText,
|
||||
cancelButtonText: "Annuller",
|
||||
preConfirm: async () => {
|
||||
const values = readInviteFormValues({ includeCustomerNumber });
|
||||
const values = readInviteFormValues();
|
||||
if (!values) {
|
||||
return false;
|
||||
}
|
||||
@@ -157,15 +142,13 @@ export const Subusers = {
|
||||
|
||||
return `${normalized.slice(0, 3).join(", ")} +${normalized.length - 3}`;
|
||||
},
|
||||
async showInviteForm(refreshCallback = null, options = {}) {
|
||||
const superuser = Boolean(options.superuser);
|
||||
async showInviteForm(refreshCallback = null) {
|
||||
const response = await submitInviteForm({
|
||||
title: "Invitér chauffør",
|
||||
confirmButtonText: "Send invitation",
|
||||
endpoint: superuser ? "/superuser/subusers/invite" : "/subusers/invite",
|
||||
endpoint: "/subusers/invite",
|
||||
method: "POST",
|
||||
payloadBuilder: (values) => values,
|
||||
includeCustomerNumber: superuser,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
@@ -179,14 +162,9 @@ export const Subusers = {
|
||||
|
||||
return response;
|
||||
},
|
||||
async resendInvite(subuser, refreshCallback = null, options = {}) {
|
||||
const superuser = Boolean(options.superuser);
|
||||
async resendInvite(subuser, refreshCallback = null) {
|
||||
try {
|
||||
const response = await authenticatedRequest(
|
||||
superuser ? "/superuser/subusers/invite/resend" : "/subusers/invite/resend",
|
||||
"POST",
|
||||
{ id: subuser.id, ...(subuser.grant_id ? { grant_id: subuser.grant_id } : {}) }
|
||||
);
|
||||
const response = await authenticatedRequest("/subusers/invite/resend", "POST", { id: subuser.id });
|
||||
await showInviteFeedback(response);
|
||||
if (typeof refreshCallback === "function") {
|
||||
await refreshCallback();
|
||||
|
||||
@@ -507,63 +507,6 @@ export const clearStoredPosOrderId = () => {
|
||||
localStorage.removeItem("pos_order_id");
|
||||
};
|
||||
|
||||
const hasActivePosOrderContext = () => {
|
||||
return Boolean(
|
||||
toPositiveInteger(order_id.value) ||
|
||||
getSelectedCustomerNumber() ||
|
||||
String(customer_name.value ?? "").trim() ||
|
||||
(Array.isArray(order_items.value) && order_items.value.length > 0) ||
|
||||
String(reference.value ?? "").trim() ||
|
||||
String(order_notes.value ?? "").trim() ||
|
||||
String(order_po.value ?? "").trim() ||
|
||||
String(order_safety_seal.value ?? "").trim() ||
|
||||
String(reg_1.value ?? "").trim() ||
|
||||
String(reg_2.value ?? "").trim() ||
|
||||
String(reg_3.value ?? "").trim() ||
|
||||
invoiceCollectionId.value ||
|
||||
completed_at.value ||
|
||||
getStoredPosOrderId()
|
||||
);
|
||||
};
|
||||
|
||||
export const clearActivePosOrderContext = (options = {}) => {
|
||||
const normalizedOptions = {
|
||||
clearStep: false,
|
||||
clearStoredOrderId: true,
|
||||
...options,
|
||||
};
|
||||
const hadActiveContext = hasActivePosOrderContext();
|
||||
|
||||
clearErrors();
|
||||
if (normalizedOptions.clearStep) {
|
||||
step.value = 1;
|
||||
}
|
||||
order_id.value = null;
|
||||
clearSelectedCustomerState({ clearOrderNotes: true });
|
||||
order_items.value = [];
|
||||
user_discounts.value = [];
|
||||
has_user_discounts_loaded.value = false;
|
||||
scans.value = [];
|
||||
scan_data.value = [];
|
||||
invoiceCollectionId.value = null;
|
||||
completed_at.value = null;
|
||||
reference.value = "";
|
||||
order_notes.value = "";
|
||||
order_po.value = "";
|
||||
order_safety_seal.value = "";
|
||||
reg_1.value = "";
|
||||
reg_2.value = "";
|
||||
reg_3.value = "";
|
||||
isCreatingOrder.value = false;
|
||||
createOrderRequest = null;
|
||||
clearSelectedOrderBookingSelection();
|
||||
if (normalizedOptions.clearStoredOrderId) {
|
||||
clearStoredPosOrderId();
|
||||
}
|
||||
|
||||
return hadActiveContext;
|
||||
};
|
||||
|
||||
const SELECTED_ORDER_BOOKING_ID_STORAGE_KEY = "pos_selected_order_booking_id";
|
||||
const SELECTED_ORDER_BOOKING_PLATE_STORAGE_KEY = "pos_selected_order_booking_plate";
|
||||
const SELECTED_ORDER_BOOKING_SKIPPED_PLATE_STORAGE_KEY = "pos_selected_order_booking_skipped_plate";
|
||||
@@ -749,7 +692,26 @@ export const restoreStoredPosOrderId = async (
|
||||
|
||||
export const reset_all_values = () => {
|
||||
// Reset all values to their initial state
|
||||
clearActivePosOrderContext({ clearStep: true });
|
||||
clearErrors();
|
||||
step.value = 1;
|
||||
order_id.value = null;
|
||||
clearCustomerSelection();
|
||||
clearCache();
|
||||
user_discounts.value = [];
|
||||
has_user_discounts_loaded.value = false;
|
||||
scans.value = [];
|
||||
scan_data.value = [];
|
||||
invoiceCollectionId.value = null;
|
||||
completed_at.value = null;
|
||||
reference.value = "";
|
||||
order_notes.value = "";
|
||||
order_po.value = "";
|
||||
order_safety_seal.value = "";
|
||||
isCreatingOrder.value = false;
|
||||
createOrderRequest = null;
|
||||
clearSelectedOrderBookingSelection();
|
||||
// Clear the order id from the local storage (if present)
|
||||
clearStoredPosOrderId();
|
||||
// Clear the query parameters
|
||||
window.history.pushState({}, "", `?step=1`);
|
||||
};
|
||||
@@ -1188,7 +1150,6 @@ export const selectScan = (scan) => {
|
||||
};
|
||||
|
||||
const cached_customer_names = [];
|
||||
const activeCustomerSelectionRequests = new Map();
|
||||
|
||||
/** Get the customer's name */
|
||||
export const getCustomerName = async (customerNumber) => {
|
||||
@@ -1217,62 +1178,23 @@ export const getCustomerName = async (customerNumber) => {
|
||||
|
||||
/** Search, then select customer by customer number */
|
||||
export const searchAndSelectCustomer = async (customerNumber, options = {}) => {
|
||||
const normalizedOptions = {
|
||||
forceRefresh: false,
|
||||
...options,
|
||||
};
|
||||
const normalizedCustomerNumber = resolveCustomerNumber(customerNumber);
|
||||
if (!normalizedCustomerNumber) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedCustomerNumber = getSelectedCustomerNumber();
|
||||
const hasSelectedCustomerData =
|
||||
customer_data.value &&
|
||||
typeof customer_data.value === "object" &&
|
||||
!Array.isArray(customer_data.value) &&
|
||||
Object.keys(customer_data.value).length > 0;
|
||||
|
||||
if (
|
||||
!normalizedOptions.forceRefresh &&
|
||||
selectedCustomerNumber === normalizedCustomerNumber &&
|
||||
hasSelectedCustomerData
|
||||
) {
|
||||
return customer_data.value;
|
||||
}
|
||||
|
||||
if (!normalizedOptions.forceRefresh && activeCustomerSelectionRequests.has(normalizedCustomerNumber)) {
|
||||
return await activeCustomerSelectionRequests.get(normalizedCustomerNumber);
|
||||
}
|
||||
|
||||
// Get the customer data
|
||||
const request = authenticatedRequest(`/users/customer?customer_number=${normalizedCustomerNumber}`, "GET")
|
||||
return await authenticatedRequest(`/users/customer?customer_number=${customerNumber}`, "GET")
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
const responseData = response?.data?.data ?? {};
|
||||
const rawEconomicCustomer = responseData.economic_customer ?? null;
|
||||
const normalizedCustomer = normalizeCustomerRecord(rawEconomicCustomer, {
|
||||
...responseData,
|
||||
customerNumber: resolveCustomerNumber(rawEconomicCustomer) ?? normalizedCustomerNumber,
|
||||
customerNumber: resolveCustomerNumber(rawEconomicCustomer) ?? resolveCustomerNumber(customerNumber),
|
||||
});
|
||||
selectCustomer(normalizedCustomer, normalizedOptions);
|
||||
selectCustomer(normalizedCustomer, options);
|
||||
return normalizedCustomer;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return null;
|
||||
})
|
||||
.finally(() => {
|
||||
if (activeCustomerSelectionRequests.get(normalizedCustomerNumber) === request) {
|
||||
activeCustomerSelectionRequests.delete(normalizedCustomerNumber);
|
||||
}
|
||||
});
|
||||
|
||||
if (!normalizedOptions.forceRefresh) {
|
||||
activeCustomerSelectionRequests.set(normalizedCustomerNumber, request);
|
||||
}
|
||||
|
||||
return await request;
|
||||
};
|
||||
|
||||
/** Get order items */
|
||||
|
||||
@@ -7,9 +7,7 @@ import SubuserGrantSelector from "@/components/session/subuser/SubuserGrantSelec
|
||||
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();
|
||||
const isAuthenticatedSessionReady = computed(() => SessionUser.isInitiated() && SessionUser.authenticated.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -64,13 +62,13 @@ const isAuthenticatedSessionReady = computed(() => SessionUser.isInitiated() &&
|
||||
<span>{{ $t('header.return_to_account') }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
<router-link class="button is-white" to="/user/profile" v-if="isAuthenticatedSessionReady">
|
||||
<router-link class="button is-white" to="/user/profile" v-if="SessionUser.authenticated.value">
|
||||
<span class="icon">
|
||||
<i class="fas fa-user-circle" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
|
||||
</span>
|
||||
<span :class="{'has-text-black': !IS_DEV, 'has-text-red': IS_DEV}">{{ isAuthenticatedSessionReady ? SessionUser.getName() : ''}}</span>
|
||||
<span :class="{'has-text-black': !IS_DEV, 'has-text-red': IS_DEV}">{{ SessionUser.authenticated.value ? SessionUser.getName() : ''}}</span>
|
||||
</router-link>
|
||||
<template v-if="!SessionUser.authenticated.value">
|
||||
<template v-else>
|
||||
<router-link class="button is-white" to="/register">
|
||||
<span class="icon">
|
||||
<i class="fas fa-user-plus" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { BIcon, BMenu, BMenuItem, BMenuList, BTooltip } from "buefy";
|
||||
import type { NavigationItemProps } from "@/components/models/navigation/NavigationItem.vue";
|
||||
import { useNavigationItems } from "@/components/models/navigation/items/NavigationMenuItems.vue";
|
||||
@@ -12,7 +11,6 @@ import LanguageSelector from "@/components/i18n/LanguageSelector.vue";
|
||||
import ReleaseChannelSidebarSelector from "@/components/release/ReleaseChannelSidebarSelector.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import NavigationMenuGlobalSearch from "@/components/viewport/page/headers/menu/NavigationMenuGlobalSearch.vue";
|
||||
import { requestOpenErrorReport } from "@/services/errorReportLauncher.js";
|
||||
|
||||
type NavigationMenuItem = NavigationItemProps & {
|
||||
priority_order?: number | null;
|
||||
@@ -21,17 +19,9 @@ type NavigationMenuItem = NavigationItemProps & {
|
||||
type NavigationBadge = NonNullable<NavigationMenuItem["badge"]>;
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
const { parsedItems, parsedItemsGlobal } = useNavigationItems();
|
||||
const ERROR_REPORT_BUTTON_FALLBACK = "Report error";
|
||||
|
||||
const isMatchingRoute = (item: NavigationMenuItem) => item.to === route.path;
|
||||
const isAuthenticated = computed(() => SessionUser.isInitiated() && SessionUser.authenticated.value);
|
||||
const shouldShowErrorReportAction = computed(() => !isSmall.value && isAuthenticated.value);
|
||||
const errorReportButtonLabel = computed(() => {
|
||||
const label = t("error_report.button");
|
||||
return label === "error_report.button" ? ERROR_REPORT_BUTTON_FALLBACK : label;
|
||||
});
|
||||
|
||||
const navigationItems = computed<NavigationMenuItem[]>(() => {
|
||||
const items = [...parsedItems(), ...parsedItemsGlobal()] as NavigationMenuItem[];
|
||||
@@ -464,39 +454,10 @@ const getChildBadgeTestId = (item: NavigationMenuItem) => (isDraftsNavigationChi
|
||||
</BMenu>
|
||||
<LanguageSelector />
|
||||
<ReleaseChannelSidebarSelector />
|
||||
<div v-if="shouldShowErrorReportAction" class="desktop-buefy-error-report-action">
|
||||
<button
|
||||
class="button is-text desktop-buefy-error-report-button"
|
||||
type="button"
|
||||
data-testid="error-report-button"
|
||||
@click="requestOpenErrorReport"
|
||||
>
|
||||
<span class="icon is-small"><i class="fas fa-bug" aria-hidden="true"></i></span>
|
||||
<span>{{ errorReportButtonLabel }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-buefy-error-report-action {
|
||||
padding: 0 16px 14px;
|
||||
}
|
||||
|
||||
.desktop-buefy-error-report-button {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
color: #172033;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button.desktop-buefy-error-report-button:hover,
|
||||
.button.desktop-buefy-error-report-button:focus-visible {
|
||||
background: rgba(180, 35, 24, 0.08);
|
||||
color: #b42318;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.desktop-buefy-item-label,
|
||||
.desktop-buefy-child-label {
|
||||
display: flex;
|
||||
|
||||
@@ -13,7 +13,6 @@ const showProfile = computed(() => {
|
||||
const path = route.path;
|
||||
return path.startsWith("/user");
|
||||
});
|
||||
const isAuthenticatedSessionReady = computed(() => SessionUser.isInitiated() && SessionUser.authenticated.value);
|
||||
|
||||
|
||||
</script>
|
||||
@@ -24,7 +23,7 @@ const isAuthenticatedSessionReady = computed(() => SessionUser.isInitiated() &&
|
||||
<nav class="navbar is-link is-mobile is-fixed-top mobile-header-section" role="navigation" aria-label="main navigation" :class="{'is-blurred': isTransparent}">
|
||||
<div class="navbar-item" v-if="showProfile">
|
||||
<!-- Profile -->
|
||||
<router-link class="button is-white py-0" to="/user/profile" v-if="isAuthenticatedSessionReady">
|
||||
<router-link class="button is-white py-0" to="/user/profile" v-if="SessionUser.authenticated.value">
|
||||
<span class="icon is-large">
|
||||
<i class="fas fa-user-circle fa-2x"
|
||||
:class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
|
||||
@@ -46,7 +45,7 @@ const isAuthenticatedSessionReady = computed(() => SessionUser.isInitiated() &&
|
||||
<NavigationMenuDepartment/>
|
||||
</div>
|
||||
<!-- Subuser Grant Selection -->
|
||||
<div class="navbar-item" v-if="isAuthenticatedSessionReady && SessionUser.isSubuser.value && showProfile">
|
||||
<div class="navbar-item" v-if="SessionUser.isSubuser.value && showProfile">
|
||||
<SubuserGrantSelector :showLabel="false" :compact="true" />
|
||||
</div>
|
||||
<div class="navbar-end" style="margin-left: auto;" v-show="!SessionUser.canAccessAdmin()">
|
||||
|
||||
@@ -1,31 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { toggleExpanded, isOpen } from '@/components/viewport/page/headers/ViewportHeaderSettings.vue';
|
||||
import NavigationMenuItems from "@/components/viewport/page/headers/menu/NavigationMenuItems.vue";
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
import DesktopNavigationBuefy from "@/components/viewport/page/headers/DesktopNavigationBuefy.vue";
|
||||
import { requestOpenErrorReport } from "@/services/errorReportLauncher.js";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
const VITE_BUILD_DATE = import.meta.env.VITE_BUILD_DATE || 'No build date';
|
||||
const VITE_COMMIT_HASH = import.meta.env.VITE_COMMIT_HASH || 'No commit hash';
|
||||
const VITE_APP_VERSION = import.meta.env.VITE_APP_VERSION || 'No app version';
|
||||
const VITE_IS_DEV = import.meta.env.DEV || false;
|
||||
const ERROR_REPORT_BUTTON_FALLBACK = "Report error";
|
||||
|
||||
const isAuthenticated = computed(() => SessionUser.isInitiated() && SessionUser.authenticated.value);
|
||||
const errorReportButtonLabel = computed(() => {
|
||||
const label = t("error_report.button");
|
||||
return label === "error_report.button" ? ERROR_REPORT_BUTTON_FALLBACK : label;
|
||||
});
|
||||
|
||||
const openErrorReport = () => {
|
||||
if (isOpen.value) {
|
||||
toggleExpanded();
|
||||
}
|
||||
requestOpenErrorReport();
|
||||
};
|
||||
|
||||
/**
|
||||
* Scrollable menu for mobile viewports.
|
||||
@@ -84,23 +65,12 @@ const openErrorReport = () => {
|
||||
<NavigationMenuItems />
|
||||
</div>
|
||||
<!-- Bottom version info -->
|
||||
<div class="has-text-centered" style="padding: 16px; font-size: 12px; color: gray;" data-testid="mobile-build-info">
|
||||
<div class="has-text-centered" style="padding: 16px; font-size: 12px; color: gray;">
|
||||
<!--<div>Version: {{ VITE_APP_VERSION }}</div>-->
|
||||
<div>Commit: {{ VITE_COMMIT_HASH }}</div>
|
||||
<div>Build Date: {{ SessionUser.functions.date.toLocal(VITE_BUILD_DATE) }}</div>
|
||||
<div v-if="VITE_IS_DEV" style="color: red;">Development Mode</div>
|
||||
</div>
|
||||
<div v-if="isAuthenticated" class="mobile-error-report-action">
|
||||
<button
|
||||
class="button is-danger mobile-error-report-button"
|
||||
type="button"
|
||||
data-testid="error-report-button"
|
||||
@click="openErrorReport"
|
||||
>
|
||||
<span class="icon is-small"><i class="fas fa-bug" aria-hidden="true"></i></span>
|
||||
<span>{{ errorReportButtonLabel }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,12 +111,6 @@ const openErrorReport = () => {
|
||||
.mobile-menu-open .mobile-menu-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.mobile-error-report-action {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
.mobile-error-report-button {
|
||||
width: 100%;
|
||||
}
|
||||
.mobile-menu-open .text {
|
||||
/* Menu */
|
||||
margin: 0 auto;
|
||||
@@ -180,4 +144,4 @@ const openErrorReport = () => {
|
||||
/* Prevent click-through */
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -6,9 +6,7 @@ import CollectedInvoiceQueueMonitor from "@/components/viewport/page/headers/men
|
||||
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();
|
||||
const isAuthenticatedSessionReady = computed(() => SessionUser.isInitiated() && SessionUser.authenticated.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -64,13 +62,13 @@ const isAuthenticatedSessionReady = computed(() => SessionUser.isInitiated() &&
|
||||
<span>{{ $t('header.return_to_account') }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
<router-link class="button is-white" to="/user/profile" v-if="isAuthenticatedSessionReady">
|
||||
<router-link class="button is-white" to="/user/profile" v-if="SessionUser.authenticated.value">
|
||||
<span class="icon">
|
||||
<i class="fas fa-user-circle" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
|
||||
</span>
|
||||
<span :class="{'has-text-black': !IS_DEV, 'has-text-red': IS_DEV}">{{ isAuthenticatedSessionReady ? SessionUser.getName() : ''}}</span>
|
||||
<span :class="{'has-text-black': !IS_DEV, 'has-text-red': IS_DEV}">{{ SessionUser.authenticated.value ? SessionUser.getName() : ''}}</span>
|
||||
</router-link>
|
||||
<template v-if="!SessionUser.authenticated.value">
|
||||
<template v-else>
|
||||
<router-link class="button is-white" to="/register">
|
||||
<span class="icon">
|
||||
<i class="fas fa-user-plus" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
|
||||
|
||||
@@ -106,42 +106,12 @@ const sortByOrderPriority = (list) => [...list].sort((a, b) => {
|
||||
return parseInt(a?.id ?? 0) - parseInt(b?.id ?? 0);
|
||||
});
|
||||
|
||||
const mergeStableQuestionOrder = (previousIds, incomingIds) => {
|
||||
const normalizedIncomingIds = [];
|
||||
const seenIncomingIds = new Set();
|
||||
incomingIds.forEach((id) => {
|
||||
if (seenIncomingIds.has(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
seenIncomingIds.add(id);
|
||||
normalizedIncomingIds.push(id);
|
||||
});
|
||||
|
||||
if (normalizedIncomingIds.length === 0 || !Array.isArray(previousIds) || previousIds.length === 0) {
|
||||
return normalizedIncomingIds;
|
||||
}
|
||||
|
||||
const incomingIdSet = new Set(normalizedIncomingIds);
|
||||
const nextIds = previousIds.filter((id) => incomingIdSet.has(id));
|
||||
const nextIdSet = new Set(nextIds);
|
||||
|
||||
normalizedIncomingIds.forEach((id) => {
|
||||
if (!nextIdSet.has(id)) {
|
||||
nextIds.push(id);
|
||||
nextIdSet.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
return nextIds;
|
||||
};
|
||||
|
||||
const normalizePositiveInt = (value) => {
|
||||
const parsed = parseInt(value);
|
||||
return !Number.isNaN(parsed) && parsed > 0 ? parsed : null;
|
||||
};
|
||||
|
||||
const extractErrorMessage = (error, fallback = "Kunne ikke hente selvvaskdata. Prøv igen.") => {
|
||||
const extractErrorMessage = (error, fallback = "Kunne ikke hente selvvaskdata. Prov igen.") => {
|
||||
const candidates = [
|
||||
error?.response?.data?.data?.message,
|
||||
error?.response?.data?.message,
|
||||
@@ -312,10 +282,9 @@ export function useSelfServeLogic() {
|
||||
const normalizedIds = (Array.isArray(questionList) ? questionList : [])
|
||||
.map((question) => parseInt(question?.id ?? 0))
|
||||
.filter((id) => id > 0);
|
||||
const orderedIds = mergeStableQuestionOrder(summaryVisibleQuestionIds.value, normalizedIds);
|
||||
|
||||
summaryVisibleQuestionIds.value = orderedIds;
|
||||
summaryQuestionOrder.value = orderedIds.reduce((accumulator, id, index) => {
|
||||
summaryVisibleQuestionIds.value = normalizedIds;
|
||||
summaryQuestionOrder.value = normalizedIds.reduce((accumulator, id, index) => {
|
||||
accumulator[id] = index;
|
||||
return accumulator;
|
||||
}, {});
|
||||
@@ -565,7 +534,7 @@ export function useSelfServeLogic() {
|
||||
} catch (error) {
|
||||
console.error("Error fetching self-serve summary:", error);
|
||||
if (isFetchRequestActive(requestId)) {
|
||||
requestError.value = extractErrorMessage(error, "Kunne ikke hente vaskestatus. Prøv igen.");
|
||||
requestError.value = extractErrorMessage(error, "Kunne ikke hente vaskestatus. Prov igen.");
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
@@ -757,7 +726,7 @@ export function useSelfServeLogic() {
|
||||
return payload;
|
||||
} catch (error) {
|
||||
console.error("Error synchronizing vehicle answer:", error);
|
||||
requestError.value = extractErrorMessage(error, "Kunne ikke gemme svaret. Prøv igen.");
|
||||
requestError.value = extractErrorMessage(error, "Kunne ikke gemme svaret. Prov igen.");
|
||||
throw error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -819,7 +788,7 @@ export function useSelfServeLogic() {
|
||||
return { deletedCount: conditionIdsToDelete.length };
|
||||
} catch (error) {
|
||||
console.error("Error clearing self-serve answers:", error);
|
||||
requestError.value = extractErrorMessage(error, "Kunne ikke nulstille svar. Prøv igen.");
|
||||
requestError.value = extractErrorMessage(error, "Kunne ikke nulstille svar. Prov igen.");
|
||||
throw error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -995,7 +964,7 @@ export function useSelfServeLogic() {
|
||||
});
|
||||
const successValue = response?.data?.success ?? response?.success;
|
||||
if (successValue === false) {
|
||||
throw new Error(extractErrorMessage(response, "Kunne ikke opdatere vaskebanens tjenester. Prøv igen."));
|
||||
throw new Error(extractErrorMessage(response, "Kunne ikke opdatere vaskebanens tjenester. Prov igen."));
|
||||
}
|
||||
|
||||
const responsePayload = response?.data?.data ?? response?.data ?? response ?? {};
|
||||
@@ -1007,7 +976,7 @@ export function useSelfServeLogic() {
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error updating lane allowed services:", error);
|
||||
requestError.value = extractErrorMessage(error, "Kunne ikke opdatere vaskebanens tjenester. Prøv igen.");
|
||||
requestError.value = extractErrorMessage(error, "Kunne ikke opdatere vaskebanens tjenester. Prov igen.");
|
||||
if (!allowedServicesKnown.value) {
|
||||
allowedServices.value = activeTaskServices.value;
|
||||
}
|
||||
@@ -1027,7 +996,7 @@ export function useSelfServeLogic() {
|
||||
return await SessionUser.request('/modules/self-serve/lane/relay/machine/enable', 'post', payload);
|
||||
} catch (error) {
|
||||
console.error("Error enabling machine relay:", error);
|
||||
requestError.value = extractErrorMessage(error, "Kunne ikke starte maskinen. Prøv igen.");
|
||||
requestError.value = extractErrorMessage(error, "Kunne ikke starte maskinen. Prov igen.");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,11 +93,6 @@ export function useWashProgress(options) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!washInProgress.value && Number(currentStep.value) === 5) {
|
||||
clearProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
washInProgress: washInProgress.value,
|
||||
|
||||
@@ -41,7 +41,7 @@ export function useWashSessionActions(options) {
|
||||
|
||||
const extractCommandErrorMessage = (
|
||||
source,
|
||||
fallback = "Der opstod en fejl ved udførelse af kommandoen. Prøv igen senere."
|
||||
fallback = "Der opstod en fejl ved udforelse af kommandoen. Prov igen senere."
|
||||
) => {
|
||||
const candidates = [
|
||||
source?.response?.data?.data?.message,
|
||||
@@ -220,7 +220,6 @@ export function useWashSessionActions(options) {
|
||||
await updateLaneAllowedServices(laneId);
|
||||
} catch (error) {
|
||||
console.error("Error updating allowed services before start:", error);
|
||||
alertFn(extractCommandErrorMessage(error, "Der opstod en fejl ved klargøring af vasken. Prøv igen senere."));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -256,7 +255,7 @@ export function useWashSessionActions(options) {
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error starting self-serve wash:", error);
|
||||
alertFn(extractCommandErrorMessage(error, "Der opstod en fejl ved start af vasken. Prøv igen senere."));
|
||||
alertFn(extractCommandErrorMessage(error, "Der opstod en fejl ved start af vasken. Prov igen senere."));
|
||||
return false;
|
||||
} finally {
|
||||
isStartingWash.value = false;
|
||||
@@ -289,7 +288,7 @@ export function useWashSessionActions(options) {
|
||||
return true;
|
||||
}
|
||||
|
||||
alertFn(lastCommandErrorMessage.value || "Der opstod en fejl ved udførelse af kommandoen. Prøv igen senere.");
|
||||
alertFn(lastCommandErrorMessage.value || "Der opstod en fejl ved udforelse af kommandoen. Prov igen senere.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+83
-111
@@ -112,7 +112,7 @@
|
||||
"api": "API",
|
||||
"app": "app",
|
||||
"appen": "appen",
|
||||
"ar": "@:{'words.generated.years'}",
|
||||
"ar": "år",
|
||||
"are": "are",
|
||||
"arsag": "årsag",
|
||||
"at": "at",
|
||||
@@ -679,7 +679,7 @@
|
||||
"nøgle": "nøgle",
|
||||
"nogle": "nogle",
|
||||
"nok": "nok",
|
||||
"nøkkel": "@:{'words.generated.nøgle'}",
|
||||
"nøkkel": "nøgle",
|
||||
"nøkkelen": "nøglen",
|
||||
"not": "not",
|
||||
"note": "note",
|
||||
@@ -1599,25 +1599,7 @@
|
||||
"view_lane_new_tab": "@:{'words.generated.abn'} @:{'words.generated.bane'} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
|
||||
"view_lane_setup_new_tab": "@:{'words.generated.abn'} baneopsætning @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
|
||||
"view_order_new_tab": "@:{'words.generated.abn'} @:{'words.generated.ordre'} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
|
||||
"view_vehicle_new_tab": "?@:{'words.generated.bn'} @:{'words.generated.k'}?@:{'words.generated.ret'}?j {reg} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
|
||||
"accept_self_serve_wash": "Godkend selvbetjent vask",
|
||||
"accept_self_serve_wash_confirm": "Flyt denne kladde til kunde #{customerNumber} med selvbetjeningsdetaljerne?",
|
||||
"accept_self_serve_wash_success": "Den selvbetjente vask blev godkendt.",
|
||||
"email_notifications_section": "E-mail notifikationer",
|
||||
"resend_booking_completion_confirmation": "(Gen)send bekræftelse for færdig booking",
|
||||
"resend_booking_completion_confirmation_error": "Bekræftelsen for færdig booking kunne ikke sendes",
|
||||
"resend_booking_completion_confirmation_success": "Bekræftelsen for færdig booking er sendt",
|
||||
"resend_booking_confirmation": "(Gen)send bookingbekræftelse",
|
||||
"resend_booking_confirmation_error": "Bookingbekræftelsen kunne ikke sendes",
|
||||
"resend_booking_confirmation_success": "Bookingbekræftelsen er sendt",
|
||||
"resend_wash_certificate": "(Gen)send vaskecertifikat",
|
||||
"resend_wash_certificate_error": "Vaskecertifikatet kunne ikke sendes",
|
||||
"resend_wash_certificate_success": "Vaskecertifikatet er sendt",
|
||||
"self_serve_customer": "Kunde",
|
||||
"self_serve_driver": "Chauffør",
|
||||
"self_serve_elapsed": "Tid",
|
||||
"self_serve_wash_attachment": "Selvbetjent vask",
|
||||
"self_serve_wash_attachment_for_customer": "Selvbetjent vask for kunde #{customerNumber}"
|
||||
"view_vehicle_new_tab": "?@:{'words.generated.bn'} @:{'words.generated.k'}?@:{'words.generated.ret'}?j {reg} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}"
|
||||
},
|
||||
"show_details": "@:{'words.generated.vis'} @:{'words.generated.detaljer'}",
|
||||
"show_order_details": "@:{'words.generated.vis'} @:{'words.generated.ordredetaljer'}",
|
||||
@@ -2457,59 +2439,6 @@
|
||||
"username_desc": "@.capitalize:{'words.generated.brugernavnet'} @:{'words.generated.bruges'} @:{'words.generated.til'} @:{'words.generated.at'} @:{'words.generated.autentificere'} @:{'words.generated.api'} @:{'words.generated.forbindelsen'}."
|
||||
},
|
||||
"release_manager": {
|
||||
"integrations": {
|
||||
"title": "GitHub og Coolify",
|
||||
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
|
||||
"choose_repository": "Vælg repository",
|
||||
"add_health_url": "tilføj health-URL",
|
||||
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
|
||||
"open_github_token_settings": "Åbn GitHub token-indstillinger",
|
||||
"channel": "Kanal",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Eksempel: truckwash/backend-php",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: release/canary",
|
||||
"branch_placeholder": "branch",
|
||||
"latest_prefix": "Seneste: {commit}",
|
||||
"coolify_instance": "Coolify-instans",
|
||||
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
|
||||
"no_coolify_instance": "Ingen Coolify-instans",
|
||||
"coolify_instance_placeholder": "Coolify-instans-id",
|
||||
"coolify_service_uuid": "Coolify service-UUID",
|
||||
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
|
||||
"coolify_service_uuid_placeholder": "Coolify service-UUID",
|
||||
"health_url": "Health-URL",
|
||||
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
|
||||
"health_url_placeholder": "Health-URL",
|
||||
"ssl_domain": "Load balancer-domæne",
|
||||
"ssl_domain_message": "Skal være et DNS-domæne, der routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
|
||||
"ssl_domain_placeholder": "Load balancer-domæne",
|
||||
"https_domain_for_coolify": "Domæne kontrolleret af load balanceren",
|
||||
"endpoint_mode": "Endpoint mode",
|
||||
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
|
||||
"endpoint_mode_auto": "Auto",
|
||||
"endpoint_mode_manual": "Manual",
|
||||
"manual_endpoint_host": "Manual host",
|
||||
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
|
||||
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
|
||||
"manual_endpoint_port": "Manual public port",
|
||||
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
|
||||
"app_port": "App port",
|
||||
"app_port_message": "Internal container port exposed to Coolify routing.",
|
||||
"auto_gateway_endpoint": "Automatic gateway endpoint",
|
||||
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
|
||||
"auto_deploy": "Auto deploy",
|
||||
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
|
||||
"deploy_now": "Deploy nu",
|
||||
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
|
||||
"create_missing_service": "Opret manglende service",
|
||||
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
|
||||
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
|
||||
},
|
||||
"title": "Release Manager",
|
||||
"subtitle": "Gradvise API- og Front-End-udgivelser",
|
||||
"channel_names": {
|
||||
@@ -2613,7 +2542,7 @@
|
||||
"manual_fallback": "Manuel fallback",
|
||||
"related_deployment": "Relateret deployment",
|
||||
"recent_result": "Seneste resultat",
|
||||
"no_automated_action": "Ingen automatisk handling tilgængelig.",
|
||||
"no_automated_action": "Ingen automatisk handling tilgaengelig.",
|
||||
"deployment": "Deployment",
|
||||
"target": "Maal",
|
||||
"coolify_target": "Coolify-maal",
|
||||
@@ -2720,7 +2649,6 @@
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"use_local_frontend": "Brug lokal frontend",
|
||||
"switch_error": "Release-kanalen kunne ikke skiftes. Den forrige kanal er stadig aktiv."
|
||||
},
|
||||
"common": {
|
||||
@@ -2955,6 +2883,59 @@
|
||||
"severity": "Alvorlighed",
|
||||
"any_severity": "Alle alvorligheder",
|
||||
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "GitHub og Coolify",
|
||||
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
|
||||
"choose_repository": "Vælg repository",
|
||||
"add_health_url": "tilføj health-URL",
|
||||
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
|
||||
"open_github_token_settings": "Åbn GitHub token-indstillinger",
|
||||
"channel": "Kanal",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Eksempel: truckwash/backend-php",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: release/canary",
|
||||
"branch_placeholder": "branch",
|
||||
"latest_prefix": "Seneste: {commit}",
|
||||
"coolify_instance": "Coolify-instans",
|
||||
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
|
||||
"no_coolify_instance": "Ingen Coolify-instans",
|
||||
"coolify_instance_placeholder": "Coolify-instans-id",
|
||||
"coolify_service_uuid": "Coolify service-UUID",
|
||||
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
|
||||
"coolify_service_uuid_placeholder": "Coolify service-UUID",
|
||||
"health_url": "Health-URL",
|
||||
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
|
||||
"health_url_placeholder": "Health-URL",
|
||||
"ssl_domain": "Load balancer-domæne",
|
||||
"ssl_domain_message": "Skal være et DNS-domæne, der routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
|
||||
"ssl_domain_placeholder": "Load balancer-domæne",
|
||||
"https_domain_for_coolify": "Domæne kontrolleret af load balanceren",
|
||||
"endpoint_mode": "Endpoint mode",
|
||||
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
|
||||
"endpoint_mode_auto": "Auto",
|
||||
"endpoint_mode_manual": "Manual",
|
||||
"manual_endpoint_host": "Manual host",
|
||||
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
|
||||
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
|
||||
"manual_endpoint_port": "Manual public port",
|
||||
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
|
||||
"app_port": "App port",
|
||||
"app_port_message": "Internal container port exposed to Coolify routing.",
|
||||
"auto_gateway_endpoint": "Automatic gateway endpoint",
|
||||
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
|
||||
"auto_deploy": "Auto deploy",
|
||||
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
|
||||
"deploy_now": "Deploy nu",
|
||||
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
|
||||
"create_missing_service": "Opret manglende service",
|
||||
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
|
||||
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3212,34 +3193,31 @@
|
||||
"subtitle": "@.capitalize:{'words.generated.administrer'} @:{'words.generated.medarbejdere'}"
|
||||
},
|
||||
"error_report": {
|
||||
"button": "Rapporter fejl",
|
||||
"title": "Rapporter fejl",
|
||||
"subtitle": "Send det aktuelle skærmbillede og de seneste fejldetaljer til support.",
|
||||
"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.",
|
||||
"submit": "Send rapport",
|
||||
"submitted": "Fejlrapporten er sendt.",
|
||||
"capture_failed": "Skærmbilledet kunne ikke oprettes. Prøv igen.",
|
||||
"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",
|
||||
"screenshot": "Skærmbillede",
|
||||
"request_errors": "Request-fejl",
|
||||
"vue_errors": "Vue-fejl",
|
||||
"runtime_context": "Runtime-kontekst",
|
||||
"empty": "Ingen fejlrapporter fundet.",
|
||||
"resolve": "Marker som løst",
|
||||
"reopen": "Genåbn",
|
||||
"resolution_note": "Løsningsnote",
|
||||
"button": "Report error",
|
||||
"title": "Report error",
|
||||
"subtitle": "Send the current screen and recent error details to support.",
|
||||
"before_error": "What were you doing before the error occurred?",
|
||||
"expected": "What did you expect would happen?",
|
||||
"actual": "What actually happened?",
|
||||
"consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
|
||||
"submit": "Submit report",
|
||||
"submitted": "Error report submitted.",
|
||||
"capture_failed": "The screen capture failed. Please try again.",
|
||||
"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",
|
||||
"screenshot": "Screenshot",
|
||||
"request_errors": "Request errors",
|
||||
"vue_errors": "Vue errors",
|
||||
"runtime_context": "Runtime context",
|
||||
"empty": "No error reports found.",
|
||||
"resolve": "Mark resolved",
|
||||
"reopen": "Reopen",
|
||||
"resolution_note": "Resolution note",
|
||||
"filters": {
|
||||
"open": "Åben",
|
||||
"resolved": "Løst",
|
||||
"all": "Alle"
|
||||
"open": "Open",
|
||||
"resolved": "Resolved",
|
||||
"all": "All"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
@@ -4104,7 +4082,6 @@
|
||||
"products": {
|
||||
"columns": {
|
||||
"economic_product": "@.upper:{'words.generated.e'}-@:{'words.generated.conomic'} @:{'words.generated.produkt'}",
|
||||
"is_wash": "Er vask",
|
||||
"piktogram": "Piktogram",
|
||||
"requires_note": "@.capitalize:{'words.generated.kræver'} @:{'words.generated.note'}",
|
||||
"show_in_booking_form": "@:{'words.generated.vis'} @:{'words.generated.i'} @:{'words.generated.booking'} @:{'words.generated.formular'}",
|
||||
@@ -4395,6 +4372,7 @@
|
||||
"lift": "Lift",
|
||||
"mandskabsvogn": "Mandskabsvogn",
|
||||
"motorcykel": "Motorcykel",
|
||||
"s??ttevogn": "Semi-@:{'words.generated.trailer'}",
|
||||
"sættevogn": "Semitrailer",
|
||||
"trailer": "@:{'templates.generated.compat.vehicles.types.trailer'}",
|
||||
"traktor": "@:{'templates.generated.compat.vehicles.types.tractor'}",
|
||||
@@ -4529,7 +4507,7 @@
|
||||
"allow_preseeded_replica": "@.capitalize:{'words.generated.replikaen'} @:{'words.generated.er'} @:{'words.generated.allerede'} @:{'words.generated.sikkert'} @:{'words.generated.klargjort'}",
|
||||
"buckets": "@.capitalize:{'words.generated.buckets'}",
|
||||
"console_port": "Konsolport",
|
||||
"replication_transfer_limit": "Overførselsgrænse",
|
||||
"replication_transfer_limit": "Overfoerselsgraense",
|
||||
"database_index": "Databaseindeks",
|
||||
"port": "@.capitalize:{'words.generated.port'}",
|
||||
"replication_password": "Replikeringsadgangskode",
|
||||
@@ -4593,9 +4571,7 @@
|
||||
"loading_department": "@.capitalize:{'words.generated.indlæser'} @:{'words.generated.afdeling'}...",
|
||||
"loading_types": "@.capitalize:{'words.generated.indlæser'} @:{'words.generated.typer'}...",
|
||||
"loading_vehicle_types": "@.capitalize:{'words.generated.indlæser'} @:{'words.generated.tilgængelige'} @:{'words.generated.køretøjstyper'}...",
|
||||
"lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
||||
"machine": "@.capitalize:{'words.generated.maskine'}",
|
||||
"machine_unavailable_for_lane": "Maskinvask er ikke tilgængelig for den valgte bane. Vælg manuel vask eller en anden ledig bane.",
|
||||
"no_departments_found": "@:{'templates.generated.compat.departments.no_departments'}",
|
||||
"occupied": "Optaget",
|
||||
"open_property_access_gate": "@:{'words.generated.abn'} adgangsport",
|
||||
@@ -4611,10 +4587,6 @@
|
||||
"start_wash": "@:{'templates.generated.compat.guest.start_wash'}",
|
||||
"total_time": "@.capitalize:{'words.generated.samlet'} @:{'words.generated.tid'}: {minutes} @:{'words.generated.minutter'} @:{'words.generated.og'} {seconds} @:{'words.generated.sekunder'}",
|
||||
"unavailable": "@.capitalize:{'words.generated.utilgængelig'}",
|
||||
"vehicle_step_missing_customer_number": "Indtast kundenummer for at fortsætte.",
|
||||
"vehicle_step_missing_department": "Vent på, at nærmeste afdeling er indlæst, før du fortsætter.",
|
||||
"vehicle_step_missing_registration": "Indtast registreringsnummer for at fortsætte.",
|
||||
"vehicle_step_missing_vehicle_type": "Vælg en tilgængelig køretøjstype for at fortsætte.",
|
||||
"wash_completed_contact": "@.capitalize:{'words.generated.vi'} håber, @:{'words.generated.at'} @:{'words.generated.du'} @:{'words.generated.er'} tilfreds @:{'words.generated.med'} resultatet. @.capitalize:{'words.generated.hvis'} @:{'words.generated.du'} @:{'words.generated.har'} @:{'words.generated.brug'} @:{'words.generated.for'} @:{'words.generated.yderligere'} assistance, @:{'words.generated.er'} @:{'words.generated.du'} @:{'words.generated.velkommen'} @:{'words.generated.til'} @:{'words.generated.at'} kontakte @:{'words.generated.vores'} personale.",
|
||||
"wash_completed_message": "@.capitalize:{'words.generated.din'} @:{'words.generated.vask'} @:{'words.generated.er'} @:{'words.generated.nu'} @:{'words.generated.færdig'}! @:{'words.generated.tak'} @:{'words.generated.fordi'} @:{'words.generated.du'} brugte @:{'words.generated.vores'} selvbetjeningsvask.",
|
||||
"wash_completed": "@.capitalize:{'words.generated.vask'} @:{'words.generated.færdig'}",
|
||||
|
||||
+80
-125
@@ -1061,7 +1061,7 @@
|
||||
"uhrzeit": "Uhrzeit",
|
||||
"um": "um",
|
||||
"umbenannt": "umbenannt",
|
||||
"umbenennen": "@:{'words.generated.rename'}",
|
||||
"umbenennen": "umbenennen",
|
||||
"umsatz": "Umsatz",
|
||||
"umweltgeb": "Umweltgeb",
|
||||
"unabhangig": "unabhängig",
|
||||
@@ -1709,25 +1709,7 @@
|
||||
"view_lane_new_tab": "@:{'words.generated.spur'} @:{'words.generated.in'} @:{'words.generated.neuem'} @:{'words.generated.tab'} @:{'words.generated.anzeigen'}",
|
||||
"view_lane_setup_new_tab": "Spurkonfiguration @:{'words.generated.in'} @:{'words.generated.neuem'} @:{'words.generated.tab'} @:{'words.generated.anzeigen'}",
|
||||
"view_order_new_tab": "@:{'words.generated.auftrag'} @:{'words.generated.in'} @:{'words.generated.neuem'} @:{'words.generated.tab'} @:{'words.generated.anzeigen'}",
|
||||
"view_vehicle_new_tab": "@:{'words.generated.fahrzeug'} {reg} @:{'words.generated.in'} @:{'words.generated.neuem'} @:{'words.generated.tab'} @:{'words.generated.anzeigen'}",
|
||||
"accept_self_serve_wash": "Self-Service-Wäsche akzeptieren",
|
||||
"accept_self_serve_wash_confirm": "Diesen Entwurf mit den Self-Service-Details zu Kunde #{customerNumber} verschieben?",
|
||||
"accept_self_serve_wash_success": "Die Self-Service-Wäsche wurde akzeptiert.",
|
||||
"email_notifications_section": "E-Mail-Benachrichtigungen",
|
||||
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
|
||||
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
|
||||
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
|
||||
"resend_booking_confirmation": "(Re)send booking confirmation",
|
||||
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
|
||||
"resend_booking_confirmation_success": "The booking confirmation has been sent",
|
||||
"resend_wash_certificate": "(Re)send wash certificate",
|
||||
"resend_wash_certificate_error": "The wash certificate could not be sent",
|
||||
"resend_wash_certificate_success": "The wash certificate has been sent",
|
||||
"self_serve_customer": "Kunde",
|
||||
"self_serve_driver": "Fahrer",
|
||||
"self_serve_elapsed": "Zeit",
|
||||
"self_serve_wash_attachment": "Self-Service-Wäsche",
|
||||
"self_serve_wash_attachment_for_customer": "Self-Service-Wäsche für Kunde #{customerNumber}"
|
||||
"view_vehicle_new_tab": "@:{'words.generated.fahrzeug'} {reg} @:{'words.generated.in'} @:{'words.generated.neuem'} @:{'words.generated.tab'} @:{'words.generated.anzeigen'}"
|
||||
},
|
||||
"show_details": "@.capitalize:{'words.generated.details'} @:{'words.generated.anzeigen'}",
|
||||
"show_order_details": "@:{'words.generated.auftragsdetails'} @:{'words.generated.anzeigen'}",
|
||||
@@ -2567,59 +2549,6 @@
|
||||
"username_desc": "@.capitalize:{'words.generated.der'} @:{'words.generated.benutzername'} @:{'words.generated.wird'} @:{'words.generated.zur'} @:{'words.generated.authentifizierung'} @:{'words.generated.der'} @:{'words.generated.api'}-@:{'words.generated.verbindung'} @:{'words.generated.verwendet'}."
|
||||
},
|
||||
"release_manager": {
|
||||
"integrations": {
|
||||
"title": "GitHub und Coolify",
|
||||
"subtitle": "GitHub-Repositories und Branches kanalspezifischen Coolify-Services zuordnen.",
|
||||
"choose_repository": "Repository auswählen",
|
||||
"add_health_url": "Health-URL hinzufügen",
|
||||
"github_token_not_configured": "GitHub-Token ist nicht konfiguriert; private Repositories können nicht aufgelistet werden.",
|
||||
"open_github_token_settings": "GitHub-Token-Einstellungen öffnen",
|
||||
"channel": "Channel",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Example: truckwash/backend-php",
|
||||
"repository_placeholder": "owner/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Example: release/canary",
|
||||
"branch_placeholder": "branch",
|
||||
"latest_prefix": "Neueste: {commit}",
|
||||
"coolify_instance": "Coolify-Instanz",
|
||||
"coolify_instance_message": "Leer lassen für manuelle Deployment-Einträge.",
|
||||
"no_coolify_instance": "Keine Coolify-Instanz",
|
||||
"coolify_instance_placeholder": "Coolify-Instanz-ID",
|
||||
"coolify_service_uuid": "Coolify service UUID",
|
||||
"coolify_service_uuid_message": "Bestehenden Service auswählen oder UUID einfügen.",
|
||||
"coolify_service_uuid_placeholder": "Coolify service UUID",
|
||||
"health_url": "Health URL",
|
||||
"health_url_message": "Example: https://api-canary.example.test/ping",
|
||||
"health_url_placeholder": "Health URL",
|
||||
"ssl_domain": "Load-Balancer-Domain",
|
||||
"ssl_domain_message": "Muss eine DNS-Domain sein, die zum Coolify Load Balancer geroutet wird. Beispiel: api-v2.truckwash.io",
|
||||
"ssl_domain_placeholder": "Load-Balancer-Domain",
|
||||
"https_domain_for_coolify": "Domain unter Kontrolle des Load Balancers",
|
||||
"endpoint_mode": "Endpoint mode",
|
||||
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
|
||||
"endpoint_mode_auto": "Auto",
|
||||
"endpoint_mode_manual": "Manual",
|
||||
"manual_endpoint_host": "Manual host",
|
||||
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
|
||||
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
|
||||
"manual_endpoint_port": "Manual public port",
|
||||
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
|
||||
"app_port": "App port",
|
||||
"app_port_message": "Internal container port exposed to Coolify routing.",
|
||||
"auto_gateway_endpoint": "Automatic gateway endpoint",
|
||||
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
|
||||
"auto_deploy": "Auto-Deploy",
|
||||
"auto_deploy_tooltip": "Automatisch Deployments erstellen, wenn sich dieses Ziel ändert.",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"coolify_ssl_tooltip": "Coolify anweisen, HTTPS für die konfigurierte Domain bereitzustellen.",
|
||||
"deploy_now": "Jetzt deployen",
|
||||
"deploy_now_tooltip": "Deployment direkt nach dem Speichern des Ziels starten.",
|
||||
"create_missing_service": "Fehlenden Service erstellen",
|
||||
"create_missing_service_tooltip": "Release Manager erlauben, einen fehlenden Coolify-Service zu erstellen.",
|
||||
"test_access_tooltip": "Prüfen, ob der konfigurierte GitHub-Token dieses Repository und den Branch lesen kann."
|
||||
},
|
||||
"title": "Release Manager",
|
||||
"subtitle": "Schrittweise API- und Front-End-Releases",
|
||||
"channel_names": {
|
||||
@@ -2814,9 +2743,7 @@
|
||||
"release_runtime": "Release-Laufzeit",
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"use_local_frontend": "Lokales Frontend verwenden",
|
||||
"switch_error": "Release channel could not be switched. The previous channel is still active."
|
||||
"api": "API"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -2840,8 +2767,7 @@
|
||||
"save_target": "Ziel speichern",
|
||||
"delete": "Löschen",
|
||||
"back": "Zurück",
|
||||
"next": "Weiter",
|
||||
"publish_release": "Publish release"
|
||||
"next": "Weiter"
|
||||
},
|
||||
"tables": {
|
||||
"channel": "Kanal",
|
||||
@@ -3051,20 +2977,58 @@
|
||||
"any_severity": "Beliebiger Schweregrad",
|
||||
"no_timeline_events": "Keine Timeline-Events entsprechen den aktuellen Filtern."
|
||||
},
|
||||
"status": {
|
||||
"confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.",
|
||||
"choose_bundle_prompt": "Enter the bundle id to set for this channel.",
|
||||
"impact": "Impact",
|
||||
"cause": "Cause",
|
||||
"automated_fix": "Automated fix",
|
||||
"manual_fallback": "Manual fallback",
|
||||
"related_deployment": "Related deployment",
|
||||
"recent_result": "Recent result",
|
||||
"no_automated_action": "No automated action available.",
|
||||
"deployment": "Deployment",
|
||||
"target": "Target",
|
||||
"coolify_target": "Coolify target",
|
||||
"service_set": "Service set"
|
||||
"integrations": {
|
||||
"title": "GitHub und Coolify",
|
||||
"subtitle": "GitHub-Repositories und Branches kanalspezifischen Coolify-Services zuordnen.",
|
||||
"choose_repository": "Repository auswählen",
|
||||
"add_health_url": "Health-URL hinzufügen",
|
||||
"github_token_not_configured": "GitHub-Token ist nicht konfiguriert; private Repositories können nicht aufgelistet werden.",
|
||||
"open_github_token_settings": "GitHub-Token-Einstellungen öffnen",
|
||||
"channel": "Channel",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Example: truckwash/backend-php",
|
||||
"repository_placeholder": "owner/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Example: release/canary",
|
||||
"branch_placeholder": "branch",
|
||||
"latest_prefix": "Neueste: {commit}",
|
||||
"coolify_instance": "Coolify-Instanz",
|
||||
"coolify_instance_message": "Leer lassen für manuelle Deployment-Einträge.",
|
||||
"no_coolify_instance": "Keine Coolify-Instanz",
|
||||
"coolify_instance_placeholder": "Coolify-Instanz-ID",
|
||||
"coolify_service_uuid": "Coolify service UUID",
|
||||
"coolify_service_uuid_message": "Bestehenden Service auswählen oder UUID einfügen.",
|
||||
"coolify_service_uuid_placeholder": "Coolify service UUID",
|
||||
"health_url": "Health URL",
|
||||
"health_url_message": "Example: https://api-canary.example.test/ping",
|
||||
"health_url_placeholder": "Health URL",
|
||||
"ssl_domain": "Load-Balancer-Domain",
|
||||
"ssl_domain_message": "Muss eine DNS-Domain sein, die zum Coolify Load Balancer geroutet wird. Beispiel: api-v2.truckwash.io",
|
||||
"ssl_domain_placeholder": "Load-Balancer-Domain",
|
||||
"https_domain_for_coolify": "Domain unter Kontrolle des Load Balancers",
|
||||
"endpoint_mode": "Endpoint mode",
|
||||
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
|
||||
"endpoint_mode_auto": "Auto",
|
||||
"endpoint_mode_manual": "Manual",
|
||||
"manual_endpoint_host": "Manual host",
|
||||
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
|
||||
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
|
||||
"manual_endpoint_port": "Manual public port",
|
||||
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
|
||||
"app_port": "App port",
|
||||
"app_port_message": "Internal container port exposed to Coolify routing.",
|
||||
"auto_gateway_endpoint": "Automatic gateway endpoint",
|
||||
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
|
||||
"auto_deploy": "Auto-Deploy",
|
||||
"auto_deploy_tooltip": "Automatisch Deployments erstellen, wenn sich dieses Ziel ändert.",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"coolify_ssl_tooltip": "Coolify anweisen, HTTPS für die konfigurierte Domain bereitzustellen.",
|
||||
"deploy_now": "Jetzt deployen",
|
||||
"deploy_now_tooltip": "Deployment direkt nach dem Speichern des Ziels starten.",
|
||||
"create_missing_service": "Fehlenden Service erstellen",
|
||||
"create_missing_service_tooltip": "Release Manager erlauben, einen fehlenden Coolify-Service zu erstellen.",
|
||||
"test_access_tooltip": "Prüfen, ob der konfigurierte GitHub-Token dieses Repository und den Branch lesen kann."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3322,34 +3286,31 @@
|
||||
"subtitle": "@:{'words.generated.mitarbeiter'} @:{'words.generated.verwalten'}"
|
||||
},
|
||||
"error_report": {
|
||||
"button": "Fehler melden",
|
||||
"title": "Fehler melden",
|
||||
"subtitle": "Sende den aktuellen Bildschirm und die letzten Fehlerdetails an den Support.",
|
||||
"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.",
|
||||
"submit": "Bericht senden",
|
||||
"submitted": "Fehlerbericht gesendet.",
|
||||
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
|
||||
"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",
|
||||
"button": "Report error",
|
||||
"title": "Report error",
|
||||
"subtitle": "Send the current screen and recent error details to support.",
|
||||
"before_error": "What were you doing before the error occurred?",
|
||||
"expected": "What did you expect would happen?",
|
||||
"actual": "What actually happened?",
|
||||
"consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
|
||||
"submit": "Submit report",
|
||||
"submitted": "Error report submitted.",
|
||||
"capture_failed": "The screen capture failed. Please try again.",
|
||||
"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",
|
||||
"screenshot": "Screenshot",
|
||||
"request_errors": "Request-Fehler",
|
||||
"vue_errors": "Vue-Fehler",
|
||||
"runtime_context": "Runtime-Kontext",
|
||||
"empty": "Keine Fehlerberichte gefunden.",
|
||||
"resolve": "Als gelöst markieren",
|
||||
"reopen": "Wieder öffnen",
|
||||
"resolution_note": "Lösungsnotiz",
|
||||
"request_errors": "Request errors",
|
||||
"vue_errors": "Vue errors",
|
||||
"runtime_context": "Runtime context",
|
||||
"empty": "No error reports found.",
|
||||
"resolve": "Mark resolved",
|
||||
"reopen": "Reopen",
|
||||
"resolution_note": "Resolution note",
|
||||
"filters": {
|
||||
"open": "Offen",
|
||||
"resolved": "Gelöst",
|
||||
"all": "Alle"
|
||||
"open": "Open",
|
||||
"resolved": "Resolved",
|
||||
"all": "All"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
@@ -4214,7 +4175,6 @@
|
||||
"products": {
|
||||
"columns": {
|
||||
"economic_product": "@.upper:{'words.generated.e'}-@:{'words.generated.conomic'}-@.capitalize:{'words.generated.produkt'}",
|
||||
"is_wash": "Ist Wäsche",
|
||||
"piktogram": "Piktogramm",
|
||||
"requires_note": "@:{'words.generated.notiz'} @:{'words.generated.erforderlich'}",
|
||||
"show_in_booking_form": "@.capitalize:{'words.generated.im'} Buchungsformular @:{'words.generated.anzeigen'}",
|
||||
@@ -4505,6 +4465,7 @@
|
||||
"lift": "Lift",
|
||||
"mandskabsvogn": "Mannschaftswagen",
|
||||
"motorcykel": "Motorrad",
|
||||
"s??ttevogn": "Semi-trailer",
|
||||
"sættevogn": "Sattelauflieger",
|
||||
"trailer": "Tilhenger",
|
||||
"traktor": "@:{'words.generated.zugmaschine'}",
|
||||
@@ -4703,9 +4664,7 @@
|
||||
"loading_department": "@:{'words.generated.abteilung'} @:{'words.generated.wird'} @:{'words.generated.geladen'}...",
|
||||
"loading_types": "@.capitalize:{'words.generated.typen'} @:{'words.generated.werden'} @:{'words.generated.geladen'}...",
|
||||
"loading_vehicle_types": "@.capitalize:{'words.generated.verf'}?@:{'words.generated.gbare'} @:{'words.generated.fahrzeugtypen'} @:{'words.generated.werden'} @:{'words.generated.geladen'}...",
|
||||
"lane_unavailable": "Waschspur nicht verfugbar",
|
||||
"machine": "@:{'words.generated.maschine'}",
|
||||
"machine_unavailable_for_lane": "Maschinenwasche ist fur die ausgewahlte Spur nicht verfugbar. Wahlen Sie manuelle Wasche oder eine andere freie Spur.",
|
||||
"no_departments_found": "@:{'templates.generated.compat.departments.no_departments'}",
|
||||
"occupied": "Belegt",
|
||||
"open_property_access_gate": "Grundstueckszugangstor @:{'words.generated.oeffnen'}",
|
||||
@@ -4721,10 +4680,6 @@
|
||||
"start_wash": "@:{'templates.generated.compat.guest.start_wash'}",
|
||||
"total_time": "Gesamtzeit: {minutes} Minuten @:{'words.generated.und'} {seconds} @:{'words.generated.sekunden'}",
|
||||
"unavailable": "@.capitalize:{'words.generated.nicht'} @:{'words.generated.verfugbar'}",
|
||||
"vehicle_step_missing_customer_number": "Geben Sie Ihre Kundennummer ein, um fortzufahren.",
|
||||
"vehicle_step_missing_department": "Warten Sie, bis die nachste Abteilung geladen ist, bevor Sie fortfahren.",
|
||||
"vehicle_step_missing_registration": "Geben Sie das Kennzeichen ein, um fortzufahren.",
|
||||
"vehicle_step_missing_vehicle_type": "Wahlen Sie einen verfugbaren Fahrzeugtyp, um fortzufahren.",
|
||||
"wash_completed_contact": "@.capitalize:{'words.generated.wir'} hoffen, @.capitalize:{'words.generated.sie'} @:{'words.generated.sind'} @:{'words.generated.mit'} @:{'words.generated.dem'} @:{'words.generated.ergebnis'} zufrieden. @.capitalize:{'words.generated.wenn'} @.capitalize:{'words.generated.sie'} @:{'words.generated.weitere'} @:{'words.generated.hilfe'} @:{'words.generated.ben'}?@:{'words.generated.tigen'}, @:{'words.generated.kontaktieren'} @.capitalize:{'words.generated.sie'} @:{'words.generated.bitte'} @:{'words.generated.unser'} Personal.",
|
||||
"wash_completed_message": "@.capitalize:{'words.generated.ihre'} @.upper:{'words.generated.w'}?@:{'words.generated.sche'} @:{'words.generated.ist'} @:{'words.generated.jetzt'} @:{'words.generated.abgeschlossen'}! @:{'words.generated.danke'}, @:{'words.generated.dass'} @.capitalize:{'words.generated.sie'} @:{'words.generated.unsere'} Selbstbedienungsw?@:{'words.generated.sche'} genutzt @:{'words.generated.haben'}.",
|
||||
"wash_completed": "@.upper:{'words.generated.w'}?@:{'words.generated.sche'} @:{'words.generated.abgeschlossen'}",
|
||||
|
||||
@@ -1433,25 +1433,7 @@
|
||||
"view_lane_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.lane'} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
|
||||
"view_lane_setup_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.lane'} @:{'words.generated.setup'} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
|
||||
"view_order_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.order'} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
|
||||
"view_vehicle_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.vehicle'} {reg} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
|
||||
"accept_self_serve_wash": "Accept self-serve wash",
|
||||
"accept_self_serve_wash_confirm": "Move this draft to customer #{customerNumber} with the self-serve details?",
|
||||
"accept_self_serve_wash_success": "The self-serve wash was accepted.",
|
||||
"email_notifications_section": "Email notifications",
|
||||
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
|
||||
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
|
||||
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
|
||||
"resend_booking_confirmation": "(Re)send booking confirmation",
|
||||
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
|
||||
"resend_booking_confirmation_success": "The booking confirmation has been sent",
|
||||
"resend_wash_certificate": "(Re)send wash certificate",
|
||||
"resend_wash_certificate_error": "The wash certificate could not be sent",
|
||||
"resend_wash_certificate_success": "The wash certificate has been sent",
|
||||
"self_serve_customer": "Customer",
|
||||
"self_serve_driver": "Driver",
|
||||
"self_serve_elapsed": "Elapsed",
|
||||
"self_serve_wash_attachment": "Self-serve wash",
|
||||
"self_serve_wash_attachment_for_customer": "Self-serve wash for customer #{customerNumber}"
|
||||
"view_vehicle_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.vehicle'} {reg} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}"
|
||||
},
|
||||
"show_details": "@.capitalize:{'words.generated.show'} @:{'words.generated.details'}",
|
||||
"show_order_details": "@.capitalize:{'words.generated.show'} @:{'words.generated.order'} @:{'words.generated.details'}",
|
||||
@@ -2291,59 +2273,6 @@
|
||||
"username_desc": "@.capitalize:{'words.replication.article.host_mention'} @:{'words.generated.username'} @:{'words.generated.is'} @:{'words.generated.used'} @:{'words.generated.to'} @:{'words.generated.authenticate'} @:{'words.replication.article.host_mention'} @:{'words.generated.api'} @:{'words.generated.connection'}."
|
||||
},
|
||||
"release_manager": {
|
||||
"integrations": {
|
||||
"title": "GitHub and Coolify",
|
||||
"subtitle": "Map GitHub repositories and branches to channel-specific Coolify services.",
|
||||
"choose_repository": "Choose repository",
|
||||
"add_health_url": "add health URL",
|
||||
"github_token_not_configured": "GitHub token is not configured; private repositories cannot be listed.",
|
||||
"open_github_token_settings": "Open GitHub token settings",
|
||||
"channel": "Channel",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Example: truckwash/backend-php",
|
||||
"repository_placeholder": "owner/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Example: release/canary",
|
||||
"branch_placeholder": "branch",
|
||||
"latest_prefix": "Latest: {commit}",
|
||||
"coolify_instance": "Coolify instance",
|
||||
"coolify_instance_message": "Leave empty for manual deployment records.",
|
||||
"no_coolify_instance": "No Coolify instance",
|
||||
"coolify_instance_placeholder": "Coolify instance id",
|
||||
"coolify_service_uuid": "Coolify service UUID",
|
||||
"coolify_service_uuid_message": "Select an existing service or paste a UUID.",
|
||||
"coolify_service_uuid_placeholder": "Coolify service UUID",
|
||||
"health_url": "Health URL",
|
||||
"health_url_message": "Example: https://api-canary.example.test/ping",
|
||||
"health_url_placeholder": "Health URL",
|
||||
"ssl_domain": "Load balancer domain",
|
||||
"ssl_domain_message": "Must be a DNS domain routed to the Coolify load balancer. Example: api-v2.truckwash.io",
|
||||
"ssl_domain_placeholder": "Load balancer domain",
|
||||
"https_domain_for_coolify": "Domain controlled by the load balancer",
|
||||
"endpoint_mode": "Endpoint mode",
|
||||
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
|
||||
"endpoint_mode_auto": "Auto",
|
||||
"endpoint_mode_manual": "Manual",
|
||||
"manual_endpoint_host": "Manual host",
|
||||
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
|
||||
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
|
||||
"manual_endpoint_port": "Manual public port",
|
||||
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
|
||||
"app_port": "App port",
|
||||
"app_port_message": "Internal container port exposed to Coolify routing.",
|
||||
"auto_gateway_endpoint": "Automatic gateway endpoint",
|
||||
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
|
||||
"auto_deploy": "Auto deploy",
|
||||
"auto_deploy_tooltip": "Automatically create deployments when this target changes.",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"coolify_ssl_tooltip": "Ask Coolify to provision HTTPS for the configured domain.",
|
||||
"deploy_now": "Deploy now",
|
||||
"deploy_now_tooltip": "Start a deployment immediately after saving the target.",
|
||||
"create_missing_service": "Create missing service",
|
||||
"create_missing_service_tooltip": "Allow Release Manager to create a missing Coolify service.",
|
||||
"test_access_tooltip": "Verify the configured GitHub token can read this repository and branch."
|
||||
},
|
||||
"title": "Release Manager",
|
||||
"subtitle": "Gradual API and Front-End releases",
|
||||
"channel_names": {
|
||||
@@ -2554,7 +2483,6 @@
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"use_local_frontend": "Use local frontend",
|
||||
"switch_error": "Release channel could not be switched. The previous channel is still active."
|
||||
},
|
||||
"common": {
|
||||
@@ -2789,6 +2717,59 @@
|
||||
"severity": "Severity",
|
||||
"any_severity": "Any severity",
|
||||
"no_timeline_events": "No timeline events match the current filters."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "GitHub and Coolify",
|
||||
"subtitle": "Map GitHub repositories and branches to channel-specific Coolify services.",
|
||||
"choose_repository": "Choose repository",
|
||||
"add_health_url": "add health URL",
|
||||
"github_token_not_configured": "GitHub token is not configured; private repositories cannot be listed.",
|
||||
"open_github_token_settings": "Open GitHub token settings",
|
||||
"channel": "Channel",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Example: truckwash/backend-php",
|
||||
"repository_placeholder": "owner/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Example: release/canary",
|
||||
"branch_placeholder": "branch",
|
||||
"latest_prefix": "Latest: {commit}",
|
||||
"coolify_instance": "Coolify instance",
|
||||
"coolify_instance_message": "Leave empty for manual deployment records.",
|
||||
"no_coolify_instance": "No Coolify instance",
|
||||
"coolify_instance_placeholder": "Coolify instance id",
|
||||
"coolify_service_uuid": "Coolify service UUID",
|
||||
"coolify_service_uuid_message": "Select an existing service or paste a UUID.",
|
||||
"coolify_service_uuid_placeholder": "Coolify service UUID",
|
||||
"health_url": "Health URL",
|
||||
"health_url_message": "Example: https://api-canary.example.test/ping",
|
||||
"health_url_placeholder": "Health URL",
|
||||
"ssl_domain": "Load balancer domain",
|
||||
"ssl_domain_message": "Must be a DNS domain routed to the Coolify load balancer. Example: api-v2.truckwash.io",
|
||||
"ssl_domain_placeholder": "Load balancer domain",
|
||||
"https_domain_for_coolify": "Domain controlled by the load balancer",
|
||||
"endpoint_mode": "Endpoint mode",
|
||||
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
|
||||
"endpoint_mode_auto": "Auto",
|
||||
"endpoint_mode_manual": "Manual",
|
||||
"manual_endpoint_host": "Manual host",
|
||||
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
|
||||
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
|
||||
"manual_endpoint_port": "Manual public port",
|
||||
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
|
||||
"app_port": "App port",
|
||||
"app_port_message": "Internal container port exposed to Coolify routing.",
|
||||
"auto_gateway_endpoint": "Automatic gateway endpoint",
|
||||
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
|
||||
"auto_deploy": "Auto deploy",
|
||||
"auto_deploy_tooltip": "Automatically create deployments when this target changes.",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"coolify_ssl_tooltip": "Ask Coolify to provision HTTPS for the configured domain.",
|
||||
"deploy_now": "Deploy now",
|
||||
"deploy_now_tooltip": "Start a deployment immediately after saving the target.",
|
||||
"create_missing_service": "Create missing service",
|
||||
"create_missing_service_tooltip": "Allow Release Manager to create a missing Coolify service.",
|
||||
"test_access_tooltip": "Verify the configured GitHub token can read this repository and branch."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3052,9 +3033,6 @@
|
||||
"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.",
|
||||
"submit": "Submit report",
|
||||
"submitted": "Error report submitted.",
|
||||
@@ -3938,7 +3916,6 @@
|
||||
"products": {
|
||||
"columns": {
|
||||
"economic_product": "@.upper:{'words.generated.e'}-@:{'words.generated.conomic'} @.capitalize:{'words.generated.product'}",
|
||||
"is_wash": "Is Wash",
|
||||
"piktogram": "Pictogram",
|
||||
"requires_note": "@.capitalize:{'words.generated.requires'} @.capitalize:{'words.generated.note'}",
|
||||
"show_in_booking_form": "@.capitalize:{'words.generated.show'} @:{'words.generated.in'} @.capitalize:{'words.generated.booking'} @.capitalize:{'words.generated.form'}",
|
||||
@@ -4229,6 +4206,7 @@
|
||||
"lift": "Lift",
|
||||
"mandskabsvogn": "@:{'templates.generated.compat.vehicles.vehicle_types.crew'}",
|
||||
"motorcykel": "Motorcycle",
|
||||
"s??ttevogn": "@:{'templates.generated.compat.vehicles.vehicle_types.semi'}",
|
||||
"sættevogn": "Semitrailer",
|
||||
"trailer": "@:{'templates.generated.compat.vehicles.types.trailer'}",
|
||||
"traktor": "Trekkbil",
|
||||
@@ -4427,9 +4405,7 @@
|
||||
"loading_department": "@.capitalize:{'words.generated.loading'} @:{'words.generated.department'}...",
|
||||
"loading_types": "@.capitalize:{'words.generated.loading'} @:{'words.generated.types'}...",
|
||||
"loading_vehicle_types": "@.capitalize:{'words.generated.loading'} @:{'words.generated.available'} @:{'words.generated.vehicle'} @:{'words.generated.types'}...",
|
||||
"lane_unavailable": "Lane unavailable",
|
||||
"machine": "@.capitalize:{'words.generated.machine'}",
|
||||
"machine_unavailable_for_lane": "Machine wash is unavailable for the selected lane. Choose manual wash or another available lane.",
|
||||
"no_departments_found": "@:{'templates.generated.compat.departments.no_departments'}",
|
||||
"occupied": "Occupied",
|
||||
"open_property_access_gate": "@.capitalize:{'words.generated.open'} @:{'words.generated.property'} @:{'words.generated.access'} @:{'words.generated.gate'}",
|
||||
@@ -4445,10 +4421,6 @@
|
||||
"start_wash": "@.capitalize:{'words.generated.start'} @:{'words.generated.wash'}",
|
||||
"total_time": "@.capitalize:{'words.generated.total'} @:{'words.generated.time'}: {minutes} @:{'words.generated.minutes'} @:{'words.generated.and'} {seconds} @:{'words.generated.seconds'}",
|
||||
"unavailable": "@.capitalize:{'words.generated.unavailable'}",
|
||||
"vehicle_step_missing_customer_number": "Enter your customer number to continue.",
|
||||
"vehicle_step_missing_department": "Wait for the nearest department to load before continuing.",
|
||||
"vehicle_step_missing_registration": "Enter your registration number to continue.",
|
||||
"vehicle_step_missing_vehicle_type": "Choose an available vehicle type to continue.",
|
||||
"wash_completed_contact": "@.capitalize:{'words.generated.we'} hope @:{'words.generated.you'} @:{'words.generated.are'} satisfied @:{'words.generated.with'} @:{'words.replication.article.host_mention'} @:{'words.generated.result'}. @.capitalize:{'words.generated.if'} @:{'words.generated.you'} @:{'words.generated.need'} @:{'words.generated.further'} @:{'words.generated.assistance'}, @:{'words.generated.please'} @:{'words.generated.contact'} @:{'words.generated.our'} staff.",
|
||||
"wash_completed_message": "@.capitalize:{'words.generated.your'} @:{'words.generated.wash'} @:{'words.generated.is'} @:{'words.generated.now'} @:{'words.generated.complete'}! @:{'words.generated.thank'} @:{'words.generated.you'} @:{'words.generated.for'} @:{'words.generated.using'} @:{'words.generated.our'} @:{'words.generated.self'}-@:{'words.generated.service'} @:{'words.generated.wash'}.",
|
||||
"wash_completed": "@.capitalize:{'words.generated.wash'} @:{'words.generated.completed'}",
|
||||
|
||||
@@ -549,25 +549,7 @@
|
||||
"view_lane_new_tab": "@:{'templates.generated.compat.admin.pos.settings_wheel.view_lane_new_tab'}",
|
||||
"view_lane_setup_new_tab": "@:{'templates.generated.compat.admin.pos.settings_wheel.view_lane_setup_new_tab'}",
|
||||
"view_order_new_tab": "@:{'templates.generated.compat.admin.pos.settings_wheel.view_order_new_tab'}",
|
||||
"view_vehicle_new_tab": "@:{'templates.generated.compat.admin.pos.settings_wheel.view_vehicle_new_tab'}",
|
||||
"accept_self_serve_wash": "@:{'templates.generated.compat.admin.pos.settings_wheel.accept_self_serve_wash'}",
|
||||
"accept_self_serve_wash_confirm": "@:{'templates.generated.compat.admin.pos.settings_wheel.accept_self_serve_wash_confirm'}",
|
||||
"accept_self_serve_wash_success": "@:{'templates.generated.compat.admin.pos.settings_wheel.accept_self_serve_wash_success'}",
|
||||
"email_notifications_section": "@:{'templates.generated.compat.admin.pos.settings_wheel.email_notifications_section'}",
|
||||
"resend_booking_completion_confirmation": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_booking_completion_confirmation'}",
|
||||
"resend_booking_completion_confirmation_error": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_booking_completion_confirmation_error'}",
|
||||
"resend_booking_completion_confirmation_success": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_booking_completion_confirmation_success'}",
|
||||
"resend_booking_confirmation": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_booking_confirmation'}",
|
||||
"resend_booking_confirmation_error": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_booking_confirmation_error'}",
|
||||
"resend_booking_confirmation_success": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_booking_confirmation_success'}",
|
||||
"resend_wash_certificate": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_wash_certificate'}",
|
||||
"resend_wash_certificate_error": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_wash_certificate_error'}",
|
||||
"resend_wash_certificate_success": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_wash_certificate_success'}",
|
||||
"self_serve_customer": "@:{'templates.generated.compat.admin.pos.settings_wheel.self_serve_customer'}",
|
||||
"self_serve_driver": "@:{'templates.generated.compat.admin.pos.settings_wheel.self_serve_driver'}",
|
||||
"self_serve_elapsed": "@:{'templates.generated.compat.admin.pos.settings_wheel.self_serve_elapsed'}",
|
||||
"self_serve_wash_attachment": "@:{'templates.generated.compat.admin.pos.settings_wheel.self_serve_wash_attachment'}",
|
||||
"self_serve_wash_attachment_for_customer": "@:{'templates.generated.compat.admin.pos.settings_wheel.self_serve_wash_attachment_for_customer'}"
|
||||
"view_vehicle_new_tab": "@:{'templates.generated.compat.admin.pos.settings_wheel.view_vehicle_new_tab'}"
|
||||
},
|
||||
"show_details": "@:{'templates.generated.compat.admin.pos.show_details'}",
|
||||
"show_order_details": "@:{'templates.generated.compat.admin.pos.show_order_details'}",
|
||||
@@ -1516,59 +1498,6 @@
|
||||
"username_desc": "@:{'templates.generated.compat.configuration.xlvask.username_desc'}"
|
||||
},
|
||||
"release_manager": {
|
||||
"integrations": {
|
||||
"title": "@:{'templates.generated.compat.configuration.release_manager.integrations.title'}",
|
||||
"subtitle": "@:{'templates.generated.compat.configuration.release_manager.integrations.subtitle'}",
|
||||
"choose_repository": "@:{'templates.generated.compat.configuration.release_manager.integrations.choose_repository'}",
|
||||
"add_health_url": "@:{'templates.generated.compat.configuration.release_manager.integrations.add_health_url'}",
|
||||
"github_token_not_configured": "@:{'templates.generated.compat.configuration.release_manager.integrations.github_token_not_configured'}",
|
||||
"open_github_token_settings": "@:{'templates.generated.compat.configuration.release_manager.integrations.open_github_token_settings'}",
|
||||
"channel": "@:{'templates.generated.compat.configuration.release_manager.integrations.channel'}",
|
||||
"app": "@:{'templates.generated.compat.configuration.release_manager.integrations.app'}",
|
||||
"repository": "@:{'templates.generated.compat.configuration.release_manager.integrations.repository'}",
|
||||
"repository_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.repository_message'}",
|
||||
"repository_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.repository_placeholder'}",
|
||||
"branch": "@:{'templates.generated.compat.configuration.release_manager.integrations.branch'}",
|
||||
"branch_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.branch_message'}",
|
||||
"branch_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.branch_placeholder'}",
|
||||
"latest_prefix": "@:{'templates.generated.compat.configuration.release_manager.integrations.latest_prefix'}",
|
||||
"coolify_instance": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_instance'}",
|
||||
"coolify_instance_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_instance_message'}",
|
||||
"no_coolify_instance": "@:{'templates.generated.compat.configuration.release_manager.integrations.no_coolify_instance'}",
|
||||
"coolify_instance_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_instance_placeholder'}",
|
||||
"coolify_service_uuid": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_service_uuid'}",
|
||||
"coolify_service_uuid_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_service_uuid_message'}",
|
||||
"coolify_service_uuid_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_service_uuid_placeholder'}",
|
||||
"health_url": "@:{'templates.generated.compat.configuration.release_manager.integrations.health_url'}",
|
||||
"health_url_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.health_url_message'}",
|
||||
"health_url_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.health_url_placeholder'}",
|
||||
"ssl_domain": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain'}",
|
||||
"ssl_domain_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain_message'}",
|
||||
"ssl_domain_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain_placeholder'}",
|
||||
"https_domain_for_coolify": "@:{'templates.generated.compat.configuration.release_manager.integrations.https_domain_for_coolify'}",
|
||||
"endpoint_mode": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode'}",
|
||||
"endpoint_mode_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_message'}",
|
||||
"endpoint_mode_auto": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_auto'}",
|
||||
"endpoint_mode_manual": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_manual'}",
|
||||
"manual_endpoint_host": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host'}",
|
||||
"manual_endpoint_host_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host_message'}",
|
||||
"manual_endpoint_host_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host_placeholder'}",
|
||||
"manual_endpoint_port": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_port'}",
|
||||
"manual_endpoint_port_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_port_message'}",
|
||||
"app_port": "@:{'templates.generated.compat.configuration.release_manager.integrations.app_port'}",
|
||||
"app_port_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.app_port_message'}",
|
||||
"auto_gateway_endpoint": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_gateway_endpoint'}",
|
||||
"pending_automatic_endpoint": "@:{'templates.generated.compat.configuration.release_manager.integrations.pending_automatic_endpoint'}",
|
||||
"auto_deploy": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_deploy'}",
|
||||
"auto_deploy_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_deploy_tooltip'}",
|
||||
"coolify_ssl": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_ssl'}",
|
||||
"coolify_ssl_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_ssl_tooltip'}",
|
||||
"deploy_now": "@:{'templates.generated.compat.configuration.release_manager.integrations.deploy_now'}",
|
||||
"deploy_now_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.deploy_now_tooltip'}",
|
||||
"create_missing_service": "@:{'templates.generated.compat.configuration.release_manager.integrations.create_missing_service'}",
|
||||
"create_missing_service_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.create_missing_service_tooltip'}",
|
||||
"test_access_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.test_access_tooltip'}"
|
||||
},
|
||||
"title": "@:{'templates.generated.compat.configuration.release_manager.title'}",
|
||||
"subtitle": "@:{'templates.generated.compat.configuration.release_manager.subtitle'}",
|
||||
"channel_names": {
|
||||
@@ -1746,8 +1675,7 @@
|
||||
"release_runtime": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.release_runtime'}",
|
||||
"bundle": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.bundle'}",
|
||||
"frontend": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.frontend'}",
|
||||
"api": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.api'}",
|
||||
"use_local_frontend": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.use_local_frontend'}"
|
||||
"api": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.api'}"
|
||||
},
|
||||
"common": {
|
||||
"yes": "@:{'templates.generated.compat.configuration.release_manager.common.yes'}",
|
||||
@@ -1981,22 +1909,58 @@
|
||||
"any_severity": "@:{'templates.generated.compat.configuration.release_manager.replay.any_severity'}",
|
||||
"no_timeline_events": "@:{'templates.generated.compat.configuration.release_manager.replay.no_timeline_events'}"
|
||||
},
|
||||
"loading": {
|
||||
"assignments": "@:{'templates.generated.compat.configuration.release_manager.loading.assignments'}",
|
||||
"channels": "@:{'templates.generated.compat.configuration.release_manager.loading.channels'}",
|
||||
"channels_detail": "@:{'templates.generated.compat.configuration.release_manager.loading.channels_detail'}",
|
||||
"data_services": "@:{'templates.generated.compat.configuration.release_manager.loading.data_services'}",
|
||||
"deployments": "@:{'templates.generated.compat.configuration.release_manager.loading.deployments'}",
|
||||
"github_repositories": "@:{'templates.generated.compat.configuration.release_manager.loading.github_repositories'}",
|
||||
"integrations": "@:{'templates.generated.compat.configuration.release_manager.loading.integrations'}",
|
||||
"operations": "@:{'templates.generated.compat.configuration.release_manager.loading.operations'}",
|
||||
"refreshing_summary": "@:{'templates.generated.compat.configuration.release_manager.loading.refreshing_summary'}",
|
||||
"refreshing_timeline": "@:{'templates.generated.compat.configuration.release_manager.loading.refreshing_timeline'}",
|
||||
"settings": "@:{'templates.generated.compat.configuration.release_manager.loading.settings'}",
|
||||
"summary": "@:{'templates.generated.compat.configuration.release_manager.loading.summary'}",
|
||||
"summary_detail": "@:{'templates.generated.compat.configuration.release_manager.loading.summary_detail'}",
|
||||
"timeline_events": "@:{'templates.generated.compat.configuration.release_manager.loading.timeline_events'}",
|
||||
"timeline_sessions": "@:{'templates.generated.compat.configuration.release_manager.loading.timeline_sessions'}"
|
||||
"integrations": {
|
||||
"title": "@:{'templates.generated.compat.configuration.release_manager.integrations.title'}",
|
||||
"subtitle": "@:{'templates.generated.compat.configuration.release_manager.integrations.subtitle'}",
|
||||
"choose_repository": "@:{'templates.generated.compat.configuration.release_manager.integrations.choose_repository'}",
|
||||
"add_health_url": "@:{'templates.generated.compat.configuration.release_manager.integrations.add_health_url'}",
|
||||
"github_token_not_configured": "@:{'templates.generated.compat.configuration.release_manager.integrations.github_token_not_configured'}",
|
||||
"open_github_token_settings": "@:{'templates.generated.compat.configuration.release_manager.integrations.open_github_token_settings'}",
|
||||
"channel": "@:{'templates.generated.compat.configuration.release_manager.integrations.channel'}",
|
||||
"app": "@:{'templates.generated.compat.configuration.release_manager.integrations.app'}",
|
||||
"repository": "@:{'templates.generated.compat.configuration.release_manager.integrations.repository'}",
|
||||
"repository_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.repository_message'}",
|
||||
"repository_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.repository_placeholder'}",
|
||||
"branch": "@:{'templates.generated.compat.configuration.release_manager.integrations.branch'}",
|
||||
"branch_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.branch_message'}",
|
||||
"branch_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.branch_placeholder'}",
|
||||
"latest_prefix": "@:{'templates.generated.compat.configuration.release_manager.integrations.latest_prefix'}",
|
||||
"coolify_instance": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_instance'}",
|
||||
"coolify_instance_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_instance_message'}",
|
||||
"no_coolify_instance": "@:{'templates.generated.compat.configuration.release_manager.integrations.no_coolify_instance'}",
|
||||
"coolify_instance_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_instance_placeholder'}",
|
||||
"coolify_service_uuid": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_service_uuid'}",
|
||||
"coolify_service_uuid_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_service_uuid_message'}",
|
||||
"coolify_service_uuid_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_service_uuid_placeholder'}",
|
||||
"health_url": "@:{'templates.generated.compat.configuration.release_manager.integrations.health_url'}",
|
||||
"health_url_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.health_url_message'}",
|
||||
"health_url_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.health_url_placeholder'}",
|
||||
"ssl_domain": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain'}",
|
||||
"ssl_domain_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain_message'}",
|
||||
"ssl_domain_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain_placeholder'}",
|
||||
"https_domain_for_coolify": "@:{'templates.generated.compat.configuration.release_manager.integrations.https_domain_for_coolify'}",
|
||||
"endpoint_mode": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode'}",
|
||||
"endpoint_mode_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_message'}",
|
||||
"endpoint_mode_auto": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_auto'}",
|
||||
"endpoint_mode_manual": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_manual'}",
|
||||
"manual_endpoint_host": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host'}",
|
||||
"manual_endpoint_host_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host_message'}",
|
||||
"manual_endpoint_host_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host_placeholder'}",
|
||||
"manual_endpoint_port": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_port'}",
|
||||
"manual_endpoint_port_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_port_message'}",
|
||||
"app_port": "@:{'templates.generated.compat.configuration.release_manager.integrations.app_port'}",
|
||||
"app_port_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.app_port_message'}",
|
||||
"auto_gateway_endpoint": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_gateway_endpoint'}",
|
||||
"pending_automatic_endpoint": "@:{'templates.generated.compat.configuration.release_manager.integrations.pending_automatic_endpoint'}",
|
||||
"auto_deploy": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_deploy'}",
|
||||
"auto_deploy_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_deploy_tooltip'}",
|
||||
"coolify_ssl": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_ssl'}",
|
||||
"coolify_ssl_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_ssl_tooltip'}",
|
||||
"deploy_now": "@:{'templates.generated.compat.configuration.release_manager.integrations.deploy_now'}",
|
||||
"deploy_now_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.deploy_now_tooltip'}",
|
||||
"create_missing_service": "@:{'templates.generated.compat.configuration.release_manager.integrations.create_missing_service'}",
|
||||
"create_missing_service_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.create_missing_service_tooltip'}",
|
||||
"test_access_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.test_access_tooltip'}"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2425,37 +2389,6 @@
|
||||
"subtitle": "@:{'templates.generated.compat.employees.subtitle'}",
|
||||
"title": "@:superuser.nav.employees"
|
||||
},
|
||||
"error_report": {
|
||||
"button": "@:{'templates.generated.compat.error_report.button'}",
|
||||
"title": "@:{'templates.generated.compat.error_report.title'}",
|
||||
"subtitle": "@:{'templates.generated.compat.error_report.subtitle'}",
|
||||
"before_error": "@:{'templates.generated.compat.error_report.before_error'}",
|
||||
"expected": "@:{'templates.generated.compat.error_report.expected'}",
|
||||
"actual": "@:{'templates.generated.compat.error_report.actual'}",
|
||||
"before_error_placeholder": "@:{'templates.generated.compat.error_report.before_error_placeholder'}",
|
||||
"expected_placeholder": "@:{'templates.generated.compat.error_report.expected_placeholder'}",
|
||||
"actual_placeholder": "@:{'templates.generated.compat.error_report.actual_placeholder'}",
|
||||
"consent": "@:{'templates.generated.compat.error_report.consent'}",
|
||||
"submit": "@:{'templates.generated.compat.error_report.submit'}",
|
||||
"submitted": "@:{'templates.generated.compat.error_report.submitted'}",
|
||||
"capture_failed": "@:{'templates.generated.compat.error_report.capture_failed'}",
|
||||
"submit_failed": "@:{'templates.generated.compat.error_report.submit_failed'}",
|
||||
"required": "@:{'templates.generated.compat.error_report.required'}",
|
||||
"page_subtitle": "@:{'templates.generated.compat.error_report.page_subtitle'}",
|
||||
"screenshot": "@:{'templates.generated.compat.error_report.screenshot'}",
|
||||
"request_errors": "@:{'templates.generated.compat.error_report.request_errors'}",
|
||||
"vue_errors": "@:{'templates.generated.compat.error_report.vue_errors'}",
|
||||
"runtime_context": "@:{'templates.generated.compat.error_report.runtime_context'}",
|
||||
"empty": "@:{'templates.generated.compat.error_report.empty'}",
|
||||
"resolve": "@:{'templates.generated.compat.error_report.resolve'}",
|
||||
"reopen": "@:{'templates.generated.compat.error_report.reopen'}",
|
||||
"resolution_note": "@:{'templates.generated.compat.error_report.resolution_note'}",
|
||||
"filters": {
|
||||
"open": "@:{'templates.generated.compat.error_report.filters.open'}",
|
||||
"resolved": "@:{'templates.generated.compat.error_report.filters.resolved'}",
|
||||
"all": "@:{'templates.generated.compat.error_report.filters.all'}"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"forbidden": "@:{'templates.generated.compat.errors.forbidden'}",
|
||||
"general": "@:{'templates.generated.compat.errors.general'}",
|
||||
@@ -3552,7 +3485,7 @@
|
||||
"columns": {
|
||||
"category": "@:tables.common.category",
|
||||
"economic_product": "@:{'templates.generated.compat.objects.products.columns.economic_product'}",
|
||||
"is_wash": "@:{'templates.generated.compat.objects.products.columns.is_wash'}",
|
||||
"is_wash": "@:{'templates.generated.compat.guest.pwa.installed_label'}",
|
||||
"piktogram": "@:{'templates.generated.compat.objects.products.columns.piktogram'}",
|
||||
"requires_note": "@:{'templates.generated.compat.objects.products.columns.requires_note'}",
|
||||
"show_in_booking_form": "@:{'templates.generated.compat.objects.products.columns.show_in_booking_form'}",
|
||||
@@ -3962,6 +3895,7 @@
|
||||
"mandskabsvogn": "@:{'templates.generated.compat.pos.vehicle_types.mandskabsvogn'}",
|
||||
"motorcykel": "@:{'templates.generated.compat.pos.vehicle_types.motorcykel'}",
|
||||
"personbil": "@:vehicles.vehicle_types.car",
|
||||
"s??ttevogn": "@:{'templates.generated.compat.pos.vehicle_types.s??ttevogn'}",
|
||||
"sættevogn": "@:{'templates.generated.compat.pos.vehicle_types.sættevogn'}",
|
||||
"trailer": "@:{'templates.generated.compat.pos.vehicle_types.trailer'}",
|
||||
"traktor": "@:{'templates.generated.compat.pos.vehicle_types.traktor'}",
|
||||
@@ -4222,9 +4156,7 @@
|
||||
"loading_department": "@:{'templates.generated.compat.self_wash.loading_department'}",
|
||||
"loading_types": "@:{'templates.generated.compat.self_wash.loading_types'}",
|
||||
"loading_vehicle_types": "@:{'templates.generated.compat.self_wash.loading_vehicle_types'}",
|
||||
"lane_unavailable": "@:{'templates.generated.compat.self_wash.lane_unavailable'}",
|
||||
"machine": "@:{'templates.generated.compat.self_wash.machine'}",
|
||||
"machine_unavailable_for_lane": "@:{'templates.generated.compat.self_wash.machine_unavailable_for_lane'}",
|
||||
"no_departments_found": "@:{'templates.generated.compat.self_wash.no_departments_found'}",
|
||||
"no_vehicles_found": "@:vehicles.no_vehicles",
|
||||
"occupied": "@:{'templates.generated.compat.self_wash.occupied'}",
|
||||
@@ -4241,10 +4173,6 @@
|
||||
"start_wash": "@:{'templates.generated.compat.self_wash.start_wash'}",
|
||||
"total_time": "@:{'templates.generated.compat.self_wash.total_time'}",
|
||||
"unavailable": "@:{'templates.generated.compat.self_wash.unavailable'}",
|
||||
"vehicle_step_missing_customer_number": "@:{'templates.generated.compat.self_wash.vehicle_step_missing_customer_number'}",
|
||||
"vehicle_step_missing_department": "@:{'templates.generated.compat.self_wash.vehicle_step_missing_department'}",
|
||||
"vehicle_step_missing_registration": "@:{'templates.generated.compat.self_wash.vehicle_step_missing_registration'}",
|
||||
"vehicle_step_missing_vehicle_type": "@:{'templates.generated.compat.self_wash.vehicle_step_missing_vehicle_type'}",
|
||||
"vehicle_type": "@:{'templates.generated.compat.products.form.vehicle_type'}",
|
||||
"vehicle": "@:common.vehicle",
|
||||
"wash_completed_contact": "@:{'templates.generated.compat.self_wash.wash_completed_contact'}",
|
||||
|
||||
+80
-125
@@ -1710,25 +1710,7 @@
|
||||
"view_lane_new_tab": "@.capitalize:{'words.generated.se'} kjørefelt @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
|
||||
"view_lane_setup_new_tab": "@.capitalize:{'words.generated.se'} kjørefeltoppsett @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
|
||||
"view_order_new_tab": "@.capitalize:{'words.generated.se'} @:{'words.generated.ordre'} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
|
||||
"view_vehicle_new_tab": "@.capitalize:{'words.generated.se'} @:{'words.generated.kjøretøy'} {reg} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
|
||||
"accept_self_serve_wash": "Godta selvbetjent vask",
|
||||
"accept_self_serve_wash_confirm": "Flytt denne kladden til kunde #{customerNumber} med selvbetjeningsdetaljene?",
|
||||
"accept_self_serve_wash_success": "Den selvbetjente vasken ble godtatt.",
|
||||
"email_notifications_section": "E-postvarsler",
|
||||
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
|
||||
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
|
||||
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
|
||||
"resend_booking_confirmation": "(Re)send booking confirmation",
|
||||
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
|
||||
"resend_booking_confirmation_success": "The booking confirmation has been sent",
|
||||
"resend_wash_certificate": "(Re)send wash certificate",
|
||||
"resend_wash_certificate_error": "The wash certificate could not be sent",
|
||||
"resend_wash_certificate_success": "The wash certificate has been sent",
|
||||
"self_serve_customer": "Kunde",
|
||||
"self_serve_driver": "Sjåfør",
|
||||
"self_serve_elapsed": "Tid",
|
||||
"self_serve_wash_attachment": "Selvbetjent vask",
|
||||
"self_serve_wash_attachment_for_customer": "Selvbetjent vask for kunde #{customerNumber}"
|
||||
"view_vehicle_new_tab": "@.capitalize:{'words.generated.se'} @:{'words.generated.kjøretøy'} {reg} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}"
|
||||
},
|
||||
"show_details": "@:{'words.generated.vis'} @:{'words.generated.detaljer'}",
|
||||
"show_order_details": "@:{'words.generated.vis'} @:{'words.generated.bestillingsdetaljer'}",
|
||||
@@ -2568,59 +2550,6 @@
|
||||
"username_desc": "@:{'words.generated.brukernavnet'} @:{'words.generated.brukes'} @:{'words.generated.til'} @:{'words.generated.a'} @:{'words.generated.autentisere'} @:{'words.generated.api'}-@:{'words.generated.tilkoblingen'}."
|
||||
},
|
||||
"release_manager": {
|
||||
"integrations": {
|
||||
"title": "GitHub og Coolify",
|
||||
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
|
||||
"choose_repository": "Velg repository",
|
||||
"add_health_url": "legg til health-URL",
|
||||
"github_token_not_configured": "GitHub-token er ikke konfigurert; private repositories kan ikke vises.",
|
||||
"open_github_token_settings": "Åpne GitHub token-innstillinger",
|
||||
"channel": "Kanal",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Eksempel: truckwash/backend-php",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: release/canary",
|
||||
"branch_placeholder": "branch",
|
||||
"latest_prefix": "Seneste: {commit}",
|
||||
"coolify_instance": "Coolify-instans",
|
||||
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
|
||||
"no_coolify_instance": "Ingen Coolify-instans",
|
||||
"coolify_instance_placeholder": "Coolify-instans-id",
|
||||
"coolify_service_uuid": "Coolify service-UUID",
|
||||
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
|
||||
"coolify_service_uuid_placeholder": "Coolify service-UUID",
|
||||
"health_url": "Health-URL",
|
||||
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
|
||||
"health_url_placeholder": "Health-URL",
|
||||
"ssl_domain": "Load balancer-domene",
|
||||
"ssl_domain_message": "Må være et DNS-domene som routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
|
||||
"ssl_domain_placeholder": "Load balancer-domene",
|
||||
"https_domain_for_coolify": "Domene kontrollert av load balanceren",
|
||||
"endpoint_mode": "Endpoint mode",
|
||||
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
|
||||
"endpoint_mode_auto": "Auto",
|
||||
"endpoint_mode_manual": "Manual",
|
||||
"manual_endpoint_host": "Manual host",
|
||||
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
|
||||
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
|
||||
"manual_endpoint_port": "Manual public port",
|
||||
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
|
||||
"app_port": "App port",
|
||||
"app_port_message": "Internal container port exposed to Coolify routing.",
|
||||
"auto_gateway_endpoint": "Automatic gateway endpoint",
|
||||
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
|
||||
"auto_deploy": "Auto deploy",
|
||||
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
|
||||
"deploy_now": "Deploy nå",
|
||||
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
|
||||
"create_missing_service": "Opprett manglende service",
|
||||
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
|
||||
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
|
||||
},
|
||||
"title": "Release Manager",
|
||||
"subtitle": "Gradvise API- og Front-End-utgivelser",
|
||||
"channel_names": {
|
||||
@@ -2815,9 +2744,7 @@
|
||||
"release_runtime": "Release-runtime",
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"use_local_frontend": "Bruk lokal frontend",
|
||||
"switch_error": "Release channel could not be switched. The previous channel is still active."
|
||||
"api": "API"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -2841,8 +2768,7 @@
|
||||
"save_target": "Lagre mål",
|
||||
"delete": "Slett",
|
||||
"back": "Tilbake",
|
||||
"next": "Neste",
|
||||
"publish_release": "Publish release"
|
||||
"next": "Neste"
|
||||
},
|
||||
"tables": {
|
||||
"channel": "Kanal",
|
||||
@@ -3052,20 +2978,58 @@
|
||||
"any_severity": "Alle alvorligheder",
|
||||
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
|
||||
},
|
||||
"status": {
|
||||
"confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.",
|
||||
"choose_bundle_prompt": "Enter the bundle id to set for this channel.",
|
||||
"impact": "Impact",
|
||||
"cause": "Cause",
|
||||
"automated_fix": "Automated fix",
|
||||
"manual_fallback": "Manual fallback",
|
||||
"related_deployment": "Related deployment",
|
||||
"recent_result": "Recent result",
|
||||
"no_automated_action": "No automated action available.",
|
||||
"deployment": "Deployment",
|
||||
"target": "Target",
|
||||
"coolify_target": "Coolify target",
|
||||
"service_set": "Service set"
|
||||
"integrations": {
|
||||
"title": "GitHub og Coolify",
|
||||
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
|
||||
"choose_repository": "Velg repository",
|
||||
"add_health_url": "legg til health-URL",
|
||||
"github_token_not_configured": "GitHub-token er ikke konfigurert; private repositories kan ikke vises.",
|
||||
"open_github_token_settings": "Åpne GitHub token-innstillinger",
|
||||
"channel": "Kanal",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Eksempel: truckwash/backend-php",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: release/canary",
|
||||
"branch_placeholder": "branch",
|
||||
"latest_prefix": "Seneste: {commit}",
|
||||
"coolify_instance": "Coolify-instans",
|
||||
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
|
||||
"no_coolify_instance": "Ingen Coolify-instans",
|
||||
"coolify_instance_placeholder": "Coolify-instans-id",
|
||||
"coolify_service_uuid": "Coolify service-UUID",
|
||||
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
|
||||
"coolify_service_uuid_placeholder": "Coolify service-UUID",
|
||||
"health_url": "Health-URL",
|
||||
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
|
||||
"health_url_placeholder": "Health-URL",
|
||||
"ssl_domain": "Load balancer-domene",
|
||||
"ssl_domain_message": "Må være et DNS-domene som routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
|
||||
"ssl_domain_placeholder": "Load balancer-domene",
|
||||
"https_domain_for_coolify": "Domene kontrollert av load balanceren",
|
||||
"endpoint_mode": "Endpoint mode",
|
||||
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
|
||||
"endpoint_mode_auto": "Auto",
|
||||
"endpoint_mode_manual": "Manual",
|
||||
"manual_endpoint_host": "Manual host",
|
||||
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
|
||||
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
|
||||
"manual_endpoint_port": "Manual public port",
|
||||
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
|
||||
"app_port": "App port",
|
||||
"app_port_message": "Internal container port exposed to Coolify routing.",
|
||||
"auto_gateway_endpoint": "Automatic gateway endpoint",
|
||||
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
|
||||
"auto_deploy": "Auto deploy",
|
||||
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
|
||||
"deploy_now": "Deploy nå",
|
||||
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
|
||||
"create_missing_service": "Opprett manglende service",
|
||||
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
|
||||
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3323,34 +3287,31 @@
|
||||
"subtitle": "@.capitalize:{'words.generated.administrer'} @:{'words.generated.ansatte'}"
|
||||
},
|
||||
"error_report": {
|
||||
"button": "Rapporter feil",
|
||||
"title": "Rapporter feil",
|
||||
"subtitle": "Send gjeldende skjermbilde og de siste feildetaljene til support.",
|
||||
"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.",
|
||||
"submit": "Send rapport",
|
||||
"submitted": "Feilrapport sendt.",
|
||||
"capture_failed": "Skjermbildet kunne ikke tas. Prøv igjen.",
|
||||
"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",
|
||||
"screenshot": "Skjermbilde",
|
||||
"request_errors": "Request-feil",
|
||||
"vue_errors": "Vue-feil",
|
||||
"runtime_context": "Runtime-kontekst",
|
||||
"empty": "Ingen feilrapporter funnet.",
|
||||
"resolve": "Marker som løst",
|
||||
"reopen": "Åpne igjen",
|
||||
"resolution_note": "Løsningsnotat",
|
||||
"button": "Report error",
|
||||
"title": "Report error",
|
||||
"subtitle": "Send the current screen and recent error details to support.",
|
||||
"before_error": "What were you doing before the error occurred?",
|
||||
"expected": "What did you expect would happen?",
|
||||
"actual": "What actually happened?",
|
||||
"consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
|
||||
"submit": "Submit report",
|
||||
"submitted": "Error report submitted.",
|
||||
"capture_failed": "The screen capture failed. Please try again.",
|
||||
"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",
|
||||
"screenshot": "Screenshot",
|
||||
"request_errors": "Request errors",
|
||||
"vue_errors": "Vue errors",
|
||||
"runtime_context": "Runtime context",
|
||||
"empty": "No error reports found.",
|
||||
"resolve": "Mark resolved",
|
||||
"reopen": "Reopen",
|
||||
"resolution_note": "Resolution note",
|
||||
"filters": {
|
||||
"open": "Åpen",
|
||||
"resolved": "Løst",
|
||||
"all": "Alle"
|
||||
"open": "Open",
|
||||
"resolved": "Resolved",
|
||||
"all": "All"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
@@ -4215,7 +4176,6 @@
|
||||
"products": {
|
||||
"columns": {
|
||||
"economic_product": "@.upper:{'words.generated.e'}-@:{'words.generated.conomic'} @:{'words.generated.produkt'}",
|
||||
"is_wash": "Er vask",
|
||||
"piktogram": "@:{'templates.generated.compat.objects.columns.icon'}",
|
||||
"requires_note": "@.capitalize:{'words.generated.krever'} @:{'words.generated.merknad'}",
|
||||
"show_in_booking_form": "@:{'words.generated.vis'} @:{'words.generated.i'} bestillingsskjema",
|
||||
@@ -4506,6 +4466,7 @@
|
||||
"lift": "Løfte",
|
||||
"mandskabsvogn": "@:{'templates.generated.compat.vehicles.vehicle_types.crew'}",
|
||||
"motorcykel": "Motorsykkel",
|
||||
"s??ttevogn": "Semi-trailer",
|
||||
"sættevogn": "@.capitalize:{'words.generated.semitrailer'}",
|
||||
"trailer": "@.capitalize:{'words.generated.tilhenger'}",
|
||||
"traktor": "Trekkbil",
|
||||
@@ -4704,9 +4665,7 @@
|
||||
"loading_department": "@.capitalize:{'words.generated.laster'} @:{'words.generated.avdelingen'}...",
|
||||
"loading_types": "@.capitalize:{'words.generated.laster'} @:{'words.generated.inn'} @:{'words.generated.typer'}...",
|
||||
"loading_vehicle_types": "@.capitalize:{'words.generated.laster'} @:{'words.generated.inn'} @:{'words.generated.tilgjengelige'} @:{'words.generated.kjøretøytyper'}...",
|
||||
"lane_unavailable": "Vaskebanen er ikke tilgjengelig",
|
||||
"machine": "@.capitalize:{'words.generated.maskin'}",
|
||||
"machine_unavailable_for_lane": "Maskinvask er ikke tilgjengelig for valgt bane. Velg manuell vask eller en annen ledig bane.",
|
||||
"no_departments_found": "@:{'templates.generated.compat.departments.no_departments'}",
|
||||
"occupied": "Okkupert",
|
||||
"open_property_access_gate": "@:{'words.generated.aapne'} @:{'words.generated.eiendommens'} inngangsport",
|
||||
@@ -4722,10 +4681,6 @@
|
||||
"start_wash": "@:{'templates.generated.compat.guest.start_wash'}",
|
||||
"total_time": "@:{'words.generated.total'} @:{'words.generated.tid'}: {minutes} minutter @:{'words.generated.og'} {seconds} @:{'words.generated.sekunder'}",
|
||||
"unavailable": "@.capitalize:{'words.generated.utilgjengelig'}",
|
||||
"vehicle_step_missing_customer_number": "Skriv inn kundenummer for a fortsette.",
|
||||
"vehicle_step_missing_department": "Vent til naermeste avdeling er lastet inn for du fortsetter.",
|
||||
"vehicle_step_missing_registration": "Skriv inn registreringsnummer for a fortsette.",
|
||||
"vehicle_step_missing_vehicle_type": "Velg en tilgjengelig kjoretoytype for a fortsette.",
|
||||
"wash_completed_contact": "@.capitalize:{'words.generated.vi'} håper @:{'words.generated.du'} @:{'words.generated.er'} fornøyd @:{'words.generated.med'} resultatet. @.capitalize:{'words.generated.hvis'} @:{'words.generated.du'} @:{'words.generated.trenger'} @:{'words.generated.ytterligere'} @:{'words.generated.hjelp'}, @:{'words.generated.vennligst'} @:{'words.generated.kontakt'} @:{'words.generated.vare'} @:{'words.generated.ansatte'}.",
|
||||
"wash_completed_message": "@.capitalize:{'words.generated.vasken'} @:{'words.generated.din'} @:{'words.generated.er'} @:{'words.generated.na'} @:{'words.generated.fullført'}! @:{'words.generated.takk'} @:{'words.generated.for'} @:{'words.generated.at'} @:{'words.generated.du'} @:{'words.generated.bruker'} @:{'words.generated.var_2'} @:{'words.generated.selvbetjente'} @:{'words.generated.vask'}.",
|
||||
"wash_completed": "@:{'words.generated.vasking'} @:{'words.generated.fullført'}",
|
||||
|
||||
+80
-125
@@ -1760,25 +1760,7 @@
|
||||
"view_lane_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.lane'} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
|
||||
"view_lane_setup_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.lane'} @:{'words.generated.setup'} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
|
||||
"view_order_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.order'} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
|
||||
"view_vehicle_new_tab": "@.capitalize:{'words.generated.visa'} @:{'words.generated.fordon'} {reg} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.flik'}",
|
||||
"accept_self_serve_wash": "Godkänn självbetjäningstvätt",
|
||||
"accept_self_serve_wash_confirm": "Flytta detta utkast till kund #{customerNumber} med självbetjäningsdetaljerna?",
|
||||
"accept_self_serve_wash_success": "Självbetjäningstvätten godkändes.",
|
||||
"email_notifications_section": "E-postaviseringar",
|
||||
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
|
||||
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
|
||||
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
|
||||
"resend_booking_confirmation": "(Re)send booking confirmation",
|
||||
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
|
||||
"resend_booking_confirmation_success": "The booking confirmation has been sent",
|
||||
"resend_wash_certificate": "(Re)send wash certificate",
|
||||
"resend_wash_certificate_error": "The wash certificate could not be sent",
|
||||
"resend_wash_certificate_success": "The wash certificate has been sent",
|
||||
"self_serve_customer": "Kund",
|
||||
"self_serve_driver": "Förare",
|
||||
"self_serve_elapsed": "Tid",
|
||||
"self_serve_wash_attachment": "Självbetjäningstvätt",
|
||||
"self_serve_wash_attachment_for_customer": "Självbetjäningstvätt för kund #{customerNumber}"
|
||||
"view_vehicle_new_tab": "@.capitalize:{'words.generated.visa'} @:{'words.generated.fordon'} {reg} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.flik'}"
|
||||
},
|
||||
"show_details": "@:{'words.generated.show'} @:{'words.generated.details'}",
|
||||
"show_order_details": "@:{'words.generated.show'} @:{'words.generated.order'} @:{'words.generated.details'}",
|
||||
@@ -2618,59 +2600,6 @@
|
||||
"username_desc": "Användarnamnet @:{'words.generated.anvands'} @:{'words.generated.for'} @:{'words.generated.att'} @:{'words.generated.autentisera'} @:{'words.generated.api'}-@:{'words.generated.anslutningen'}."
|
||||
},
|
||||
"release_manager": {
|
||||
"integrations": {
|
||||
"title": "GitHub och Coolify",
|
||||
"subtitle": "Koppla GitHub-repositories och branches till kanalspecifika Coolify-tjänster.",
|
||||
"choose_repository": "Välj repository",
|
||||
"add_health_url": "lägg till health-URL",
|
||||
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
|
||||
"open_github_token_settings": "Öppna GitHub token-inställningar",
|
||||
"channel": "Kanal",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Eksempel: truckwash/backend-php",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: release/canary",
|
||||
"branch_placeholder": "branch",
|
||||
"latest_prefix": "Seneste: {commit}",
|
||||
"coolify_instance": "Coolify-instans",
|
||||
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
|
||||
"no_coolify_instance": "Ingen Coolify-instans",
|
||||
"coolify_instance_placeholder": "Coolify-instans-id",
|
||||
"coolify_service_uuid": "Coolify service-UUID",
|
||||
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
|
||||
"coolify_service_uuid_placeholder": "Coolify service-UUID",
|
||||
"health_url": "Health-URL",
|
||||
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
|
||||
"health_url_placeholder": "Health-URL",
|
||||
"ssl_domain": "Load balancer-domän",
|
||||
"ssl_domain_message": "Måste vara en DNS-domän som routas till Coolify load balancern. Exempel: api-v2.truckwash.io",
|
||||
"ssl_domain_placeholder": "Load balancer-domän",
|
||||
"https_domain_for_coolify": "Domän som kontrolleras av load balancern",
|
||||
"endpoint_mode": "Endpoint mode",
|
||||
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
|
||||
"endpoint_mode_auto": "Auto",
|
||||
"endpoint_mode_manual": "Manual",
|
||||
"manual_endpoint_host": "Manual host",
|
||||
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
|
||||
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
|
||||
"manual_endpoint_port": "Manual public port",
|
||||
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
|
||||
"app_port": "App port",
|
||||
"app_port_message": "Internal container port exposed to Coolify routing.",
|
||||
"auto_gateway_endpoint": "Automatic gateway endpoint",
|
||||
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
|
||||
"auto_deploy": "Auto deploy",
|
||||
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
|
||||
"deploy_now": "Deploy nu",
|
||||
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
|
||||
"create_missing_service": "Skapa saknad tjänst",
|
||||
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
|
||||
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
|
||||
},
|
||||
"title": "Release Manager",
|
||||
"subtitle": "Gradvisa API- och Front-End-versioner",
|
||||
"channel_names": {
|
||||
@@ -2865,9 +2794,7 @@
|
||||
"release_runtime": "Release-runtime",
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"use_local_frontend": "Använd lokal frontend",
|
||||
"switch_error": "Release channel could not be switched. The previous channel is still active."
|
||||
"api": "API"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -2891,8 +2818,7 @@
|
||||
"save_target": "Spara mål",
|
||||
"delete": "Radera",
|
||||
"back": "Tillbaka",
|
||||
"next": "Nästa",
|
||||
"publish_release": "Publish release"
|
||||
"next": "Nästa"
|
||||
},
|
||||
"tables": {
|
||||
"channel": "Kanal",
|
||||
@@ -3102,20 +3028,58 @@
|
||||
"any_severity": "Alla allvarlighetsgrader",
|
||||
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
|
||||
},
|
||||
"status": {
|
||||
"confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.",
|
||||
"choose_bundle_prompt": "Enter the bundle id to set for this channel.",
|
||||
"impact": "Impact",
|
||||
"cause": "Cause",
|
||||
"automated_fix": "Automated fix",
|
||||
"manual_fallback": "Manual fallback",
|
||||
"related_deployment": "Related deployment",
|
||||
"recent_result": "Recent result",
|
||||
"no_automated_action": "No automated action available.",
|
||||
"deployment": "Deployment",
|
||||
"target": "Target",
|
||||
"coolify_target": "Coolify target",
|
||||
"service_set": "Service set"
|
||||
"integrations": {
|
||||
"title": "GitHub och Coolify",
|
||||
"subtitle": "Koppla GitHub-repositories och branches till kanalspecifika Coolify-tjänster.",
|
||||
"choose_repository": "Välj repository",
|
||||
"add_health_url": "lägg till health-URL",
|
||||
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
|
||||
"open_github_token_settings": "Öppna GitHub token-inställningar",
|
||||
"channel": "Kanal",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Eksempel: truckwash/backend-php",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: release/canary",
|
||||
"branch_placeholder": "branch",
|
||||
"latest_prefix": "Seneste: {commit}",
|
||||
"coolify_instance": "Coolify-instans",
|
||||
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
|
||||
"no_coolify_instance": "Ingen Coolify-instans",
|
||||
"coolify_instance_placeholder": "Coolify-instans-id",
|
||||
"coolify_service_uuid": "Coolify service-UUID",
|
||||
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
|
||||
"coolify_service_uuid_placeholder": "Coolify service-UUID",
|
||||
"health_url": "Health-URL",
|
||||
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
|
||||
"health_url_placeholder": "Health-URL",
|
||||
"ssl_domain": "Load balancer-domän",
|
||||
"ssl_domain_message": "Måste vara en DNS-domän som routas till Coolify load balancern. Exempel: api-v2.truckwash.io",
|
||||
"ssl_domain_placeholder": "Load balancer-domän",
|
||||
"https_domain_for_coolify": "Domän som kontrolleras av load balancern",
|
||||
"endpoint_mode": "Endpoint mode",
|
||||
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
|
||||
"endpoint_mode_auto": "Auto",
|
||||
"endpoint_mode_manual": "Manual",
|
||||
"manual_endpoint_host": "Manual host",
|
||||
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
|
||||
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
|
||||
"manual_endpoint_port": "Manual public port",
|
||||
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
|
||||
"app_port": "App port",
|
||||
"app_port_message": "Internal container port exposed to Coolify routing.",
|
||||
"auto_gateway_endpoint": "Automatic gateway endpoint",
|
||||
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
|
||||
"auto_deploy": "Auto deploy",
|
||||
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
|
||||
"deploy_now": "Deploy nu",
|
||||
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
|
||||
"create_missing_service": "Skapa saknad tjänst",
|
||||
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
|
||||
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3373,34 +3337,31 @@
|
||||
"subtitle": "@:{'words.generated.administrer'} @:{'words.generated.ansatte'}"
|
||||
},
|
||||
"error_report": {
|
||||
"button": "Rapportera fel",
|
||||
"title": "Rapportera fel",
|
||||
"subtitle": "Skicka aktuell skärm och de senaste feldetaljerna till support.",
|
||||
"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.",
|
||||
"submit": "Skicka rapport",
|
||||
"submitted": "Felrapport skickad.",
|
||||
"capture_failed": "Skärmbilden kunde inte tas. Försök igen.",
|
||||
"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",
|
||||
"screenshot": "Skärmbild",
|
||||
"request_errors": "Request-fel",
|
||||
"vue_errors": "Vue-fel",
|
||||
"runtime_context": "Runtime-kontext",
|
||||
"empty": "Inga felrapporter hittades.",
|
||||
"resolve": "Markera som löst",
|
||||
"reopen": "Öppna igen",
|
||||
"resolution_note": "Lösningsnotering",
|
||||
"button": "Report error",
|
||||
"title": "Report error",
|
||||
"subtitle": "Send the current screen and recent error details to support.",
|
||||
"before_error": "What were you doing before the error occurred?",
|
||||
"expected": "What did you expect would happen?",
|
||||
"actual": "What actually happened?",
|
||||
"consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
|
||||
"submit": "Submit report",
|
||||
"submitted": "Error report submitted.",
|
||||
"capture_failed": "The screen capture failed. Please try again.",
|
||||
"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",
|
||||
"screenshot": "Screenshot",
|
||||
"request_errors": "Request errors",
|
||||
"vue_errors": "Vue errors",
|
||||
"runtime_context": "Runtime context",
|
||||
"empty": "No error reports found.",
|
||||
"resolve": "Mark resolved",
|
||||
"reopen": "Reopen",
|
||||
"resolution_note": "Resolution note",
|
||||
"filters": {
|
||||
"open": "Öppen",
|
||||
"resolved": "Löst",
|
||||
"all": "Alla"
|
||||
"open": "Open",
|
||||
"resolved": "Resolved",
|
||||
"all": "All"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
@@ -4265,7 +4226,6 @@
|
||||
"products": {
|
||||
"columns": {
|
||||
"economic_product": "@:{'templates.generated.compat.objects.economic_products.single'}",
|
||||
"is_wash": "Är tvätt",
|
||||
"piktogram": "Pictogram",
|
||||
"requires_note": "@.capitalize:{'words.generated.kraver'} @:{'words.generated.anteckning'}",
|
||||
"show_in_booking_form": "@.capitalize:{'words.generated.visa'} @:{'words.generated.i'} bokningsformulär",
|
||||
@@ -4556,6 +4516,7 @@
|
||||
"lift": "Lift",
|
||||
"mandskabsvogn": "@:{'templates.generated.compat.vehicles.vehicle_types.crew'}",
|
||||
"motorcykel": "Motorsykkel",
|
||||
"s??ttevogn": "@.capitalize:{'words.generated.semi'}-@:{'words.generated.trailer'}",
|
||||
"sættevogn": "Semitrailer",
|
||||
"trailer": "Släp",
|
||||
"traktor": "@:{'templates.generated.compat.vehicles.types.semi'}",
|
||||
@@ -4754,9 +4715,7 @@
|
||||
"loading_department": "@.capitalize:{'words.generated.laser'} @:{'words.generated.in'} @:{'words.generated.avdelning'}...",
|
||||
"loading_types": "@:{'words.generated.laddar'} @:{'words.generated.typer'}...",
|
||||
"loading_vehicle_types": "@.capitalize:{'words.generated.laser'} @:{'words.generated.in'} @:{'words.generated.tillgangliga'} @:{'words.generated.fordonstyper'}...",
|
||||
"lane_unavailable": "Tvattbanan ar inte tillganglig",
|
||||
"machine": "@.capitalize:{'words.generated.machine'}",
|
||||
"machine_unavailable_for_lane": "Maskintvatt ar inte tillganglig for vald bana. Valj manuell tvatt eller en annan ledig bana.",
|
||||
"no_departments_found": "@.capitalize:{'words.generated.inga'} @:{'words.generated.avdelningar'} @:{'words.generated.hittades'}",
|
||||
"occupied": "Upptagen",
|
||||
"open_property_access_gate": "@:{'words.generated.oeppna'} @:{'words.generated.fastighetens'} ingangsgrind",
|
||||
@@ -4772,10 +4731,6 @@
|
||||
"start_wash": "@.capitalize:{'words.generated.start'} @:{'words.generated.wash'}",
|
||||
"total_time": "@.capitalize:{'words.generated.total'} @:{'words.generated.tid'}: {minutes} minuter @:{'words.generated.och'} {seconds} @:{'words.generated.sekunder'}",
|
||||
"unavailable": "@:{'templates.generated.compat.common.not_available'}",
|
||||
"vehicle_step_missing_customer_number": "Ange kundnummer for att fortsatta.",
|
||||
"vehicle_step_missing_department": "Vanta tills narmaste avdelning har laddats innan du fortsatter.",
|
||||
"vehicle_step_missing_registration": "Ange registreringsnummer for att fortsatta.",
|
||||
"vehicle_step_missing_vehicle_type": "Valj en tillganglig fordonstyp for att fortsatta.",
|
||||
"wash_completed_contact": "@.capitalize:{'words.generated.vi'} hoppas @:{'words.generated.att'} @:{'words.generated.du'} @:{'words.generated.ar'} nöjd @:{'words.generated.med'} resultatet. @.capitalize:{'words.generated.om'} @:{'words.generated.du'} @:{'words.generated.behover'} @:{'words.generated.ytterligare'} @:{'words.generated.hjalp'}, @:{'words.generated.kontakta'} @:{'words.generated.var_2'} personal.",
|
||||
"wash_completed_message": "@.capitalize:{'words.generated.din'} @:{'words.generated.tvatt'} @:{'words.generated.ar'} @:{'words.generated.nu'} @:{'words.generated.klar'}! @:{'words.generated.tack'} @:{'words.generated.for'} @:{'words.generated.att'} @:{'words.generated.du'} @:{'words.generated.anvander'} @:{'words.generated.var_2'} självbetjäningstvätt.",
|
||||
"wash_completed": "@.capitalize:{'words.generated.tvatt'} @:{'words.generated.slutford'}",
|
||||
|
||||
@@ -1,35 +1,4 @@
|
||||
{
|
||||
"error_report": {
|
||||
"button": "Rapporter fejl",
|
||||
"title": "Rapporter fejl",
|
||||
"subtitle": "Send det aktuelle skærmbillede og de seneste fejldetaljer til support.",
|
||||
"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.",
|
||||
"submit": "Send rapport",
|
||||
"submitted": "Fejlrapporten er sendt.",
|
||||
"capture_failed": "Skærmbilledet kunne ikke oprettes. Prøv igen.",
|
||||
"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",
|
||||
"screenshot": "Skærmbillede",
|
||||
"request_errors": "Request-fejl",
|
||||
"vue_errors": "Vue-fejl",
|
||||
"runtime_context": "Runtime-kontekst",
|
||||
"empty": "Ingen fejlrapporter fundet.",
|
||||
"resolve": "Marker som løst",
|
||||
"reopen": "Genåbn",
|
||||
"resolution_note": "Løsningsnote",
|
||||
"filters": {
|
||||
"open": "Åben",
|
||||
"resolved": "Løst",
|
||||
"all": "Alle"
|
||||
}
|
||||
},
|
||||
"about_us": {
|
||||
"solutions": {
|
||||
"customer": {
|
||||
@@ -583,9 +552,6 @@
|
||||
"settings_wheel": {
|
||||
"associate_order": "Tilknyt ordre",
|
||||
"attach_wash_certificate": "Vedhæft vaskecertifikat",
|
||||
"accept_self_serve_wash": "Godkend selvbetjent vask",
|
||||
"accept_self_serve_wash_confirm": "Flyt denne kladde til kunde #{customerNumber} med selvbetjeningsdetaljerne?",
|
||||
"accept_self_serve_wash_success": "Den selvbetjente vask blev godkendt.",
|
||||
"attached_files": "Vedhæftede filer",
|
||||
"booking": "Booking",
|
||||
"change_association": "Skift tilknytning",
|
||||
@@ -600,20 +566,10 @@
|
||||
"delete_user": "Slet bruger",
|
||||
"download_invoice": "Download faktura",
|
||||
"edit_permissions": "Rediger tilladelser",
|
||||
"email_notifications_section": "E-mail notifikationer",
|
||||
"enter_password": "Indtast adgangskode",
|
||||
"error": "Fejl",
|
||||
"error_changing_password": "Fejl ved ændring af adgangskode",
|
||||
"error_downloading_attachment": "Fejl ved download af vedhæftet fil",
|
||||
"resend_booking_confirmation": "(Gen)send bookingbekræftelse",
|
||||
"resend_booking_confirmation_error": "Bookingbekræftelsen kunne ikke sendes",
|
||||
"resend_booking_confirmation_success": "Bookingbekræftelsen er sendt",
|
||||
"resend_booking_completion_confirmation": "(Gen)send bekræftelse for færdig booking",
|
||||
"resend_booking_completion_confirmation_error": "Bekræftelsen for færdig booking kunne ikke sendes",
|
||||
"resend_booking_completion_confirmation_success": "Bekræftelsen for færdig booking er sendt",
|
||||
"resend_wash_certificate": "(Gen)send vaskecertifikat",
|
||||
"resend_wash_certificate_error": "Vaskecertifikatet kunne ikke sendes",
|
||||
"resend_wash_certificate_success": "Vaskecertifikatet er sendt",
|
||||
"link_copied": "Link kopieret",
|
||||
"login_as_customer": "Log ind som kunde",
|
||||
"login_as_user": "Log ind som bruger",
|
||||
@@ -629,11 +585,6 @@
|
||||
"shortcut_other": "Andet",
|
||||
"shortcut_vehicles": "Køretøjer",
|
||||
"self_serve_studio_section": "Selvvask Studio",
|
||||
"self_serve_wash_attachment": "Selvbetjent vask",
|
||||
"self_serve_wash_attachment_for_customer": "Selvbetjent vask for kunde #{customerNumber}",
|
||||
"self_serve_customer": "Kunde",
|
||||
"self_serve_driver": "Chauffør",
|
||||
"self_serve_elapsed": "Tid",
|
||||
"gates_section": "Porte",
|
||||
"relays_section": "Relæer",
|
||||
"gateways_section": "Gateways",
|
||||
@@ -1740,8 +1691,7 @@
|
||||
"release_runtime": "Release-runtime",
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"use_local_frontend": "Brug lokal frontend"
|
||||
"api": "API"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -3661,13 +3611,7 @@
|
||||
"loading_department": "Indlæser afdeling...",
|
||||
"loading_types": "Indlæser typer...",
|
||||
"loading_vehicle_types": "Indlæser tilgængelige køretøjstyper...",
|
||||
"lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
||||
"machine": "Maskine",
|
||||
"machine_unavailable_for_lane": "Maskinvask er ikke tilgængelig for den valgte bane. Vælg manuel vask eller en anden ledig bane.",
|
||||
"vehicle_step_missing_customer_number": "Indtast kundenummer for at fortsætte.",
|
||||
"vehicle_step_missing_department": "Vent på, at nærmeste afdeling er indlæst, før du fortsætter.",
|
||||
"vehicle_step_missing_registration": "Indtast registreringsnummer for at fortsætte.",
|
||||
"vehicle_step_missing_vehicle_type": "Vælg en tilgængelig køretøjstype for at fortsætte.",
|
||||
"no_departments_found": "Ingen afdelinger fundet",
|
||||
"no_vehicles_found": "Ingen køretøjer fundet",
|
||||
"occupied": "Optaget",
|
||||
|
||||
@@ -1,35 +1,4 @@
|
||||
{
|
||||
"error_report": {
|
||||
"button": "Fehler melden",
|
||||
"title": "Fehler melden",
|
||||
"subtitle": "Sende den aktuellen Bildschirm und die letzten Fehlerdetails an den Support.",
|
||||
"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.",
|
||||
"submit": "Bericht senden",
|
||||
"submitted": "Fehlerbericht gesendet.",
|
||||
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
|
||||
"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",
|
||||
"screenshot": "Screenshot",
|
||||
"request_errors": "Request-Fehler",
|
||||
"vue_errors": "Vue-Fehler",
|
||||
"runtime_context": "Runtime-Kontext",
|
||||
"empty": "Keine Fehlerberichte gefunden.",
|
||||
"resolve": "Als gelöst markieren",
|
||||
"reopen": "Wieder öffnen",
|
||||
"resolution_note": "Lösungsnotiz",
|
||||
"filters": {
|
||||
"open": "Offen",
|
||||
"resolved": "Gelöst",
|
||||
"all": "Alle"
|
||||
}
|
||||
},
|
||||
"about_us": {
|
||||
"solutions": {
|
||||
"customer": {
|
||||
@@ -583,9 +552,6 @@
|
||||
"settings_wheel": {
|
||||
"associate_order": "{order} zuordnen",
|
||||
"attach_wash_certificate": "Waschzertifikat anh?ngen",
|
||||
"accept_self_serve_wash": "Self-Service-Wäsche akzeptieren",
|
||||
"accept_self_serve_wash_confirm": "Diesen Entwurf mit den Self-Service-Details zu Kunde #{customerNumber} verschieben?",
|
||||
"accept_self_serve_wash_success": "Die Self-Service-Wäsche wurde akzeptiert.",
|
||||
"attached_files": "Angeh?ngte Dateien",
|
||||
"booking": "Buchung",
|
||||
"change_association": "Zuordnung ?ndern",
|
||||
@@ -600,20 +566,10 @@
|
||||
"delete_user": "Benutzer l?schen",
|
||||
"download_invoice": "Rechnung herunterladen",
|
||||
"edit_permissions": "Berechtigungen bearbeiten",
|
||||
"email_notifications_section": "E-Mail-Benachrichtigungen",
|
||||
"enter_password": "Passwort eingeben",
|
||||
"error": "Fehler",
|
||||
"error_changing_password": "Beim ?ndern des Passworts ist ein Fehler aufgetreten.",
|
||||
"error_downloading_attachment": "Beim Herunterladen der angeh?ngten Datei ist ein Fehler aufgetreten.",
|
||||
"resend_booking_confirmation": "(Re)send booking confirmation",
|
||||
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
|
||||
"resend_booking_confirmation_success": "The booking confirmation has been sent",
|
||||
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
|
||||
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
|
||||
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
|
||||
"resend_wash_certificate": "(Re)send wash certificate",
|
||||
"resend_wash_certificate_error": "The wash certificate could not be sent",
|
||||
"resend_wash_certificate_success": "The wash certificate has been sent",
|
||||
"link_copied": "Link in die Zwischenablage kopiert",
|
||||
"login_as_customer": "Als Kunde anmelden",
|
||||
"login_as_user": "Als Benutzer anmelden",
|
||||
@@ -629,11 +585,6 @@
|
||||
"shortcut_other": "Sonstiges",
|
||||
"shortcut_vehicles": "Fahrzeuge",
|
||||
"self_serve_studio_section": "Self-serve Studio",
|
||||
"self_serve_wash_attachment": "Self-Service-Wäsche",
|
||||
"self_serve_wash_attachment_for_customer": "Self-Service-Wäsche für Kunde #{customerNumber}",
|
||||
"self_serve_customer": "Kunde",
|
||||
"self_serve_driver": "Fahrer",
|
||||
"self_serve_elapsed": "Zeit",
|
||||
"gates_section": "Tore",
|
||||
"relays_section": "Relais",
|
||||
"gateways_section": "Gateways",
|
||||
@@ -1740,8 +1691,7 @@
|
||||
"release_runtime": "Release-Laufzeit",
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"use_local_frontend": "Lokales Frontend verwenden"
|
||||
"api": "API"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -3661,13 +3611,7 @@
|
||||
"loading_department": "Abteilung wird geladen...",
|
||||
"loading_types": "Typen werden geladen...",
|
||||
"loading_vehicle_types": "Verf?gbare Fahrzeugtypen werden geladen...",
|
||||
"lane_unavailable": "Waschspur nicht verfugbar",
|
||||
"machine": "Maschine",
|
||||
"machine_unavailable_for_lane": "Maschinenwasche ist fur die ausgewahlte Spur nicht verfugbar. Wahlen Sie manuelle Wasche oder eine andere freie Spur.",
|
||||
"vehicle_step_missing_customer_number": "Geben Sie Ihre Kundennummer ein, um fortzufahren.",
|
||||
"vehicle_step_missing_department": "Warten Sie, bis die nachste Abteilung geladen ist, bevor Sie fortfahren.",
|
||||
"vehicle_step_missing_registration": "Geben Sie das Kennzeichen ein, um fortzufahren.",
|
||||
"vehicle_step_missing_vehicle_type": "Wahlen Sie einen verfugbaren Fahrzeugtyp, um fortzufahren.",
|
||||
"no_departments_found": "Keine Abteilungen gefunden",
|
||||
"no_vehicles_found": "Keine Fahrzeuge gefunden",
|
||||
"occupied": "Belegt",
|
||||
|
||||
@@ -1,35 +1,4 @@
|
||||
{
|
||||
"error_report": {
|
||||
"button": "Report error",
|
||||
"title": "Report error",
|
||||
"subtitle": "Send the current screen and recent error details to support.",
|
||||
"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.",
|
||||
"submit": "Submit report",
|
||||
"submitted": "Error report submitted.",
|
||||
"capture_failed": "The screen capture failed. Please try again.",
|
||||
"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",
|
||||
"screenshot": "Screenshot",
|
||||
"request_errors": "Request errors",
|
||||
"vue_errors": "Vue errors",
|
||||
"runtime_context": "Runtime context",
|
||||
"empty": "No error reports found.",
|
||||
"resolve": "Mark resolved",
|
||||
"reopen": "Reopen",
|
||||
"resolution_note": "Resolution note",
|
||||
"filters": {
|
||||
"open": "Open",
|
||||
"resolved": "Resolved",
|
||||
"all": "All"
|
||||
}
|
||||
},
|
||||
"about_us": {
|
||||
"solutions": {
|
||||
"customer": {
|
||||
@@ -583,9 +552,6 @@
|
||||
"settings_wheel": {
|
||||
"associate_order": "Associate {order}",
|
||||
"attach_wash_certificate": "Attach wash certificate",
|
||||
"accept_self_serve_wash": "Accept self-serve wash",
|
||||
"accept_self_serve_wash_confirm": "Move this draft to customer #{customerNumber} with the self-serve details?",
|
||||
"accept_self_serve_wash_success": "The self-serve wash was accepted.",
|
||||
"attached_files": "Attached files",
|
||||
"booking": "Booking",
|
||||
"change_association": "Change association",
|
||||
@@ -600,20 +566,10 @@
|
||||
"delete_user": "Delete user",
|
||||
"download_invoice": "Download invoice",
|
||||
"edit_permissions": "Edit permissions",
|
||||
"email_notifications_section": "Email notifications",
|
||||
"enter_password": "Enter password",
|
||||
"error": "Error",
|
||||
"error_changing_password": "An error occurred while changing the password.",
|
||||
"error_downloading_attachment": "An error occurred while downloading the attached file.",
|
||||
"resend_booking_confirmation": "(Re)send booking confirmation",
|
||||
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
|
||||
"resend_booking_confirmation_success": "The booking confirmation has been sent",
|
||||
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
|
||||
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
|
||||
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
|
||||
"resend_wash_certificate": "(Re)send wash certificate",
|
||||
"resend_wash_certificate_error": "The wash certificate could not be sent",
|
||||
"resend_wash_certificate_success": "The wash certificate has been sent",
|
||||
"link_copied": "Link copied to clipboard",
|
||||
"login_as_customer": "Log in as customer",
|
||||
"login_as_user": "Log in as user",
|
||||
@@ -629,11 +585,6 @@
|
||||
"shortcut_other": "Other",
|
||||
"shortcut_vehicles": "Vehicles",
|
||||
"self_serve_studio_section": "Self-serve Studio",
|
||||
"self_serve_wash_attachment": "Self-serve wash",
|
||||
"self_serve_wash_attachment_for_customer": "Self-serve wash for customer #{customerNumber}",
|
||||
"self_serve_customer": "Customer",
|
||||
"self_serve_driver": "Driver",
|
||||
"self_serve_elapsed": "Elapsed",
|
||||
"gates_section": "Gates",
|
||||
"relays_section": "Relays",
|
||||
"gateways_section": "Gateways",
|
||||
@@ -1740,8 +1691,7 @@
|
||||
"release_runtime": "Release runtime",
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"use_local_frontend": "Use local frontend"
|
||||
"api": "API"
|
||||
},
|
||||
"common": {
|
||||
"yes": "yes",
|
||||
@@ -3578,7 +3528,8 @@
|
||||
"sættevogn": "Semitrailer",
|
||||
"trailer": "Trailer",
|
||||
"traktor": "Trekkbil",
|
||||
"varevogn": "Van"
|
||||
"varevogn": "Van",
|
||||
"s??ttevogn": "Semi-trailer"
|
||||
},
|
||||
"wash_certificate": "Washing certificate",
|
||||
"wash_type": "Washing type"
|
||||
@@ -3660,13 +3611,7 @@
|
||||
"loading_department": "Loading department...",
|
||||
"loading_types": "Loading types...",
|
||||
"loading_vehicle_types": "Loading available vehicle types...",
|
||||
"lane_unavailable": "Lane unavailable",
|
||||
"machine": "Machine",
|
||||
"machine_unavailable_for_lane": "Machine wash is unavailable for the selected lane. Choose manual wash or another available lane.",
|
||||
"vehicle_step_missing_customer_number": "Enter your customer number to continue.",
|
||||
"vehicle_step_missing_department": "Wait for the nearest department to load before continuing.",
|
||||
"vehicle_step_missing_registration": "Enter your registration number to continue.",
|
||||
"vehicle_step_missing_vehicle_type": "Choose an available vehicle type to continue.",
|
||||
"no_departments_found": "No departments found",
|
||||
"no_vehicles_found": "No vehicles found",
|
||||
"occupied": "Occupied",
|
||||
|
||||
@@ -1,35 +1,4 @@
|
||||
{
|
||||
"error_report": {
|
||||
"button": "Rapporter feil",
|
||||
"title": "Rapporter feil",
|
||||
"subtitle": "Send gjeldende skjermbilde og de siste feildetaljene til support.",
|
||||
"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.",
|
||||
"submit": "Send rapport",
|
||||
"submitted": "Feilrapport sendt.",
|
||||
"capture_failed": "Skjermbildet kunne ikke tas. Prøv igjen.",
|
||||
"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",
|
||||
"screenshot": "Skjermbilde",
|
||||
"request_errors": "Request-feil",
|
||||
"vue_errors": "Vue-feil",
|
||||
"runtime_context": "Runtime-kontekst",
|
||||
"empty": "Ingen feilrapporter funnet.",
|
||||
"resolve": "Marker som løst",
|
||||
"reopen": "Åpne igjen",
|
||||
"resolution_note": "Løsningsnotat",
|
||||
"filters": {
|
||||
"open": "Åpen",
|
||||
"resolved": "Løst",
|
||||
"all": "Alle"
|
||||
}
|
||||
},
|
||||
"about_us": {
|
||||
"solutions": {
|
||||
"customer": {
|
||||
@@ -583,9 +552,6 @@
|
||||
"settings_wheel": {
|
||||
"associate_order": "Associate {order}",
|
||||
"attach_wash_certificate": "Legg ved vaskesertifikat",
|
||||
"accept_self_serve_wash": "Godta selvbetjent vask",
|
||||
"accept_self_serve_wash_confirm": "Flytt denne kladden til kunde #{customerNumber} med selvbetjeningsdetaljene?",
|
||||
"accept_self_serve_wash_success": "Den selvbetjente vasken ble godtatt.",
|
||||
"attached_files": "Vedlagte filer",
|
||||
"booking": "Bestilling",
|
||||
"change_association": "Bytt forening",
|
||||
@@ -600,20 +566,10 @@
|
||||
"delete_user": "Delete user",
|
||||
"download_invoice": "Last ned faktura",
|
||||
"edit_permissions": "Rediger tillatelser",
|
||||
"email_notifications_section": "E-postvarsler",
|
||||
"enter_password": "Skriv inn passord",
|
||||
"error": "Feil",
|
||||
"error_changing_password": "Det oppstod en feil under endring av passord.",
|
||||
"error_downloading_attachment": "Det oppsto en feil under nedlasting av den vedlagte filen.",
|
||||
"resend_booking_confirmation": "(Re)send booking confirmation",
|
||||
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
|
||||
"resend_booking_confirmation_success": "The booking confirmation has been sent",
|
||||
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
|
||||
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
|
||||
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
|
||||
"resend_wash_certificate": "(Re)send wash certificate",
|
||||
"resend_wash_certificate_error": "The wash certificate could not be sent",
|
||||
"resend_wash_certificate_success": "The wash certificate has been sent",
|
||||
"link_copied": "Linken er kopiert til utklippstavlen",
|
||||
"login_as_customer": "Logg inn som kunde",
|
||||
"login_as_user": "Logg inn som bruker",
|
||||
@@ -629,11 +585,6 @@
|
||||
"shortcut_other": "Annet",
|
||||
"shortcut_vehicles": "Kjøretøy",
|
||||
"self_serve_studio_section": "Self-serve Studio",
|
||||
"self_serve_wash_attachment": "Selvbetjent vask",
|
||||
"self_serve_wash_attachment_for_customer": "Selvbetjent vask for kunde #{customerNumber}",
|
||||
"self_serve_customer": "Kunde",
|
||||
"self_serve_driver": "Sjåfør",
|
||||
"self_serve_elapsed": "Tid",
|
||||
"gates_section": "Porter",
|
||||
"relays_section": "Reléer",
|
||||
"gateways_section": "Gateways",
|
||||
@@ -1740,8 +1691,7 @@
|
||||
"release_runtime": "Release-runtime",
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"use_local_frontend": "Bruk lokal frontend"
|
||||
"api": "API"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -3661,13 +3611,7 @@
|
||||
"loading_department": "Laster avdelingen...",
|
||||
"loading_types": "Laster inn typer...",
|
||||
"loading_vehicle_types": "Laster inn tilgjengelige kjøretøytyper...",
|
||||
"lane_unavailable": "Vaskebanen er ikke tilgjengelig",
|
||||
"machine": "Maskin",
|
||||
"machine_unavailable_for_lane": "Maskinvask er ikke tilgjengelig for valgt bane. Velg manuell vask eller en annen ledig bane.",
|
||||
"vehicle_step_missing_customer_number": "Skriv inn kundenummer for a fortsette.",
|
||||
"vehicle_step_missing_department": "Vent til naermeste avdeling er lastet inn for du fortsetter.",
|
||||
"vehicle_step_missing_registration": "Skriv inn registreringsnummer for a fortsette.",
|
||||
"vehicle_step_missing_vehicle_type": "Velg en tilgjengelig kjoretoytype for a fortsette.",
|
||||
"no_departments_found": "Ingen avdelinger funnet",
|
||||
"no_vehicles_found": "Ingen kjøretøy funnet",
|
||||
"occupied": "Okkupert",
|
||||
|
||||
@@ -1,35 +1,4 @@
|
||||
{
|
||||
"error_report": {
|
||||
"button": "Rapportera fel",
|
||||
"title": "Rapportera fel",
|
||||
"subtitle": "Skicka aktuell skärm och de senaste feldetaljerna till support.",
|
||||
"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.",
|
||||
"submit": "Skicka rapport",
|
||||
"submitted": "Felrapport skickad.",
|
||||
"capture_failed": "Skärmbilden kunde inte tas. Försök igen.",
|
||||
"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",
|
||||
"screenshot": "Skärmbild",
|
||||
"request_errors": "Request-fel",
|
||||
"vue_errors": "Vue-fel",
|
||||
"runtime_context": "Runtime-kontext",
|
||||
"empty": "Inga felrapporter hittades.",
|
||||
"resolve": "Markera som löst",
|
||||
"reopen": "Öppna igen",
|
||||
"resolution_note": "Lösningsnotering",
|
||||
"filters": {
|
||||
"open": "Öppen",
|
||||
"resolved": "Löst",
|
||||
"all": "Alla"
|
||||
}
|
||||
},
|
||||
"about_us": {
|
||||
"solutions": {
|
||||
"customer": {
|
||||
@@ -583,9 +552,6 @@
|
||||
"settings_wheel": {
|
||||
"associate_order": "Associera {order}",
|
||||
"attach_wash_certificate": "Bifoga tvättcertifikat",
|
||||
"accept_self_serve_wash": "Godkänn självbetjäningstvätt",
|
||||
"accept_self_serve_wash_confirm": "Flytta detta utkast till kund #{customerNumber} med självbetjäningsdetaljerna?",
|
||||
"accept_self_serve_wash_success": "Självbetjäningstvätten godkändes.",
|
||||
"attached_files": "Bifogade filer",
|
||||
"booking": "Bokning",
|
||||
"change_association": "ändra koppling",
|
||||
@@ -600,20 +566,10 @@
|
||||
"delete_user": "Delete user",
|
||||
"download_invoice": "Ladda ner faktura",
|
||||
"edit_permissions": "Edit permissions",
|
||||
"email_notifications_section": "E-postaviseringar",
|
||||
"enter_password": "Enter password",
|
||||
"error": "Fel",
|
||||
"error_changing_password": "Ett fel uppstod när lösenordet ändrades.",
|
||||
"error_downloading_attachment": "Ett fel uppstod när den bifogade filen laddades ner.",
|
||||
"resend_booking_confirmation": "(Re)send booking confirmation",
|
||||
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
|
||||
"resend_booking_confirmation_success": "The booking confirmation has been sent",
|
||||
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
|
||||
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
|
||||
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
|
||||
"resend_wash_certificate": "(Re)send wash certificate",
|
||||
"resend_wash_certificate_error": "The wash certificate could not be sent",
|
||||
"resend_wash_certificate_success": "The wash certificate has been sent",
|
||||
"link_copied": "Link copied to clipboard",
|
||||
"login_as_customer": "Log in as customer",
|
||||
"login_as_user": "Log in as user",
|
||||
@@ -629,11 +585,6 @@
|
||||
"shortcut_other": "Annat",
|
||||
"shortcut_vehicles": "Fordon",
|
||||
"self_serve_studio_section": "Self-serve Studio",
|
||||
"self_serve_wash_attachment": "Självbetjäningstvätt",
|
||||
"self_serve_wash_attachment_for_customer": "Självbetjäningstvätt för kund #{customerNumber}",
|
||||
"self_serve_customer": "Kund",
|
||||
"self_serve_driver": "Förare",
|
||||
"self_serve_elapsed": "Tid",
|
||||
"gates_section": "Grindar",
|
||||
"relays_section": "Reläer",
|
||||
"gateways_section": "Gateways",
|
||||
@@ -1740,8 +1691,7 @@
|
||||
"release_runtime": "Release-runtime",
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"use_local_frontend": "Använd lokal frontend"
|
||||
"api": "API"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -3661,13 +3611,7 @@
|
||||
"loading_department": "Läser in avdelning...",
|
||||
"loading_types": "Laddar typer...",
|
||||
"loading_vehicle_types": "Läser in tillgängliga fordonstyper...",
|
||||
"lane_unavailable": "Tvattbanan ar inte tillganglig",
|
||||
"machine": "Machine",
|
||||
"machine_unavailable_for_lane": "Maskintvatt ar inte tillganglig for vald bana. Valj manuell tvatt eller en annan ledig bana.",
|
||||
"vehicle_step_missing_customer_number": "Ange kundnummer for att fortsatta.",
|
||||
"vehicle_step_missing_department": "Vanta tills narmaste avdelning har laddats innan du fortsatter.",
|
||||
"vehicle_step_missing_registration": "Ange registreringsnummer for att fortsatta.",
|
||||
"vehicle_step_missing_vehicle_type": "Valj en tillganglig fordonstyp for att fortsatta.",
|
||||
"no_departments_found": "Inga avdelningar hittades",
|
||||
"no_vehicles_found": "Inga fordon hittades",
|
||||
"occupied": "Upptagen",
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"compat": {
|
||||
"admin": {
|
||||
"pos": {
|
||||
"settings_wheel": {
|
||||
"accept_self_serve_wash": "Godkend selvbetjent vask",
|
||||
"accept_self_serve_wash_confirm": "Flyt denne kladde til kunde #{customerNumber} med selvbetjeningsdetaljerne?",
|
||||
"accept_self_serve_wash_success": "Den selvbetjente vask blev godkendt.",
|
||||
"email_notifications_section": "E-mail notifikationer",
|
||||
"resend_booking_completion_confirmation": "(Gen)send bekræftelse for færdig booking",
|
||||
"resend_booking_completion_confirmation_error": "Bekræftelsen for færdig booking kunne ikke sendes",
|
||||
"resend_booking_completion_confirmation_success": "Bekræftelsen for færdig booking er sendt",
|
||||
"resend_booking_confirmation": "(Gen)send bookingbekræftelse",
|
||||
"resend_booking_confirmation_error": "Bookingbekræftelsen kunne ikke sendes",
|
||||
"resend_booking_confirmation_success": "Bookingbekræftelsen er sendt",
|
||||
"resend_wash_certificate": "(Gen)send vaskecertifikat",
|
||||
"resend_wash_certificate_error": "Vaskecertifikatet kunne ikke sendes",
|
||||
"resend_wash_certificate_success": "Vaskecertifikatet er sendt",
|
||||
"self_serve_customer": "Kunde",
|
||||
"self_serve_driver": "Chauffør",
|
||||
"self_serve_elapsed": "Tid",
|
||||
"self_serve_wash_attachment": "Selvbetjent vask",
|
||||
"self_serve_wash_attachment_for_customer": "Selvbetjent vask for kunde #{customerNumber}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,6 +298,506 @@
|
||||
"subtitle": "@.capitalize:{'terms.glossary.konfiguration'} @:{'terms.glossary.af'} @:{'terms.glossary.xlvask'} @:{'terms.glossary.integrationen'}",
|
||||
"title": "@:{'terms.glossary.xlvask'} @:{'terms.glossary.konfiguration'}",
|
||||
"username_desc": "@.capitalize:{'terms.glossary.brugernavnet'} @:{'terms.glossary.bruges'} @:{'terms.glossary.til'} @:{'terms.glossary.at'} @:{'terms.glossary.autentificere'} @:{'terms.glossary.api'} @:{'terms.glossary.forbindelsen'}."
|
||||
},
|
||||
"release_manager": {
|
||||
"title": "Release Manager",
|
||||
"subtitle": "Gradvise API- og Front-End-udgivelser",
|
||||
"channel_names": {
|
||||
"stable": "Stabil",
|
||||
"canary": "Canary",
|
||||
"internal": "Intern"
|
||||
},
|
||||
"channel_descriptions": {
|
||||
"stable": "Standard produktionskanal.",
|
||||
"canary": "Tidlig produktionsvalideringskanal.",
|
||||
"internal": "Intern kanal til medarbejdere og superbruger-validering."
|
||||
},
|
||||
"control_api": {
|
||||
"title": "Kontrol-API",
|
||||
"tooltip": "Release Manager-handlinger sendes til denne API. Brug produktions-API'en, medmindre du tester en staging-backend.",
|
||||
"example": "Eksempel: https://api.truckwash.io:4433",
|
||||
"endpoint_label": "API-endpoint",
|
||||
"endpoint_message": "Skift kun dette, når Release Manager-endpoints er tilgængelige på mål-API'en.",
|
||||
"endpoint_aria": "Release Manager kontrol-API URL",
|
||||
"use_tooltip": "Indlæs release-data fra denne API",
|
||||
"use": "Brug",
|
||||
"reset_tooltip": "Vend tilbage til standard kontrol-API",
|
||||
"reset": "Nulstil",
|
||||
"known_endpoints": "Kendte kontrol-API-endpoints"
|
||||
},
|
||||
"tabs": {
|
||||
"overview": {
|
||||
"label": "Oversigt",
|
||||
"description": "Kanalstatus, opsætningsfremdrift og seneste release-tilstand."
|
||||
},
|
||||
"channels": {
|
||||
"label": "Kanaler",
|
||||
"description": "Opret stabile, canary- og målrettede kanaler med rollout-grænser."
|
||||
},
|
||||
"assignments": {
|
||||
"label": "Tildelinger",
|
||||
"description": "Fastgør brugere, subbrugere eller kunder til en bestemt release-kanal."
|
||||
},
|
||||
"deployments": {
|
||||
"label": "Udrulninger",
|
||||
"description": "Start udrulninger og promover vellykkede builds."
|
||||
},
|
||||
"replay": {
|
||||
"label": "Replay",
|
||||
"description": "Aktivér målrettet opsamling og søg i release-tidslinjehændelser."
|
||||
},
|
||||
"integrations": {
|
||||
"label": "Integrationer",
|
||||
"description": "Forbind GitHub-repositories, branches og Coolify-services."
|
||||
},
|
||||
"settings": {
|
||||
"label": "Indstillinger",
|
||||
"description": "Konfigurer Release Managers GitHub-adgang og webhook-indstillinger."
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"summary": "Indl\u00e6ser Release Manager-data...",
|
||||
"refreshing_summary": "Opdaterer Release Manager-data...",
|
||||
"summary_detail": "Release-status, kanaler, deployments og service-health indl\u00e6ses.",
|
||||
"channels": "Indl\u00e6ser release-kanaler...",
|
||||
"channels_detail": "Kanalbaner og release-afh\u00e6ngigheder indl\u00e6ses.",
|
||||
"assignments": "Indl\u00e6ser release-tildelinger...",
|
||||
"deployments": "Indl\u00e6ser release-deployments...",
|
||||
"operations": "Indl\u00e6ser release-operationer...",
|
||||
"data_services": "Indl\u00e6ser release-datatjenester...",
|
||||
"integrations": "Indl\u00e6ser release-integrationer...",
|
||||
"settings": "Indl\u00e6ser Release Manager-indstillinger...",
|
||||
"github_repositories": "Indl\u00e6ser GitHub-repositories...",
|
||||
"timeline_sessions": "Indl\u00e6ser release-tidslinjesessioner...",
|
||||
"timeline_events": "Indl\u00e6ser release-tidslinjeh\u00e6ndelser...",
|
||||
"refreshing_timeline": "Opdaterer release-tidslinje..."
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "GitHub- og Coolify-mål",
|
||||
"assignments": "Pilot-tildelinger",
|
||||
"deployments": "Første udrulning",
|
||||
"replay": "Replay-opsamling"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "Mål",
|
||||
"deployments": "Udrulninger",
|
||||
"timeline_events": "Tidslinjehændelser"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Oversigt",
|
||||
"subtitle": "Aktuel kanalstatus, release-aktivitet og modulstatus.",
|
||||
"guided_setup": "Guidet opsætning",
|
||||
"next_step": "Næste: {step}",
|
||||
"ready": "Release Manager er klar til daglig drift.",
|
||||
"add_suggested_channel": "Tilføj foreslået kanal",
|
||||
"module_health_empty": "Modul-health snapshots vises, når probes er blevet registreret."
|
||||
},
|
||||
"status": {
|
||||
"confirm_issue_action": "Koer denne Release Manager-handling? Den kan aendre deployment-tilstand og bliver auditeret.",
|
||||
"choose_bundle_prompt": "Indtast bundle-id'et, der skal saettes for denne kanal.",
|
||||
"impact": "Konsekvens",
|
||||
"cause": "Aarsag",
|
||||
"automated_fix": "Automatisk rettelse",
|
||||
"manual_fallback": "Manuel fallback",
|
||||
"related_deployment": "Relateret deployment",
|
||||
"recent_result": "Seneste resultat",
|
||||
"no_automated_action": "Ingen automatisk handling tilgaengelig.",
|
||||
"deployment": "Deployment",
|
||||
"target": "Maal",
|
||||
"coolify_target": "Coolify-maal",
|
||||
"service_set": "Service set"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Release Manager-indstillinger",
|
||||
"subtitle": "Konfigurer GitHub-tokenet, der bruges til private repositories og branch-opslag.",
|
||||
"github_token": "GitHub-token",
|
||||
"github_api_url": "GitHub API-URL",
|
||||
"webhook_secret": "Webhook-hemmelighed",
|
||||
"github_webhook_secret": "GitHub webhook-hemmelighed",
|
||||
"configured": "Konfigureret",
|
||||
"not_configured": "Ikke konfigureret",
|
||||
"loaded_from": "Indlæst fra {variable}",
|
||||
"set_below": "Angiv {variable} nedenfor",
|
||||
"private_repositories_prefix": "Private repositories læses med serverens miljøvariabel",
|
||||
"private_repositories_or": "eller modulets konfigurationsvariabel",
|
||||
"github_token_message": "Eksempel: github_pat_... med adgang til de private repositories, Release Manager udruller.",
|
||||
"github_token_placeholder": "Lad feltet være tomt for at beholde det eksisterende token",
|
||||
"github_api_url_message": "Konfigureret i ReleaseManager.github_api_url. Brug https://api.github.com medmindre GitHub Enterprise bruges.",
|
||||
"webhook_secret_message": "Valgfrit; lad feltet være tomt for at beholde den eksisterende hemmelighed.",
|
||||
"webhook_secret_placeholder": "Webhook HMAC-hemmelighed",
|
||||
"save": "Gem indstillinger",
|
||||
"back_to_integrations": "Tilbage til integrationer",
|
||||
"guide": {
|
||||
"steps": {
|
||||
"github_token": "GitHub-token",
|
||||
"github_api_url": "GitHub API-URL",
|
||||
"github_webhook_secret": "GitHub webhook secret",
|
||||
"review": "Gennemgang"
|
||||
},
|
||||
"github_token_title": "Tilføj GitHub-token",
|
||||
"github_token_description": "Release Manager bruger denne token til at vise private repositories, branches og commits, før et deployment-mål gemmes eller testes.",
|
||||
"configure_this_in": "Konfigurer dette i",
|
||||
"server_environment_variable": "Serverens miljøvariabel",
|
||||
"takes_precedence": "har forrang, når den er sat.",
|
||||
"recommended_access": "Anbefalet adgang",
|
||||
"recommended_access_description": "Fine-grained GitHub-token med Contents: read-adgang til hvert release-repository.",
|
||||
"github_api_url_title": "Tilføj GitHub API-URL",
|
||||
"github_api_url_description": "Brug GitHub Cloud som standard, eller peg på REST API-base-URLen for en GitHub Enterprise Server-installation.",
|
||||
"github_api_url_invalid": "Indtast en fuld HTTP- eller HTTPS-URL.",
|
||||
"github_cloud": "GitHub Cloud",
|
||||
"enterprise_example": "Enterprise-eksempel",
|
||||
"github_webhook_secret_title": "Tilføj GitHub webhook secret",
|
||||
"github_webhook_secret_description": "Brug samme secret i GitHub repository-webhooks, så Release Manager kan verificere deployment-events, før de håndteres.",
|
||||
"github_webhook_url": "GitHub webhook-URL:",
|
||||
"store_shared_secret_in": "Gem den delte secret i",
|
||||
"review_title": "Gennemgå GitHub-opsætning",
|
||||
"review_description": "Gem værdierne, og brug derefter Integrationer til at vælge et privat repository, branch og specifik commit eller seneste commit før adgang testes.",
|
||||
"ready": "Klar",
|
||||
"needs_value": "Mangler værdi"
|
||||
}
|
||||
},
|
||||
"channel_unavailable": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Release-kanalen er ikke klar",
|
||||
"summary_prefix": "Din konto er tildelt",
|
||||
"summary_suffix": ", men kanalen mangler påkrævet release-konfiguration.",
|
||||
"release_bundle": "Release-bundle",
|
||||
"frontend_version": "Frontend-version",
|
||||
"api_version": "API-version",
|
||||
"frontend_base_url": "Frontend-URL",
|
||||
"api_base_url": "API-URL",
|
||||
"frontend_entry": "Frontend-entry",
|
||||
"release_runtime": "Release-runtime",
|
||||
"checking_again": "Tjekker igen om {seconds}s",
|
||||
"refresh_error": "Release-status kunne ikke opdateres. Det næste automatiske tjek prøver igen.",
|
||||
"ignore": "Ignorer de næste 5 minutter",
|
||||
"check_again": "Tjek igen",
|
||||
"logout": "Log ud",
|
||||
"base_image": "Basis-image"
|
||||
},
|
||||
"channel_switched": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Du er nu på {channel}",
|
||||
"summary_prefix": "Din konto er blevet tildelt release-kanalen",
|
||||
"summary_suffix": "Denne enhed husker, at du har set denne besked.",
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"frontend_version": "Frontend-version",
|
||||
"api_version": "API-version",
|
||||
"assigned_channel": "Tildelt kanal",
|
||||
"current_app_image": "Nuværende app-image",
|
||||
"current_api": "Nuværende API",
|
||||
"base_image": "Basis-image",
|
||||
"continue": "Fortsæt"
|
||||
},
|
||||
"channel_selector": {
|
||||
"title": "Release-kanal",
|
||||
"subtitle": "Vælg hvilken tildelt release-kanal denne enhed skal bruge.",
|
||||
"sidebar_title": "Release",
|
||||
"default": "Standard",
|
||||
"ready": "Klar",
|
||||
"unavailable": "Ikke klar",
|
||||
"release_bundle": "Release-bundle",
|
||||
"frontend_version": "Frontend-version",
|
||||
"api_version": "API-version",
|
||||
"frontend_base_url": "Frontend-URL",
|
||||
"api_base_url": "API-URL",
|
||||
"frontend_entry": "Frontend-entry",
|
||||
"release_runtime": "Release-runtime",
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"switch_error": "Release-kanalen kunne ikke skiftes. Den forrige kanal er stadig aktiv."
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
"no": "nej",
|
||||
"off": "fra"
|
||||
},
|
||||
"actions": {
|
||||
"edit": "Rediger",
|
||||
"rollback": "Rul tilbage",
|
||||
"update": "Opdater",
|
||||
"create": "Opret",
|
||||
"publish_release": "Publicer release",
|
||||
"clear": "Ryd",
|
||||
"assign": "Tildel",
|
||||
"remove": "Fjern",
|
||||
"test_access": "Test adgang",
|
||||
"deploy": "Deploy",
|
||||
"redeploy": "Redeploy",
|
||||
"promote": "Promover",
|
||||
"enable": "Aktiver",
|
||||
"search": "Søg",
|
||||
"save_target": "Gem mål",
|
||||
"delete": "Slet",
|
||||
"back": "Tilbage",
|
||||
"next": "Næste"
|
||||
},
|
||||
"tables": {
|
||||
"channel": "Kanal",
|
||||
"rollout": "Udrulning",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"replay": "Replay",
|
||||
"actions": "Handlinger",
|
||||
"module": "Modul",
|
||||
"status": "Status",
|
||||
"checked": "Kontrolleret",
|
||||
"subject": "Emne",
|
||||
"reason": "Årsag",
|
||||
"expires": "Udløber",
|
||||
"deployment": "Deployment",
|
||||
"source": "Kilde",
|
||||
"started": "Startet",
|
||||
"target": "Mål",
|
||||
"coolify": "Coolify",
|
||||
"auto": "Auto"
|
||||
},
|
||||
"values": {
|
||||
"app": {
|
||||
"frontend": "frontend",
|
||||
"api": "api"
|
||||
},
|
||||
"capture_level": {
|
||||
"metadata": "metadata",
|
||||
"full_redacted": "fuldt redigeret"
|
||||
},
|
||||
"subject_type": {
|
||||
"user": "bruger",
|
||||
"subuser": "medarbejder",
|
||||
"customer": "kunde",
|
||||
"channel": "kanal"
|
||||
},
|
||||
"severity": {
|
||||
"error": "fejl",
|
||||
"warning": "advarsel",
|
||||
"info": "info"
|
||||
},
|
||||
"status": {
|
||||
"ok": "ok",
|
||||
"active": "aktiv",
|
||||
"deployed": "deployet",
|
||||
"stable": "stabil",
|
||||
"enabled": "aktiveret",
|
||||
"queued": "i kø",
|
||||
"deploying": "deploying",
|
||||
"metadata": "metadata",
|
||||
"not_configured": "ikke konfigureret",
|
||||
"warning": "advarsel",
|
||||
"failed": "fejlet",
|
||||
"down": "nede",
|
||||
"error": "fejl",
|
||||
"unknown": "ukendt"
|
||||
}
|
||||
},
|
||||
"autocomplete": {
|
||||
"suggested_value": "Foreslået værdi",
|
||||
"suggestion": "Forslag",
|
||||
"use_typed_value": "Brug indtastet værdi",
|
||||
"custom": "Tilpasset",
|
||||
"api_url": "API-URL",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"known_release_url": "Kendt release-URL",
|
||||
"deploy_here_with_coolify": "Deploy {app} her med Coolify",
|
||||
"prepare_coolify_target": "Forbered et Coolify-mål med HTTPS/SSL aktiveret",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"github_repository": "GitHub-repository",
|
||||
"owner_repository": "ejer/repository",
|
||||
"git_branch": "Git-branch",
|
||||
"deployment_branch": "Deployment-branch",
|
||||
"github_commit": "GitHub-commit",
|
||||
"git_commit": "Git-commit",
|
||||
"use_specific_commit_sha": "Brug specifik commit-SHA",
|
||||
"custom_commit": "Tilpasset commit",
|
||||
"health_url": "Health-URL",
|
||||
"release_health_check": "Release health check",
|
||||
"coolify_service": "Coolify-service",
|
||||
"service_uuid": "Service-UUID",
|
||||
"known_coolify_service_uuid": "Kendt Coolify service-UUID",
|
||||
"control_api": "Kontrol-API",
|
||||
"release_manager_api_endpoint": "Release Manager API-endpoint",
|
||||
"module": "Modul",
|
||||
"timeline_module_key": "Timeline-modulnøgle"
|
||||
},
|
||||
"errors": {
|
||||
"github_repositories": "Kunne ikke indlæse GitHub-repositories.",
|
||||
"github_branches": "Kunne ikke indlæse GitHub-branches.",
|
||||
"repository_branch_required": "Repository og branch er påkrævet før GitHub-adgang kan testes.",
|
||||
"github_access_failed": "Test af GitHub repository-adgang fejlede."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Kanaler",
|
||||
"subtitle": "Opret og juster gradvise release-kanaler.",
|
||||
"rollout_percent": "{percent}% udrulning",
|
||||
"already_configured": "Allerede konfigureret",
|
||||
"slug": "Slug",
|
||||
"slug_message": "Eksempel: canary",
|
||||
"slug_placeholder": "slug",
|
||||
"name": "Navn",
|
||||
"name_message": "Vises til operatører, når de gennemgår en kanal.",
|
||||
"name_placeholder": "Navn",
|
||||
"rollout": "Udrulning",
|
||||
"rollout_message": "0-100 procent af ikke-tildelt trafik.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"frontend_url_message": "Eksempel: https://canary.example.test",
|
||||
"frontend_url_placeholder": "Frontend-URL",
|
||||
"no_matching_release_url": "Ingen matchende release-URL.",
|
||||
"api_url": "API-URL",
|
||||
"api_url_message": "Eksempel: https://api-canary.example.test",
|
||||
"api_url_placeholder": "API-URL",
|
||||
"no_matching_api_url": "Ingen matchende API-URL.",
|
||||
"replay_capture": "Replay capture",
|
||||
"replay_capture_message": "Metadata er sikrere til brede kanaler.",
|
||||
"retention_days": "Opbevaringsdage",
|
||||
"retention_days_message": "1-365 dage.",
|
||||
"enabled": "Aktiveret",
|
||||
"default_channel": "Standardkanal",
|
||||
"replay_enabled": "Replay aktiveret",
|
||||
"frontend_chip": "Frontend: {url}",
|
||||
"api_chip": "API: {url}"
|
||||
},
|
||||
"assignments": {
|
||||
"title": "Tildelinger",
|
||||
"subtitle": "Fastgør brugere, medarbejdere eller kunder til release-kanaler.",
|
||||
"subject": "Emne",
|
||||
"subject_message": "Søg efter brugere, medarbejdere eller kunder, eller skriv et manuelt emne som user:42.",
|
||||
"subject_placeholder": "Søg eller skriv emne",
|
||||
"subject_empty": "Ingen emner fundet",
|
||||
"group_users": "Brugere",
|
||||
"group_subusers": "Medarbejdere",
|
||||
"group_customers": "Kunder",
|
||||
"group_manual": "Manuel",
|
||||
"manual_user": "Bruger #{id}",
|
||||
"manual_subuser": "Medarbejder #{id}",
|
||||
"manual_customer": "Kunde #{id}",
|
||||
"manual_description": "Brug indtastet værdi {subject}",
|
||||
"subject_type": "Emnetype",
|
||||
"subject_type_message": "Vælg identitetstypen, der skal fastgøres.",
|
||||
"customer": "Kunde",
|
||||
"customer_message": "Søg på kundenummer eller navn, og vælg derefter den præcise kunde.",
|
||||
"customer_placeholder": "Søg kunde",
|
||||
"subject_id": "Emne-ID",
|
||||
"subject_id_message": "Eksempel: bruger-id 42",
|
||||
"subject_id_placeholder": "Emne-id",
|
||||
"channel": "Kanal",
|
||||
"channel_message": "Fastgjorte emner springer procentudrulningen over.",
|
||||
"reason": "Årsag",
|
||||
"reason_message": "Eksempel: canary-validering for kundesupport.",
|
||||
"reason_placeholder": "Årsag",
|
||||
"expires": "Udløber",
|
||||
"expires_message": "Lad feltet være tomt for ingen automatisk udløb."
|
||||
},
|
||||
"deployments": {
|
||||
"title": "Deployments",
|
||||
"subtitle": "Start, promover og inspicer release-deployments.",
|
||||
"manual_deployment_record": "Manuel deployment-post",
|
||||
"target": "Mål",
|
||||
"target_message": "Vælg et gemt mål, eller behold manuelle felter redigerbare.",
|
||||
"select_target_or_manual": "Vælg mål eller manuel",
|
||||
"channel": "Kanal",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Eksempel: truckwash/front-end-vue",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: master",
|
||||
"branch_placeholder": "branch",
|
||||
"commit_selection": "Commit-valg",
|
||||
"commit_selection_message": "Seneste slås op som branchens head med den konfigurerede GitHub-token.",
|
||||
"latest_from_branch": "Seneste fra branch",
|
||||
"specific_commit": "Specifik commit",
|
||||
"specific_commit_message": "Eksempel: feedface1234",
|
||||
"commit_sha_placeholder": "commit sha",
|
||||
"version_label": "Versionslabel",
|
||||
"version_label_message": "Eksempel: api-2026.05.19",
|
||||
"version_label_placeholder": "versionslabel",
|
||||
"commit_prefix": "Commit: {commit}",
|
||||
"test_access_tooltip": "Bekræft token-adgang til det valgte repository, branch og commit."
|
||||
},
|
||||
"replay": {
|
||||
"title": "Replay",
|
||||
"subtitle": "Aktiver målrettet capture og gennemgå fejltimelines trin for trin.",
|
||||
"capture_channel": "Capture {channel}",
|
||||
"target_type": "Måltype",
|
||||
"target": "Mål",
|
||||
"target_message": "Eksempel: bruger-id 42 eller kanal-slug canary.",
|
||||
"target_placeholder": "Mål-id eller kanal-slug",
|
||||
"channel_scope": "Kanalafgrænsning",
|
||||
"channel_scope_message": "Valgfrit; begrænser replay capture til en kanal.",
|
||||
"no_channel_scope": "Ingen kanalafgrænsning",
|
||||
"capture_level": "Capture-niveau",
|
||||
"expires": "Udløber",
|
||||
"expires_message": "Lad feltet være tomt for manuel deaktivering.",
|
||||
"trace_id": "Trace-ID",
|
||||
"trace_id_message": "Eksempel: trace-canary-1",
|
||||
"trace_id_placeholder": "Trace-id",
|
||||
"channel": "Kanal",
|
||||
"channel_placeholder": "Kanal",
|
||||
"principal_id": "Principal-ID",
|
||||
"principal_id_placeholder": "Principal-id",
|
||||
"module": "Modul",
|
||||
"module_placeholder": "Modul",
|
||||
"severity": "Alvorlighed",
|
||||
"any_severity": "Alle alvorligheder",
|
||||
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "GitHub og Coolify",
|
||||
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
|
||||
"choose_repository": "Vælg repository",
|
||||
"add_health_url": "tilføj health-URL",
|
||||
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
|
||||
"open_github_token_settings": "Åbn GitHub token-indstillinger",
|
||||
"channel": "Kanal",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Eksempel: truckwash/backend-php",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: release/canary",
|
||||
"branch_placeholder": "branch",
|
||||
"latest_prefix": "Seneste: {commit}",
|
||||
"coolify_instance": "Coolify-instans",
|
||||
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
|
||||
"no_coolify_instance": "Ingen Coolify-instans",
|
||||
"coolify_instance_placeholder": "Coolify-instans-id",
|
||||
"coolify_service_uuid": "Coolify service-UUID",
|
||||
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
|
||||
"coolify_service_uuid_placeholder": "Coolify service-UUID",
|
||||
"health_url": "Health-URL",
|
||||
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
|
||||
"health_url_placeholder": "Health-URL",
|
||||
"ssl_domain": "Load balancer-domæne",
|
||||
"ssl_domain_message": "Skal være et DNS-domæne, der routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
|
||||
"ssl_domain_placeholder": "Load balancer-domæne",
|
||||
"https_domain_for_coolify": "Domæne kontrolleret af load balanceren",
|
||||
"endpoint_mode": "Endpoint mode",
|
||||
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
|
||||
"endpoint_mode_auto": "Auto",
|
||||
"endpoint_mode_manual": "Manual",
|
||||
"manual_endpoint_host": "Manual host",
|
||||
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
|
||||
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
|
||||
"manual_endpoint_port": "Manual public port",
|
||||
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
|
||||
"app_port": "App port",
|
||||
"app_port_message": "Internal container port exposed to Coolify routing.",
|
||||
"auto_gateway_endpoint": "Automatic gateway endpoint",
|
||||
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
|
||||
"auto_deploy": "Auto deploy",
|
||||
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
|
||||
"deploy_now": "Deploy nu",
|
||||
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
|
||||
"create_missing_service": "Opret manglende service",
|
||||
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
|
||||
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"compat": {
|
||||
"configuration": {
|
||||
"release_manager": {
|
||||
"integrations": {
|
||||
"title": "GitHub og Coolify",
|
||||
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
|
||||
"choose_repository": "Vælg repository",
|
||||
"add_health_url": "tilføj health-URL",
|
||||
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
|
||||
"open_github_token_settings": "Åbn GitHub token-indstillinger",
|
||||
"channel": "Kanal",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Eksempel: truckwash/backend-php",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: release/canary",
|
||||
"branch_placeholder": "branch",
|
||||
"latest_prefix": "Seneste: {commit}",
|
||||
"coolify_instance": "Coolify-instans",
|
||||
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
|
||||
"no_coolify_instance": "Ingen Coolify-instans",
|
||||
"coolify_instance_placeholder": "Coolify-instans-id",
|
||||
"coolify_service_uuid": "Coolify service-UUID",
|
||||
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
|
||||
"coolify_service_uuid_placeholder": "Coolify service-UUID",
|
||||
"health_url": "Health-URL",
|
||||
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
|
||||
"health_url_placeholder": "Health-URL",
|
||||
"ssl_domain": "Load balancer-domæne",
|
||||
"ssl_domain_message": "Skal være et DNS-domæne, der routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
|
||||
"ssl_domain_placeholder": "Load balancer-domæne",
|
||||
"https_domain_for_coolify": "Domæne kontrolleret af load balanceren",
|
||||
"endpoint_mode": "Endpoint mode",
|
||||
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
|
||||
"endpoint_mode_auto": "Auto",
|
||||
"endpoint_mode_manual": "Manual",
|
||||
"manual_endpoint_host": "Manual host",
|
||||
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
|
||||
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
|
||||
"manual_endpoint_port": "Manual public port",
|
||||
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
|
||||
"app_port": "App port",
|
||||
"app_port_message": "Internal container port exposed to Coolify routing.",
|
||||
"auto_gateway_endpoint": "Automatic gateway endpoint",
|
||||
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
|
||||
"auto_deploy": "Auto deploy",
|
||||
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
|
||||
"deploy_now": "Deploy nu",
|
||||
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
|
||||
"create_missing_service": "Opret manglende service",
|
||||
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
|
||||
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,454 +0,0 @@
|
||||
{
|
||||
"compat": {
|
||||
"configuration": {
|
||||
"release_manager": {
|
||||
"title": "Release Manager",
|
||||
"subtitle": "Gradvise API- og Front-End-udgivelser",
|
||||
"channel_names": {
|
||||
"stable": "Stabil",
|
||||
"canary": "Canary",
|
||||
"internal": "Intern"
|
||||
},
|
||||
"channel_descriptions": {
|
||||
"stable": "Standard produktionskanal.",
|
||||
"canary": "Tidlig produktionsvalideringskanal.",
|
||||
"internal": "Intern kanal til medarbejdere og superbruger-validering."
|
||||
},
|
||||
"control_api": {
|
||||
"title": "Kontrol-API",
|
||||
"tooltip": "Release Manager-handlinger sendes til denne API. Brug produktions-API'en, medmindre du tester en staging-backend.",
|
||||
"example": "Eksempel: https://api.truckwash.io:4433",
|
||||
"endpoint_label": "API-endpoint",
|
||||
"endpoint_message": "Skift kun dette, når Release Manager-endpoints er tilgængelige på mål-API'en.",
|
||||
"endpoint_aria": "Release Manager kontrol-API URL",
|
||||
"use_tooltip": "Indlæs release-data fra denne API",
|
||||
"use": "Brug",
|
||||
"reset_tooltip": "Vend tilbage til standard kontrol-API",
|
||||
"reset": "Nulstil",
|
||||
"known_endpoints": "Kendte kontrol-API-endpoints"
|
||||
},
|
||||
"tabs": {
|
||||
"overview": {
|
||||
"label": "Oversigt",
|
||||
"description": "Kanalstatus, opsætningsfremdrift og seneste release-tilstand."
|
||||
},
|
||||
"channels": {
|
||||
"label": "Kanaler",
|
||||
"description": "Opret stabile, canary- og målrettede kanaler med rollout-grænser."
|
||||
},
|
||||
"assignments": {
|
||||
"label": "Tildelinger",
|
||||
"description": "Fastgør brugere, subbrugere eller kunder til en bestemt release-kanal."
|
||||
},
|
||||
"deployments": {
|
||||
"label": "Udrulninger",
|
||||
"description": "Start udrulninger og promover vellykkede builds."
|
||||
},
|
||||
"replay": {
|
||||
"label": "Replay",
|
||||
"description": "Aktivér målrettet opsamling og søg i release-tidslinjehændelser."
|
||||
},
|
||||
"integrations": {
|
||||
"label": "Integrationer",
|
||||
"description": "Forbind GitHub-repositories, branches og Coolify-services."
|
||||
},
|
||||
"settings": {
|
||||
"label": "Indstillinger",
|
||||
"description": "Konfigurer Release Managers GitHub-adgang og webhook-indstillinger."
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"summary": "Indlæser Release Manager-data...",
|
||||
"refreshing_summary": "Opdaterer Release Manager-data...",
|
||||
"summary_detail": "Release-status, kanaler, deployments og service-health indlæses.",
|
||||
"channels": "Indlæser release-kanaler...",
|
||||
"channels_detail": "Kanalbaner og release-afhængigheder indlæses.",
|
||||
"assignments": "Indlæser release-tildelinger...",
|
||||
"deployments": "Indlæser release-deployments...",
|
||||
"operations": "Indlæser release-operationer...",
|
||||
"data_services": "Indlæser release-datatjenester...",
|
||||
"integrations": "Indlæser release-integrationer...",
|
||||
"settings": "Indlæser Release Manager-indstillinger...",
|
||||
"github_repositories": "Indlæser GitHub-repositories...",
|
||||
"timeline_sessions": "Indlæser release-tidslinjesessioner...",
|
||||
"timeline_events": "Indlæser release-tidslinjehændelser...",
|
||||
"refreshing_timeline": "Opdaterer release-tidslinje..."
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "GitHub- og Coolify-mål",
|
||||
"assignments": "Pilot-tildelinger",
|
||||
"deployments": "Første udrulning",
|
||||
"replay": "Replay-opsamling"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "Mål",
|
||||
"deployments": "Udrulninger",
|
||||
"timeline_events": "Tidslinjehændelser"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Oversigt",
|
||||
"subtitle": "Aktuel kanalstatus, release-aktivitet og modulstatus.",
|
||||
"guided_setup": "Guidet opsætning",
|
||||
"next_step": "Næste: {step}",
|
||||
"ready": "Release Manager er klar til daglig drift.",
|
||||
"add_suggested_channel": "Tilføj foreslået kanal",
|
||||
"module_health_empty": "Modul-health snapshots vises, når probes er blevet registreret."
|
||||
},
|
||||
"status": {
|
||||
"confirm_issue_action": "Koer denne Release Manager-handling? Den kan aendre deployment-tilstand og bliver auditeret.",
|
||||
"choose_bundle_prompt": "Indtast bundle-id'et, der skal saettes for denne kanal.",
|
||||
"impact": "Konsekvens",
|
||||
"cause": "Aarsag",
|
||||
"automated_fix": "Automatisk rettelse",
|
||||
"manual_fallback": "Manuel fallback",
|
||||
"related_deployment": "Relateret deployment",
|
||||
"recent_result": "Seneste resultat",
|
||||
"no_automated_action": "Ingen automatisk handling tilgængelig.",
|
||||
"deployment": "Deployment",
|
||||
"target": "Maal",
|
||||
"coolify_target": "Coolify-maal",
|
||||
"service_set": "Service set"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Release Manager-indstillinger",
|
||||
"subtitle": "Konfigurer GitHub-tokenet, der bruges til private repositories og branch-opslag.",
|
||||
"github_token": "GitHub-token",
|
||||
"github_api_url": "GitHub API-URL",
|
||||
"webhook_secret": "Webhook-hemmelighed",
|
||||
"github_webhook_secret": "GitHub webhook-hemmelighed",
|
||||
"configured": "Konfigureret",
|
||||
"not_configured": "Ikke konfigureret",
|
||||
"loaded_from": "Indlæst fra {variable}",
|
||||
"set_below": "Angiv {variable} nedenfor",
|
||||
"private_repositories_prefix": "Private repositories læses med serverens miljøvariabel",
|
||||
"private_repositories_or": "eller modulets konfigurationsvariabel",
|
||||
"github_token_message": "Eksempel: github_pat_... med adgang til de private repositories, Release Manager udruller.",
|
||||
"github_token_placeholder": "Lad feltet være tomt for at beholde det eksisterende token",
|
||||
"github_api_url_message": "Konfigureret i ReleaseManager.github_api_url. Brug https://api.github.com medmindre GitHub Enterprise bruges.",
|
||||
"webhook_secret_message": "Valgfrit; lad feltet være tomt for at beholde den eksisterende hemmelighed.",
|
||||
"webhook_secret_placeholder": "Webhook HMAC-hemmelighed",
|
||||
"save": "Gem indstillinger",
|
||||
"back_to_integrations": "Tilbage til integrationer",
|
||||
"guide": {
|
||||
"steps": {
|
||||
"github_token": "GitHub-token",
|
||||
"github_api_url": "GitHub API-URL",
|
||||
"github_webhook_secret": "GitHub webhook secret",
|
||||
"review": "Gennemgang"
|
||||
},
|
||||
"github_token_title": "Tilføj GitHub-token",
|
||||
"github_token_description": "Release Manager bruger denne token til at vise private repositories, branches og commits, før et deployment-mål gemmes eller testes.",
|
||||
"configure_this_in": "Konfigurer dette i",
|
||||
"server_environment_variable": "Serverens miljøvariabel",
|
||||
"takes_precedence": "har forrang, når den er sat.",
|
||||
"recommended_access": "Anbefalet adgang",
|
||||
"recommended_access_description": "Fine-grained GitHub-token med Contents: read-adgang til hvert release-repository.",
|
||||
"github_api_url_title": "Tilføj GitHub API-URL",
|
||||
"github_api_url_description": "Brug GitHub Cloud som standard, eller peg på REST API-base-URLen for en GitHub Enterprise Server-installation.",
|
||||
"github_api_url_invalid": "Indtast en fuld HTTP- eller HTTPS-URL.",
|
||||
"github_cloud": "GitHub Cloud",
|
||||
"enterprise_example": "Enterprise-eksempel",
|
||||
"github_webhook_secret_title": "Tilføj GitHub webhook secret",
|
||||
"github_webhook_secret_description": "Brug samme secret i GitHub repository-webhooks, så Release Manager kan verificere deployment-events, før de håndteres.",
|
||||
"github_webhook_url": "GitHub webhook-URL:",
|
||||
"store_shared_secret_in": "Gem den delte secret i",
|
||||
"review_title": "Gennemgå GitHub-opsætning",
|
||||
"review_description": "Gem værdierne, og brug derefter Integrationer til at vælge et privat repository, branch og specifik commit eller seneste commit før adgang testes.",
|
||||
"ready": "Klar",
|
||||
"needs_value": "Mangler værdi"
|
||||
}
|
||||
},
|
||||
"channel_unavailable": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Release-kanalen er ikke klar",
|
||||
"summary_prefix": "Din konto er tildelt",
|
||||
"summary_suffix": ", men kanalen mangler påkrævet release-konfiguration.",
|
||||
"release_bundle": "Release-bundle",
|
||||
"frontend_version": "Frontend-version",
|
||||
"api_version": "API-version",
|
||||
"frontend_base_url": "Frontend-URL",
|
||||
"api_base_url": "API-URL",
|
||||
"frontend_entry": "Frontend-entry",
|
||||
"release_runtime": "Release-runtime",
|
||||
"checking_again": "Tjekker igen om {seconds}s",
|
||||
"refresh_error": "Release-status kunne ikke opdateres. Det næste automatiske tjek prøver igen.",
|
||||
"ignore": "Ignorer de næste 5 minutter",
|
||||
"check_again": "Tjek igen",
|
||||
"logout": "Log ud",
|
||||
"base_image": "Basis-image"
|
||||
},
|
||||
"channel_switched": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Du er nu på {channel}",
|
||||
"summary_prefix": "Din konto er blevet tildelt release-kanalen",
|
||||
"summary_suffix": "Denne enhed husker, at du har set denne besked.",
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"frontend_version": "Frontend-version",
|
||||
"api_version": "API-version",
|
||||
"assigned_channel": "Tildelt kanal",
|
||||
"current_app_image": "Nuværende app-image",
|
||||
"current_api": "Nuværende API",
|
||||
"base_image": "Basis-image",
|
||||
"continue": "Fortsæt"
|
||||
},
|
||||
"channel_selector": {
|
||||
"title": "Release-kanal",
|
||||
"subtitle": "Vælg hvilken tildelt release-kanal denne enhed skal bruge.",
|
||||
"sidebar_title": "Release",
|
||||
"default": "Standard",
|
||||
"ready": "Klar",
|
||||
"unavailable": "Ikke klar",
|
||||
"release_bundle": "Release-bundle",
|
||||
"frontend_version": "Frontend-version",
|
||||
"api_version": "API-version",
|
||||
"frontend_base_url": "Frontend-URL",
|
||||
"api_base_url": "API-URL",
|
||||
"frontend_entry": "Frontend-entry",
|
||||
"release_runtime": "Release-runtime",
|
||||
"bundle": "Bundle",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"use_local_frontend": "Brug lokal frontend",
|
||||
"switch_error": "Release-kanalen kunne ikke skiftes. Den forrige kanal er stadig aktiv."
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
"no": "nej",
|
||||
"off": "fra"
|
||||
},
|
||||
"actions": {
|
||||
"edit": "Rediger",
|
||||
"rollback": "Rul tilbage",
|
||||
"update": "Opdater",
|
||||
"create": "Opret",
|
||||
"publish_release": "Publicer release",
|
||||
"clear": "Ryd",
|
||||
"assign": "Tildel",
|
||||
"remove": "Fjern",
|
||||
"test_access": "Test adgang",
|
||||
"deploy": "Deploy",
|
||||
"redeploy": "Redeploy",
|
||||
"promote": "Promover",
|
||||
"enable": "Aktiver",
|
||||
"search": "Søg",
|
||||
"save_target": "Gem mål",
|
||||
"delete": "Slet",
|
||||
"back": "Tilbage",
|
||||
"next": "Næste"
|
||||
},
|
||||
"tables": {
|
||||
"channel": "Kanal",
|
||||
"rollout": "Udrulning",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"replay": "Replay",
|
||||
"actions": "Handlinger",
|
||||
"module": "Modul",
|
||||
"status": "Status",
|
||||
"checked": "Kontrolleret",
|
||||
"subject": "Emne",
|
||||
"reason": "Årsag",
|
||||
"expires": "Udløber",
|
||||
"deployment": "Deployment",
|
||||
"source": "Kilde",
|
||||
"started": "Startet",
|
||||
"target": "Mål",
|
||||
"coolify": "Coolify",
|
||||
"auto": "Auto"
|
||||
},
|
||||
"values": {
|
||||
"app": {
|
||||
"frontend": "frontend",
|
||||
"api": "api"
|
||||
},
|
||||
"capture_level": {
|
||||
"metadata": "metadata",
|
||||
"full_redacted": "fuldt redigeret"
|
||||
},
|
||||
"subject_type": {
|
||||
"user": "bruger",
|
||||
"subuser": "medarbejder",
|
||||
"customer": "kunde",
|
||||
"channel": "kanal"
|
||||
},
|
||||
"severity": {
|
||||
"error": "fejl",
|
||||
"warning": "advarsel",
|
||||
"info": "info"
|
||||
},
|
||||
"status": {
|
||||
"ok": "ok",
|
||||
"active": "aktiv",
|
||||
"deployed": "deployet",
|
||||
"stable": "stabil",
|
||||
"enabled": "aktiveret",
|
||||
"queued": "i kø",
|
||||
"deploying": "deploying",
|
||||
"metadata": "metadata",
|
||||
"not_configured": "ikke konfigureret",
|
||||
"warning": "advarsel",
|
||||
"failed": "fejlet",
|
||||
"down": "nede",
|
||||
"error": "fejl",
|
||||
"unknown": "ukendt"
|
||||
}
|
||||
},
|
||||
"autocomplete": {
|
||||
"suggested_value": "Foreslået værdi",
|
||||
"suggestion": "Forslag",
|
||||
"use_typed_value": "Brug indtastet værdi",
|
||||
"custom": "Tilpasset",
|
||||
"api_url": "API-URL",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"known_release_url": "Kendt release-URL",
|
||||
"deploy_here_with_coolify": "Deploy {app} her med Coolify",
|
||||
"prepare_coolify_target": "Forbered et Coolify-mål med HTTPS/SSL aktiveret",
|
||||
"coolify_ssl": "Coolify SSL",
|
||||
"github_repository": "GitHub-repository",
|
||||
"owner_repository": "ejer/repository",
|
||||
"git_branch": "Git-branch",
|
||||
"deployment_branch": "Deployment-branch",
|
||||
"github_commit": "GitHub-commit",
|
||||
"git_commit": "Git-commit",
|
||||
"use_specific_commit_sha": "Brug specifik commit-SHA",
|
||||
"custom_commit": "Tilpasset commit",
|
||||
"health_url": "Health-URL",
|
||||
"release_health_check": "Release health check",
|
||||
"coolify_service": "Coolify-service",
|
||||
"service_uuid": "Service-UUID",
|
||||
"known_coolify_service_uuid": "Kendt Coolify service-UUID",
|
||||
"control_api": "Kontrol-API",
|
||||
"release_manager_api_endpoint": "Release Manager API-endpoint",
|
||||
"module": "Modul",
|
||||
"timeline_module_key": "Timeline-modulnøgle"
|
||||
},
|
||||
"errors": {
|
||||
"github_repositories": "Kunne ikke indlæse GitHub-repositories.",
|
||||
"github_branches": "Kunne ikke indlæse GitHub-branches.",
|
||||
"repository_branch_required": "Repository og branch er påkrævet før GitHub-adgang kan testes.",
|
||||
"github_access_failed": "Test af GitHub repository-adgang fejlede."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Kanaler",
|
||||
"subtitle": "Opret og juster gradvise release-kanaler.",
|
||||
"rollout_percent": "{percent}% udrulning",
|
||||
"already_configured": "Allerede konfigureret",
|
||||
"slug": "Slug",
|
||||
"slug_message": "Eksempel: canary",
|
||||
"slug_placeholder": "slug",
|
||||
"name": "Navn",
|
||||
"name_message": "Vises til operatører, når de gennemgår en kanal.",
|
||||
"name_placeholder": "Navn",
|
||||
"rollout": "Udrulning",
|
||||
"rollout_message": "0-100 procent af ikke-tildelt trafik.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"frontend_url_message": "Eksempel: https://canary.example.test",
|
||||
"frontend_url_placeholder": "Frontend-URL",
|
||||
"no_matching_release_url": "Ingen matchende release-URL.",
|
||||
"api_url": "API-URL",
|
||||
"api_url_message": "Eksempel: https://api-canary.example.test",
|
||||
"api_url_placeholder": "API-URL",
|
||||
"no_matching_api_url": "Ingen matchende API-URL.",
|
||||
"replay_capture": "Replay capture",
|
||||
"replay_capture_message": "Metadata er sikrere til brede kanaler.",
|
||||
"retention_days": "Opbevaringsdage",
|
||||
"retention_days_message": "1-365 dage.",
|
||||
"enabled": "Aktiveret",
|
||||
"default_channel": "Standardkanal",
|
||||
"replay_enabled": "Replay aktiveret",
|
||||
"frontend_chip": "Frontend: {url}",
|
||||
"api_chip": "API: {url}"
|
||||
},
|
||||
"assignments": {
|
||||
"title": "Tildelinger",
|
||||
"subtitle": "Fastgør brugere, medarbejdere eller kunder til release-kanaler.",
|
||||
"subject": "Emne",
|
||||
"subject_message": "Søg efter brugere, medarbejdere eller kunder, eller skriv et manuelt emne som user:42.",
|
||||
"subject_placeholder": "Søg eller skriv emne",
|
||||
"subject_empty": "Ingen emner fundet",
|
||||
"group_users": "Brugere",
|
||||
"group_subusers": "Medarbejdere",
|
||||
"group_customers": "Kunder",
|
||||
"group_manual": "Manuel",
|
||||
"manual_user": "Bruger #{id}",
|
||||
"manual_subuser": "Medarbejder #{id}",
|
||||
"manual_customer": "Kunde #{id}",
|
||||
"manual_description": "Brug indtastet værdi {subject}",
|
||||
"subject_type": "Emnetype",
|
||||
"subject_type_message": "Vælg identitetstypen, der skal fastgøres.",
|
||||
"customer": "Kunde",
|
||||
"customer_message": "Søg på kundenummer eller navn, og vælg derefter den præcise kunde.",
|
||||
"customer_placeholder": "Søg kunde",
|
||||
"subject_id": "Emne-ID",
|
||||
"subject_id_message": "Eksempel: bruger-id 42",
|
||||
"subject_id_placeholder": "Emne-id",
|
||||
"channel": "Kanal",
|
||||
"channel_message": "Fastgjorte emner springer procentudrulningen over.",
|
||||
"reason": "Årsag",
|
||||
"reason_message": "Eksempel: canary-validering for kundesupport.",
|
||||
"reason_placeholder": "Årsag",
|
||||
"expires": "Udløber",
|
||||
"expires_message": "Lad feltet være tomt for ingen automatisk udløb."
|
||||
},
|
||||
"deployments": {
|
||||
"title": "Deployments",
|
||||
"subtitle": "Start, promover og inspicer release-deployments.",
|
||||
"manual_deployment_record": "Manuel deployment-post",
|
||||
"target": "Mål",
|
||||
"target_message": "Vælg et gemt mål, eller behold manuelle felter redigerbare.",
|
||||
"select_target_or_manual": "Vælg mål eller manuel",
|
||||
"channel": "Kanal",
|
||||
"app": "App",
|
||||
"repository": "Repository",
|
||||
"repository_message": "Eksempel: truckwash/front-end-vue",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: master",
|
||||
"branch_placeholder": "branch",
|
||||
"commit_selection": "Commit-valg",
|
||||
"commit_selection_message": "Seneste slås op som branchens head med den konfigurerede GitHub-token.",
|
||||
"latest_from_branch": "Seneste fra branch",
|
||||
"specific_commit": "Specifik commit",
|
||||
"specific_commit_message": "Eksempel: feedface1234",
|
||||
"commit_sha_placeholder": "commit sha",
|
||||
"version_label": "Versionslabel",
|
||||
"version_label_message": "Eksempel: api-2026.05.19",
|
||||
"version_label_placeholder": "versionslabel",
|
||||
"commit_prefix": "Commit: {commit}",
|
||||
"test_access_tooltip": "Bekræft token-adgang til det valgte repository, branch og commit."
|
||||
},
|
||||
"replay": {
|
||||
"title": "Replay",
|
||||
"subtitle": "Aktiver målrettet capture og gennemgå fejltimelines trin for trin.",
|
||||
"capture_channel": "Capture {channel}",
|
||||
"target_type": "Måltype",
|
||||
"target": "Mål",
|
||||
"target_message": "Eksempel: bruger-id 42 eller kanal-slug canary.",
|
||||
"target_placeholder": "Mål-id eller kanal-slug",
|
||||
"channel_scope": "Kanalafgrænsning",
|
||||
"channel_scope_message": "Valgfrit; begrænser replay capture til en kanal.",
|
||||
"no_channel_scope": "Ingen kanalafgrænsning",
|
||||
"capture_level": "Capture-niveau",
|
||||
"expires": "Udløber",
|
||||
"expires_message": "Lad feltet være tomt for manuel deaktivering.",
|
||||
"trace_id": "Trace-ID",
|
||||
"trace_id_message": "Eksempel: trace-canary-1",
|
||||
"trace_id_placeholder": "Trace-id",
|
||||
"channel": "Kanal",
|
||||
"channel_placeholder": "Kanal",
|
||||
"principal_id": "Principal-ID",
|
||||
"principal_id_placeholder": "Principal-id",
|
||||
"module": "Modul",
|
||||
"module_placeholder": "Modul",
|
||||
"severity": "Alvorlighed",
|
||||
"any_severity": "Alle alvorligheder",
|
||||
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,31 @@
|
||||
{
|
||||
"compat": {
|
||||
"error_report": {
|
||||
"button": "Rapporter fejl",
|
||||
"title": "Rapporter fejl",
|
||||
"subtitle": "Send det aktuelle skærmbillede og de seneste fejldetaljer til support.",
|
||||
"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.",
|
||||
"submit": "Send rapport",
|
||||
"submitted": "Fejlrapporten er sendt.",
|
||||
"capture_failed": "Skærmbilledet kunne ikke oprettes. Prøv igen.",
|
||||
"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",
|
||||
"screenshot": "Skærmbillede",
|
||||
"request_errors": "Request-fejl",
|
||||
"vue_errors": "Vue-fejl",
|
||||
"runtime_context": "Runtime-kontekst",
|
||||
"empty": "Ingen fejlrapporter fundet.",
|
||||
"resolve": "Marker som løst",
|
||||
"reopen": "Genåbn",
|
||||
"resolution_note": "Løsningsnote",
|
||||
"button": "Report error",
|
||||
"title": "Report error",
|
||||
"subtitle": "Send the current screen and recent error details to support.",
|
||||
"before_error": "What were you doing before the error occurred?",
|
||||
"expected": "What did you expect would happen?",
|
||||
"actual": "What actually happened?",
|
||||
"consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
|
||||
"submit": "Submit report",
|
||||
"submitted": "Error report submitted.",
|
||||
"capture_failed": "The screen capture failed. Please try again.",
|
||||
"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",
|
||||
"screenshot": "Screenshot",
|
||||
"request_errors": "Request errors",
|
||||
"vue_errors": "Vue errors",
|
||||
"runtime_context": "Runtime context",
|
||||
"empty": "No error reports found.",
|
||||
"resolve": "Mark resolved",
|
||||
"reopen": "Reopen",
|
||||
"resolution_note": "Resolution note",
|
||||
"filters": {
|
||||
"open": "Åben",
|
||||
"resolved": "Løst",
|
||||
"all": "Alle"
|
||||
"open": "Open",
|
||||
"resolved": "Resolved",
|
||||
"all": "All"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,6 @@
|
||||
"products": {
|
||||
"columns": {
|
||||
"economic_product": "@.upper:{'terms.glossary.e'}-@:{'terms.glossary.conomic'} @:{'terms.glossary.produkt'}",
|
||||
"is_wash": "Er vask",
|
||||
"piktogram": "Piktogram",
|
||||
"requires_note": "@.capitalize:{'terms.glossary.kræver'} @:{'terms.glossary.note'}",
|
||||
"show_in_booking_form": "@:{'terms.glossary.vis'} @:{'terms.glossary.i'} @:{'terms.glossary.booking'} @:{'terms.glossary.formular'}",
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
"lift": "Lift",
|
||||
"mandskabsvogn": "Mandskabsvogn",
|
||||
"motorcykel": "Motorcykel",
|
||||
"s??ttevogn": "Semi-@:{'terms.glossary.trailer'}",
|
||||
"sættevogn": "Semitrailer",
|
||||
"trailer": "@:{'phrases.compat.vehicles.types.trailer'}",
|
||||
"traktor": "@:{'phrases.compat.vehicles.types.tractor'}",
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"allow_preseeded_replica": "@.capitalize:{'terms.glossary.replikaen'} @:{'terms.glossary.er'} @:{'terms.glossary.allerede'} @:{'terms.glossary.sikkert'} @:{'terms.glossary.klargjort'}",
|
||||
"buckets": "@.capitalize:{'terms.glossary.buckets'}",
|
||||
"console_port": "Konsolport",
|
||||
"replication_transfer_limit": "Overførselsgrænse",
|
||||
"replication_transfer_limit": "Overfoerselsgraense",
|
||||
"database_index": "Databaseindeks",
|
||||
"port": "@.capitalize:{'terms.glossary.port'}",
|
||||
"replication_password": "Replikeringsadgangskode",
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
"loading_department": "@.capitalize:{'terms.glossary.indlæser'} @:{'terms.glossary.afdeling'}...",
|
||||
"loading_types": "@.capitalize:{'terms.glossary.indlæser'} @:{'terms.glossary.typer'}...",
|
||||
"loading_vehicle_types": "@.capitalize:{'terms.glossary.indlæser'} @:{'terms.glossary.tilgængelige'} @:{'terms.glossary.køretøjstyper'}...",
|
||||
"lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
||||
"machine": "@.capitalize:{'terms.glossary.maskine'}",
|
||||
"machine_unavailable_for_lane": "Maskinvask er ikke tilgængelig for den valgte bane. Vælg manuel vask eller en anden ledig bane.",
|
||||
"no_departments_found": "@:{'phrases.compat.departments.no_departments'}",
|
||||
"occupied": "Optaget",
|
||||
"open_property_access_gate": "@:{'terms.glossary.abn'} adgangsport",
|
||||
@@ -34,10 +32,6 @@
|
||||
"start_wash": "@:{'phrases.compat.guest.start_wash'}",
|
||||
"total_time": "@.capitalize:{'terms.glossary.samlet'} @:{'terms.glossary.tid'}: {minutes} @:{'terms.glossary.minutter'} @:{'terms.glossary.og'} {seconds} @:{'terms.glossary.sekunder'}",
|
||||
"unavailable": "@.capitalize:{'terms.glossary.utilgængelig'}",
|
||||
"vehicle_step_missing_customer_number": "Indtast kundenummer for at fortsætte.",
|
||||
"vehicle_step_missing_department": "Vent på, at nærmeste afdeling er indlæst, før du fortsætter.",
|
||||
"vehicle_step_missing_registration": "Indtast registreringsnummer for at fortsætte.",
|
||||
"vehicle_step_missing_vehicle_type": "Vælg en tilgængelig køretøjstype for at fortsætte.",
|
||||
"wash_completed_contact": "@.capitalize:{'terms.glossary.vi'} håber, @:{'terms.glossary.at'} @:{'terms.glossary.du'} @:{'terms.glossary.er'} tilfreds @:{'terms.glossary.med'} resultatet. @.capitalize:{'terms.glossary.hvis'} @:{'terms.glossary.du'} @:{'terms.glossary.har'} @:{'terms.glossary.brug'} @:{'terms.glossary.for'} @:{'terms.glossary.yderligere'} assistance, @:{'terms.glossary.er'} @:{'terms.glossary.du'} @:{'terms.glossary.velkommen'} @:{'terms.glossary.til'} @:{'terms.glossary.at'} kontakte @:{'terms.glossary.vores'} personale.",
|
||||
"wash_completed_message": "@.capitalize:{'terms.glossary.din'} @:{'terms.glossary.vask'} @:{'terms.glossary.er'} @:{'terms.glossary.nu'} @:{'terms.glossary.færdig'}! @:{'terms.glossary.tak'} @:{'terms.glossary.fordi'} @:{'terms.glossary.du'} brugte @:{'terms.glossary.vores'} selvbetjeningsvask.",
|
||||
"wash_completed": "@.capitalize:{'terms.glossary.vask'} @:{'terms.glossary.færdig'}",
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"api": "API",
|
||||
"app": "app",
|
||||
"appen": "appen",
|
||||
"ar": "@:{'terms.glossary.years'}",
|
||||
"ar": "år",
|
||||
"are": "are",
|
||||
"arsag": "årsag",
|
||||
"at": "at",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"nøgle": "nøgle",
|
||||
"nogle": "nogle",
|
||||
"nok": "nok",
|
||||
"nøkkel": "@:{'terms.glossary.nøgle'}",
|
||||
"nøkkel": "nøgle",
|
||||
"nøkkelen": "nøglen",
|
||||
"not": "not",
|
||||
"note": "note",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user