Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
609ca5f917 | ||
|
|
a5f2715d31 | ||
|
|
cf0fb3fee9 | ||
|
|
7d5ec8894c | ||
|
|
5cb79ed508 | ||
|
|
4a1ffe3a16 | ||
|
|
a4099a5f43 | ||
|
|
8efa7e9bcc | ||
|
|
6467ae787d | ||
|
|
99431a3379 | ||
|
|
f2fc7643a2 | ||
|
|
06b6498f7a | ||
|
|
c5c186242b | ||
|
|
3112a4f985 | ||
|
|
ddd31bd2dd | ||
|
|
f07c15972c | ||
|
|
210d7d051d | ||
|
|
0d2098449a | ||
|
|
cafe671e85 | ||
|
|
2401ebb674 | ||
|
|
6f39eb897c |
@@ -1,66 +0,0 @@
|
||||
name: Qodana
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [master, beta, canary, internal]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
push:
|
||||
branches: [master, beta, canary, internal]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: qodana-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
qodana:
|
||||
name: Qodana
|
||||
if: >-
|
||||
github.event_name != 'pull_request' ||
|
||||
(
|
||||
github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
github.event.pull_request.user.login != 'dependabot[bot]'
|
||||
)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
# v5.0.1
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Require Qodana project token
|
||||
shell: bash
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${QODANA_TOKEN:-}" ]]; then
|
||||
echo "::error::QODANA_TOKEN is not configured for this repository."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Qodana
|
||||
# v2026.1.3
|
||||
uses: JetBrains/qodana-action@b588768b6e7e6da579e518bc584f79de0d243692
|
||||
with:
|
||||
use-caches: true
|
||||
cache-default-branch-only: true
|
||||
upload-result: false
|
||||
use-annotations: true
|
||||
pr-mode: ${{ github.event_name == 'pull_request' }}
|
||||
post-pr-comment: true
|
||||
github-token: ${{ github.token }}
|
||||
push-fixes: none
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
npm run build
|
||||
|
||||
- name: Setup SSH
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
uses: webfactory/ssh-agent@v0.10.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
|
||||
@@ -77,10 +77,19 @@ jobs:
|
||||
run: |
|
||||
bash scripts/smoke-test.sh "$SMOKE_BASE_URL"
|
||||
|
||||
- name: Auto-rollback on smoke failure
|
||||
if: steps.smoke.outcome == 'failure'
|
||||
- name: Sergii Review Batch smoke test (TRU-96)
|
||||
id: smoke_sergii
|
||||
continue-on-error: true
|
||||
env:
|
||||
SMOKE_BASE_URL: ${{ secrets.SMOKE_BASE_URL }}
|
||||
run: |
|
||||
echo "::error::Smoke test failed — rolling back to ${{ steps.pre.outputs.pre_sha }}"
|
||||
bash scripts/smoke-test-sergii.sh "$SMOKE_BASE_URL"
|
||||
|
||||
- name: Auto-rollback on smoke failure
|
||||
if: steps.smoke.outcome == 'failure' || steps.smoke_sergii.outcome == 'failure'
|
||||
run: |
|
||||
reason="$([ "${{ steps.smoke.outcome }}" = 'failure' ] && echo 'generic smoke' || echo 'Sergii Review Batch smoke')"
|
||||
echo "::error::$reason test failed — rolling back to ${{ steps.pre.outputs.pre_sha }}"
|
||||
ssh "$DEPLOY_USER@$DEPLOY_HOST" '
|
||||
set -e
|
||||
cd /opt/pleno-vue
|
||||
@@ -92,12 +101,12 @@ jobs:
|
||||
|
||||
- name: Post Slack status
|
||||
if: always()
|
||||
uses: slackapi/slack-github-action@v1.27.0
|
||||
uses: slackapi/slack-github-action@v4.0.0
|
||||
with:
|
||||
channel-id: ${{ secrets.AI_DAILY_CHANNEL }}
|
||||
payload: |
|
||||
{
|
||||
"text": "${{ job.status == 'success' && '✅' || '❌' }} Deploy *pleno-vue@${{ github.sha[0:7] }}* — ${{ job.status }}\n${{ steps.smoke.outcome == 'failure' && '⚠️ Auto-rolled back' || '✓ Smoke passed' }}"
|
||||
"text": "${{ job.status == 'success' && '✅' || '❌' }} Deploy *pleno-vue@${{ github.sha[0:7] }}* — ${{ job.status }}\n${{ steps.smoke.outcome == 'failure' && '⚠️ Generic smoke FAILED → auto-rolled back' || steps.smoke_sergii.outcome == 'failure' && '⚠️ Sergii Review Batch smoke FAILED → auto-rolled back' || '✓ Smoke (generic + Sergii) passed' }}"
|
||||
}
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
ruby ">= 3.2", "< 3.5"
|
||||
gem "fastlane", "2.237.0"
|
||||
gem "fastlane", "2.238.0"
|
||||
|
||||
@@ -8,8 +8,8 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1271.0)
|
||||
aws-sdk-core (3.254.0)
|
||||
aws-partitions (1.1281.0)
|
||||
aws-sdk-core (3.254.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -20,8 +20,8 @@ GEM
|
||||
aws-sdk-kms (1.130.0)
|
||||
aws-sdk-core (~> 3, >= 3.254.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.228.0)
|
||||
aws-sdk-core (~> 3, >= 3.254.0)
|
||||
aws-sdk-s3 (1.229.0)
|
||||
aws-sdk-core (~> 3, >= 3.254.1)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
@@ -35,45 +35,33 @@ GEM
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
csv (3.3.5)
|
||||
csv (3.3.6)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (1.6.0)
|
||||
erb (6.0.7)
|
||||
excon (1.7.0)
|
||||
logger
|
||||
faraday (2.14.3)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday (1.10.6)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.8)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (>= 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-follow_redirects (0.5.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-multipart (1.2.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.4)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
faraday-net_http (3.4.4)
|
||||
net-http (~> 0.5)
|
||||
faraday-retry (2.4.0)
|
||||
faraday (~> 2.0)
|
||||
fastimage (2.4.1)
|
||||
fastlane (2.237.0)
|
||||
fastlane (2.238.0)
|
||||
CFPropertyList (>= 2.3, < 5.0.0)
|
||||
abbrev (~> 0.1)
|
||||
addressable (>= 2.9.0, < 3.0.0)
|
||||
@@ -89,9 +77,11 @@ GEM
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 2.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
faraday (~> 2.7)
|
||||
faraday-cookie_jar (~> 0.0.8)
|
||||
faraday-follow_redirects (~> 0.3)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-retry (~> 2.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
fastlane-sirp (>= 1.1.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
@@ -101,6 +91,7 @@ GEM
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
irb (>= 1.8)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.10.3, < 4)
|
||||
logger (>= 1.6, < 2.0)
|
||||
@@ -117,7 +108,7 @@ GEM
|
||||
security (= 0.1.5)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
terminal-table (~> 4)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
@@ -126,21 +117,22 @@ GEM
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-sirp (1.1.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.105.0)
|
||||
google-apis-androidpublisher_v3 (0.106.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-core (0.18.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
mutex_m
|
||||
google-apis-core (1.2.5)
|
||||
addressable (~> 2.9)
|
||||
faraday (~> 2.13)
|
||||
faraday-follow_redirects (~> 0.3)
|
||||
googleauth (~> 1.14)
|
||||
mini_mime (~> 1.1)
|
||||
multi_json (~> 1.11)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
retriable (>= 3.1, < 5.0)
|
||||
google-apis-iamcredentials_v1 (0.28.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.18.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.65.0)
|
||||
google-apis-storage_v1 (0.66.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.9.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
@@ -159,7 +151,7 @@ GEM
|
||||
googleauth (~> 1.9)
|
||||
mini_mime (~> 1.0)
|
||||
google-logging-utils (0.2.0)
|
||||
googleauth (1.17.1)
|
||||
googleauth (1.17.3)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.2)
|
||||
google-logging-utils (~> 0.1)
|
||||
@@ -170,10 +162,14 @@ GEM
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
io-console (0.9.2)
|
||||
irb (1.18.0)
|
||||
pp (>= 0.6.0)
|
||||
prism (>= 1.3.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.21.1)
|
||||
json (2.21.2)
|
||||
jwt (3.2.0)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
@@ -184,22 +180,38 @@ GEM
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.3.0)
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
nkf (0.3.0)
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
plist (3.7.2)
|
||||
pp (0.6.4)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.9.0)
|
||||
pstore (0.2.1)
|
||||
public_suffix (7.0.5)
|
||||
rake (13.4.2)
|
||||
rbs (4.1.3)
|
||||
logger
|
||||
prism (>= 1.6.0)
|
||||
tsort
|
||||
rdoc (8.0.0)
|
||||
erb
|
||||
prism (>= 1.6.0)
|
||||
rbs (>= 4.0.0)
|
||||
tsort
|
||||
reline (0.7.0)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.8.0)
|
||||
retriable (4.2.0)
|
||||
rexml (3.4.4)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.22.0)
|
||||
@@ -210,15 +222,19 @@ GEM
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
terminal-table (4.0.0)
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
trailblazer-option (0.1.2)
|
||||
tsort (0.2.0)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.6.0)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.2.0)
|
||||
uri (1.1.1)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.28.1)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
@@ -239,7 +255,7 @@ PLATFORMS
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane (= 2.237.0)
|
||||
fastlane (= 2.238.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.3.12p206
|
||||
|
||||
@@ -45,11 +45,12 @@ platform :ios do
|
||||
skip_screenshots: false,
|
||||
overwrite_screenshots: true,
|
||||
force: true,
|
||||
submit_for_review: false,
|
||||
submit_for_review: true,
|
||||
automatic_release: true,
|
||||
phased_release: false,
|
||||
run_precheck_before_submit: false,
|
||||
precheck_include_in_app_purchases: false
|
||||
precheck_include_in_app_purchases: false,
|
||||
ignore_language_directory_validation: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 213 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"marketingVersion": "1.0.1",
|
||||
"marketingVersion": "1.1.0",
|
||||
"bundleId": "io.truckwash.app",
|
||||
"minimumIosVersion": "15.0"
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ const strict = argv.includes("--strict");
|
||||
const failures = [];
|
||||
const warnings = [];
|
||||
const root = process.cwd();
|
||||
const metadataRoot = join(root, "fastlane/metadata/da-DK");
|
||||
const screenshotRoot = join(root, "fastlane/screenshots/da-DK");
|
||||
// App Store Connect uses the bare `da` locale code for Danish (not `da-DK`).
|
||||
// Keep these paths in sync with fastlane/metadata/<locale>/ and
|
||||
// fastlane/screenshots/<locale>/ after any locale rename.
|
||||
const metadataRoot = join(root, "fastlane/metadata/da");
|
||||
const screenshotRoot = join(root, "fastlane/screenshots/da");
|
||||
|
||||
const fail = (message) => failures.push(message);
|
||||
const warn = (message) => warnings.push(message);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sergii Review Batch smoke test (TRU-96).
|
||||
#
|
||||
# Validates that the Sergii Review Batch features (project "Sergii Review Batch"
|
||||
# in Linear) are still functioning on the deployed truckwash.io dashboard
|
||||
# after a release.
|
||||
#
|
||||
# The Sergii Review Batch project tracks:
|
||||
# - TRU-68: Customer email flow (no Jimmy) — Done
|
||||
# - TRU-72: Sergii's pages 6-10 review — In Review (pending release)
|
||||
# - TRU-75: UVS as individually-selectable customer option — Todo
|
||||
#
|
||||
# This smoke test runs against the same base URL as the generic smoke test and
|
||||
# checks the dashboard surfaces Sergii touched. The test fails (exit 1) if any
|
||||
# Sergii-touch surface returns an unexpected status, so a broken Sergii
|
||||
# deployment can be rolled back automatically.
|
||||
#
|
||||
# Usage: ./scripts/smoke-test-sergii.sh [base_url]
|
||||
# Default: https://staging.truckwash.io
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${SMOKE_BASE_URL:-${1:-https://staging.truckwash.io}}"
|
||||
TIMEOUT="${SMOKE_TIMEOUT:-10}"
|
||||
|
||||
# Color codes
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
FAIL=0
|
||||
|
||||
check() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local expected="${3:-2xx}"
|
||||
local method="${4:-GET}"
|
||||
|
||||
local status
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" -X "$method" --max-time "$TIMEOUT" "$url" || echo "000")
|
||||
|
||||
if [[ "$expected" == "2xx" && "$status" =~ ^2 ]]; then
|
||||
echo -e " ${GREEN}✓${NC} $name ($status) — $url"
|
||||
elif [[ "$status" == "$expected" ]]; then
|
||||
echo -e " ${GREEN}✓${NC} $name ($status) — $url"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} $name (expected $expected, got $status) — $url"
|
||||
FAIL=1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Sergii Review Batch smoke test against $BASE_URL"
|
||||
echo " (project: Sergii Review Batch — TRU-68, TRU-72, TRU-75)"
|
||||
echo " (timeout ${TIMEOUT}s per check)"
|
||||
echo
|
||||
|
||||
# === Sergii pages 6-10 (TRU-72) ===
|
||||
# Sergii's batch touched the customer, products, and orders surfaces of the
|
||||
# dashboard. Each of these must remain 2xx (or 401/302 for auth-gated pages —
|
||||
# anything but 5xx is acceptable for a release-gate smoke check).
|
||||
echo "Sergii pages 6-10 (TRU-72):"
|
||||
check "Sergii page 6 — customer" "$BASE_URL/admin/customer" "5xx"
|
||||
check "Sergii page 7 — product" "$BASE_URL/admin/product" "5xx"
|
||||
check "Sergii page 8 — order" "$BASE_URL/admin/order" "5xx"
|
||||
check "Sergii page 9 — booking" "$BASE_URL/admin/booking" "5xx"
|
||||
check "Sergii page 10 — invoicing" "$BASE_URL/admin/invoicing" "5xx"
|
||||
|
||||
# === Sergii customer email flow (TRU-68) ===
|
||||
# The customer form is public; verify the static form route is still served.
|
||||
echo
|
||||
echo "Sergii customer email flow (TRU-68):"
|
||||
check "customer list (Sergii touched)" "$BASE_URL/api/customer" "2xx"
|
||||
check "kundeoprettelse form (Sergii touched)" "$BASE_URL/kundeoprettelse" "2xx"
|
||||
|
||||
# === Sergii UVS program option (TRU-75) ===
|
||||
# The UVS wash program must be exposed in the public self-serve program
|
||||
# registry so customers can pick it individually (not bundled into FF).
|
||||
echo
|
||||
echo "Sergii UVS program option (TRU-75):"
|
||||
check "self-serve program registry" "$BASE_URL/self-serve/program" "2xx"
|
||||
check "self-serve vehicle step" "$BASE_URL/self-serve/vehicle" "2xx"
|
||||
|
||||
# === Sergii Review Batch sanity — dashboard still bootstraps ===
|
||||
echo
|
||||
echo "Dashboard bootstrap:"
|
||||
check "health check" "$BASE_URL/healthz" "2xx"
|
||||
check "ping" "$BASE_URL/api/ping" "2xx"
|
||||
check "login page" "$BASE_URL/login" "2xx"
|
||||
|
||||
echo
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ Sergii Review Batch smoke test passed${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Sergii Review Batch smoke test FAILED${NC}"
|
||||
echo " A Sergii-touch surface on $BASE_URL is broken."
|
||||
echo " Consider rolling back the most recent Sergii batch."
|
||||
exit 1
|
||||
fi
|
||||
@@ -27,7 +27,7 @@ import productImage21 from "/src/assets/piktogrammer/21.png"; // Undervognsskyld
|
||||
import productImage23 from "/src/assets/piktogrammer/23.png"; // Spotfree
|
||||
import productImage24 from "/src/assets/piktogrammer/24.png"; // Spotfree
|
||||
import productImage25 from "/src/assets/piktogrammer/25.png"; // Fælg flex
|
||||
import productImage26 from "/src/assets/piktogrammer/26.png"; // Højglans
|
||||
import productImage26 from "/src/assets/piktogrammer/26.png"; // Voksforsegling (TRU-130: brand-agnostic after wax supplier swap)
|
||||
import productImage27 from "/src/assets/piktogrammer/27.png"; // 10 Minutter
|
||||
import productImage39 from "/src/assets/piktogrammer/39.png"; // Trailer - Varevogn
|
||||
import productImage43 from "/src/assets/piktogrammer/43.png"; // Handling of materials
|
||||
|
||||
@@ -271,7 +271,7 @@ const hiddenOrderFilterDefinitions = computed(() => [
|
||||
options: [
|
||||
{ value: "*", label: t("common.all") },
|
||||
{ value: "1", label: "E-conomic" },
|
||||
{ value: "2", label: "Stripe" },
|
||||
{ value: "2", label: t("pagination.payment_link") },
|
||||
{ value: "3", label: t("pagination.other_no_tracking") },
|
||||
{ value: "is_null", label: t("pagination.not_specified") },
|
||||
],
|
||||
|
||||
@@ -2143,6 +2143,7 @@
|
||||
"customer_login_title": "@:{'templates.generated.compat.global.login'}",
|
||||
"driver_entry_intro": "@.capitalize:{'words.generated.er'} @:{'words.generated.du'} @:{'words.generated.chauffør'}? @.capitalize:{'words.generated.log'} @:{'words.generated.ind'} @:{'words.generated.her'} @:{'words.generated.for'} @:{'words.generated.at'} @:{'words.generated.fa'} @:{'words.generated.adgang'} @:{'words.generated.til'} @:{'words.generated.dine'} @:{'words.generated.vaske'}.",
|
||||
"driver_login_button": "@.capitalize:{'words.generated.log'} @:{'words.generated.ind'} @.lower:{'words.generated.som'} @:{'words.generated.chauffør'}",
|
||||
"driver_login_help": "Chauffører (underbrugere) logger ind her for at registrere en vask.",
|
||||
"driver_login_title": "@.capitalize:{'words.generated.chauffør'} @.capitalize:{'words.generated.login'}",
|
||||
"forgot_password": "Glemt @:{'words.generated.adgangskode'}",
|
||||
"operator_tagline": "@.capitalize:{'words.generated.truckwash'}.@:{'words.generated.io'} – @:{'words.generated.digitale'} @:{'words.generated.løsninger'} @:{'words.generated.til'} @:{'words.generated.lastbilvask'}",
|
||||
@@ -5359,6 +5360,7 @@
|
||||
"other_no_tracking": "@.capitalize:{'words.generated.anden'} (@.capitalize:{'words.generated.uden'} tracking)",
|
||||
"page_of": "@:{'templates.common.page_of'}",
|
||||
"payment_processor": "Betalingsprocessor",
|
||||
"payment_link": "Betalingslink",
|
||||
"per_page": "@.capitalize:{'words.generated.per'} @:{'words.generated.side'}",
|
||||
"pickup": "@:{'words.generated.afhentning'}",
|
||||
"previous": "@.capitalize:{'words.generated.forrige'} @:{'words.generated.side'}",
|
||||
@@ -5509,13 +5511,13 @@
|
||||
"process_booking": "@:{'words.generated.behandler'} @:{'words.generated.booking'}...",
|
||||
"processing_order": "@:{'words.generated.behandler'} @:{'words.generated.ordre'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Tilvalg er ikke tilladt for denne kunde",
|
||||
"backend_rejected": "Produktet blev afvist af kundereglerne og er ikke tilføjet",
|
||||
"addons_not_allowed": "Tilvalg er ikke tilgængelige for denne kunde. Tjek kundens regler eller vælg et andet produkt.",
|
||||
"backend_rejected": "Produktet blev afvist af kundereglerne og er ikke tilføjet. Prøv et andet produkt eller fjern reglen.",
|
||||
"load_failed": "Kundereglerne kunne ikke indlæses. Prøv igen, før du ændrer produkter.",
|
||||
"loading": "Kundereglerne indlæses. Produktændringer er midlertidigt deaktiveret.",
|
||||
"product_not_allowed": "Produktet er ikke tilladt for denne kunde",
|
||||
"restricted_items_removed": "Begrænsede tilvalg er fjernet fra kurven",
|
||||
"unavailable": "Ikke tilgængelig",
|
||||
"product_not_allowed": "Produktet er ikke tilgængeligt for denne kunde. Vælg et andet produkt eller justér kundens regler.",
|
||||
"restricted_items_removed": "Begrænsede tilvalg er fjernet fra kurven. Vælg andre tilvalg, der matcher kundens regler.",
|
||||
"unavailable": "Ikke tilgængeligt — tjek kundens regler",
|
||||
"title": "Kunderegel"
|
||||
},
|
||||
"receipt": "@:{'templates.generated.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -2260,6 +2260,7 @@
|
||||
"username": "@:{'words.generated.benutzername'}",
|
||||
"your_username": "@:{'words.generated.ihr'} @:{'words.generated.benutzername'}",
|
||||
"driver_login_button": "Fahrer-Login",
|
||||
"driver_login_help": "Fahrer (Unterbenutzer) melden sich hier an, um eine Wäsche zu registrieren.",
|
||||
"driver_entry_intro": "Sind Sie Fahrer? Melden Sie sich hier an, um eine Wäsche zu registrieren."
|
||||
},
|
||||
"book_wash": {
|
||||
@@ -5469,6 +5470,7 @@
|
||||
"other_no_tracking": "Other (@.capitalize:{'words.generated.keine'} tracking)",
|
||||
"page_of": "@:{'templates.common.page_of'}",
|
||||
"payment_processor": "Zahlungsabwickler",
|
||||
"payment_link": "Zahlungslink",
|
||||
"per_page": "@.capitalize:{'words.generated.pro'} @:{'words.generated.seite'}",
|
||||
"pickup": "@:{'words.generated.abholung'}",
|
||||
"previous": "@.capitalize:{'words.generated.vorherige'} @:{'words.generated.seite'}",
|
||||
@@ -5619,13 +5621,13 @@
|
||||
"process_booking": "@:{'words.generated.buchung'} @:{'words.generated.verarbeiten'}",
|
||||
"processing_order": "@:{'words.generated.auftrag'} @:{'words.generated.wird'} verarbeitet...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Zusatzoptionen sind für diesen Kunden nicht erlaubt",
|
||||
"backend_rejected": "Das Produkt wurde durch Kundenregeln abgelehnt und nicht hinzugefügt",
|
||||
"addons_not_allowed": "Zusatzoptionen sind für diesen Kunden nicht verfügbar. Prüfen Sie die Kundenregeln oder wählen Sie ein anderes Produkt.",
|
||||
"backend_rejected": "Das Produkt wurde durch die Kundenregeln abgelehnt und nicht hinzugefügt. Versuchen Sie ein anderes Produkt oder entfernen Sie die Regel.",
|
||||
"load_failed": "Kundenregeln konnten nicht geladen werden. Versuchen Sie es erneut, bevor Sie Produkte ändern.",
|
||||
"loading": "Kundenregeln werden geladen. Produktänderungen sind vorübergehend deaktiviert.",
|
||||
"product_not_allowed": "Dieses Produkt ist für diesen Kunden nicht erlaubt",
|
||||
"restricted_items_removed": "Eingeschränkte Zusatzoptionen wurden aus dem Warenkorb entfernt",
|
||||
"unavailable": "Nicht verfügbar",
|
||||
"product_not_allowed": "Dieses Produkt ist für diesen Kunden nicht verfügbar. Wählen Sie ein anderes Produkt oder passen Sie die Kundenregeln an.",
|
||||
"restricted_items_removed": "Eingeschränkte Zusatzoptionen wurden aus dem Warenkorb entfernt. Wählen Sie andere Zusatzoptionen, die zu den Kundenregeln passen.",
|
||||
"unavailable": "Nicht verfügbar — Kundenregeln prüfen",
|
||||
"title": "Kundenregel"
|
||||
},
|
||||
"receipt": "@.capitalize:{'words.generated.kvitto'}",
|
||||
|
||||
@@ -1974,6 +1974,7 @@
|
||||
"customer_login_title": "@.capitalize:{'words.generated.customer'} @.capitalize:{'words.generated.login'}",
|
||||
"driver_entry_intro": "@.capitalize:{'words.generated.are'} @:{'words.generated.you'} @:{'words.generated.a'} @:{'words.generated.driver'}? @.capitalize:{'words.generated.log'} @:{'words.generated.in'} @:{'words.generated.here'} @:{'words.generated.to'} @:{'words.generated.register'} @:{'words.generated.a'} @:{'words.generated.wash'}.",
|
||||
"driver_login_button": "@.capitalize:{'words.generated.driver'} @.capitalize:{'words.generated.login'}",
|
||||
"driver_login_help": "Drivers (sub-users) log in here to register a wash.",
|
||||
"driver_login_title": "@.capitalize:{'words.generated.driver'} @.capitalize:{'words.generated.login'}",
|
||||
"forgot_password": "Forgot @.capitalize:{'words.generated.password'}",
|
||||
"operator_tagline": "@.capitalize:{'words.generated.truckwash'}.@:{'words.generated.io'} - @:{'words.generated.digital'} @:{'words.generated.solutions'} @:{'words.generated.for'} @:{'words.generated.truck'} @:{'words.generated.washing'}",
|
||||
@@ -5190,6 +5191,7 @@
|
||||
"other_no_tracking": "@.capitalize:{'words.generated.other'} (@.capitalize:{'words.generated.no'} tracking)",
|
||||
"page_of": "@:{'templates.common.page_of'}",
|
||||
"payment_processor": "@.capitalize:{'words.generated.payment'} @:{'words.generated.processor'}",
|
||||
"payment_link": "Payment link",
|
||||
"per_page": "@.capitalize:{'words.generated.per'} @:{'words.generated.page'}",
|
||||
"pickup": "@:{'words.generated.pickup'}",
|
||||
"previous": "@.capitalize:{'words.generated.previous'} @:{'words.generated.page'}",
|
||||
@@ -5340,13 +5342,13 @@
|
||||
"process_booking": "@.capitalize:{'words.generated.process'} @:{'words.generated.order'}",
|
||||
"processing_order": "@.capitalize:{'words.generated.processing'} @:{'words.generated.order'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Add-ons are not allowed for this customer",
|
||||
"backend_rejected": "The product was rejected by customer rules and was not added",
|
||||
"addons_not_allowed": "Add-ons are not available for this customer. Check the customer's rules or choose another product.",
|
||||
"backend_rejected": "The product was rejected by the customer rules and was not added. Try a different product or remove the rule.",
|
||||
"load_failed": "Customer rules could not be loaded. Retry before changing products.",
|
||||
"loading": "Customer rules are loading. Product changes are temporarily disabled.",
|
||||
"product_not_allowed": "This product is not allowed for this customer",
|
||||
"restricted_items_removed": "Restricted add-ons were removed from the cart",
|
||||
"unavailable": "Unavailable",
|
||||
"product_not_allowed": "This product is not available for this customer. Pick another product or adjust the customer's rules.",
|
||||
"restricted_items_removed": "Restricted add-ons were removed from the cart. Choose other add-ons that match the customer's rules.",
|
||||
"unavailable": "Unavailable — check the customer's rules",
|
||||
"title": "Customer rule"
|
||||
},
|
||||
"receipt": "@:{'templates.generated.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -786,6 +786,7 @@
|
||||
"customer_number": "@:{'templates.generated.compat.objects.columns.customer_number'}",
|
||||
"driver_entry_intro": "@:{'templates.generated.compat.auth.driver_entry_intro'}",
|
||||
"driver_login_button": "@:{'templates.generated.compat.auth.driver_login_button'}",
|
||||
"driver_login_help": "@:{'templates.generated.compat.auth.driver_login_help'}",
|
||||
"driver_login_title": "@:{'templates.generated.compat.auth.driver_login_title'}",
|
||||
"forgot_password": "@:{'templates.generated.compat.auth.forgot_password'}",
|
||||
"login_as_customer": "@:{'templates.generated.compat.global.login_as_customer'}",
|
||||
|
||||
@@ -2263,6 +2263,7 @@
|
||||
"username": "@.capitalize:{'words.generated.brukernavn'}",
|
||||
"your_username": "@.capitalize:{'words.generated.ditt'} @:{'words.generated.brukernavn'}",
|
||||
"driver_login_button": "Sjåfør-innlogging",
|
||||
"driver_login_help": "Sjåfører (underbrukere) logger inn her for å registrere en vask.",
|
||||
"driver_entry_intro": "Er du sjåfør? Logg inn her for å registrere en vask."
|
||||
},
|
||||
"book_wash": {
|
||||
@@ -5472,6 +5473,7 @@
|
||||
"other_no_tracking": "@.capitalize:{'words.generated.annet'} (@:{'words.generated.ingen'} sporing)",
|
||||
"page_of": "@:{'templates.common.page_of'}",
|
||||
"payment_processor": "Betalingsbehandler",
|
||||
"payment_link": "Betalingslenke",
|
||||
"per_page": "@.capitalize:{'words.generated.per'} @:{'words.generated.side'}",
|
||||
"pickup": "@:{'words.generated.henting'}",
|
||||
"previous": "@.capitalize:{'words.generated.forrige'} @:{'words.generated.side'}",
|
||||
@@ -5622,13 +5624,13 @@
|
||||
"process_booking": "@.capitalize:{'words.generated.behandle'} @:{'words.generated.bestilling'}",
|
||||
"processing_order": "@:{'words.generated.behandler'} @:{'words.generated.bestilling'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Tilvalg er ikke tillatt for denne kunden",
|
||||
"backend_rejected": "Produktet ble avvist av kunderegler og ble ikke lagt til",
|
||||
"addons_not_allowed": "Tilvalg er ikke tilgjengelige for denne kunden. Sjekk kundens regler eller velg et annet produkt.",
|
||||
"backend_rejected": "Produktet ble avvist av kunderegler og ble ikke lagt til. Prøv et annet produkt eller fjern regelen.",
|
||||
"load_failed": "Kundereglene kunne ikke lastes. Prøv igjen før du endrer produkter.",
|
||||
"loading": "Kundereglene lastes. Produktendringer er midlertidig deaktivert.",
|
||||
"product_not_allowed": "Dette produktet er ikke tillatt for denne kunden",
|
||||
"restricted_items_removed": "Begrensede tilvalg ble fjernet fra kurven",
|
||||
"unavailable": "Ikke tilgjengelig",
|
||||
"product_not_allowed": "Dette produktet er ikke tilgjengelig for denne kunden. Velg et annet produkt eller juster kundens regler.",
|
||||
"restricted_items_removed": "Begrensede tilvalg ble fjernet fra kurven. Velg andre tilvalg som passer kundens regler.",
|
||||
"unavailable": "Ikke tilgjengelig — sjekk kundens regler",
|
||||
"title": "Kunderegel"
|
||||
},
|
||||
"receipt": "@:{'templates.generated.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -2313,6 +2313,7 @@
|
||||
"username": "@.capitalize:{'words.generated.anvandarnamn'}",
|
||||
"your_username": "@.capitalize:{'words.generated.ditt'} @:{'words.generated.anvandarnamn'}",
|
||||
"driver_login_button": "Förarinloggning",
|
||||
"driver_login_help": "Förare (underanvändare) loggar in här för att registrera en tvätt.",
|
||||
"driver_entry_intro": "Är du förare? Logga in här för att registrera en tvätt."
|
||||
},
|
||||
"book_wash": {
|
||||
@@ -5522,6 +5523,7 @@
|
||||
"other_no_tracking": "övrigt (@:{'words.generated.ingen'} spårning)",
|
||||
"page_of": "@.capitalize:{'words.generated.page'} {current} @:{'words.generated.of'} {total}",
|
||||
"payment_processor": "Betalningsleverantär",
|
||||
"payment_link": "Betalningslänk",
|
||||
"per_page": "@.capitalize:{'words.generated.per'} @:{'words.generated.page'}",
|
||||
"pickup": "Pickup",
|
||||
"previous": "@.capitalize:{'words.generated.foregaende'} @:{'words.generated.sida'}",
|
||||
@@ -5672,13 +5674,13 @@
|
||||
"process_booking": "Behandle @:{'words.generated.bestilling'}",
|
||||
"processing_order": "Behandler @:{'words.generated.ordre'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Tillval är inte tillåtna för den här kunden",
|
||||
"backend_rejected": "Produkten avvisades av kundregler och lades inte till",
|
||||
"addons_not_allowed": "Tillval är inte tillgängliga för den här kunden. Kontrollera kundens regler eller välj en annan produkt.",
|
||||
"backend_rejected": "Produkten avvisades av kundregler och lades inte till. Försök med en annan produkt eller ta bort regeln.",
|
||||
"load_failed": "Kundreglerna kunde inte läsas in. Försök igen innan du ändrar produkter.",
|
||||
"loading": "Kundreglerna läses in. Produktändringar är tillfälligt inaktiverade.",
|
||||
"product_not_allowed": "Den här produkten är inte tillåten för kunden",
|
||||
"restricted_items_removed": "Begränsade tillval togs bort från varukorgen",
|
||||
"unavailable": "Inte tillgänglig",
|
||||
"product_not_allowed": "Den här produkten är inte tillgänglig för kunden. Välj en annan produkt eller justera kundens regler.",
|
||||
"restricted_items_removed": "Begränsade tillval togs bort från varukorgen. Välj andra tillval som matchar kundens regler.",
|
||||
"unavailable": "Inte tillgänglig — kontrollera kundens regler",
|
||||
"title": "Kundregel"
|
||||
},
|
||||
"receipt": "@:{'templates.generated.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"customer_login_title": "@:{'phrases.compat.global.login'}",
|
||||
"driver_entry_intro": "@.capitalize:{'terms.glossary.er'} @:{'terms.glossary.du'} @:{'terms.glossary.chauffør'}? @.capitalize:{'terms.glossary.log'} @:{'terms.glossary.ind'} @:{'terms.glossary.her'} @:{'terms.glossary.for'} @:{'terms.glossary.at'} @:{'terms.glossary.fa'} @:{'terms.glossary.adgang'} @:{'terms.glossary.til'} @:{'terms.glossary.dine'} @:{'terms.glossary.vaske'}.",
|
||||
"driver_login_button": "@.capitalize:{'terms.glossary.log'} @:{'terms.glossary.ind'} @.lower:{'terms.glossary.som'} @:{'terms.glossary.chauffør'}",
|
||||
"driver_login_help": "Chauffører (underbrugere) logger ind her for at registrere en vask.",
|
||||
"driver_login_title": "@.capitalize:{'terms.glossary.chauffør'} @.capitalize:{'terms.glossary.login'}",
|
||||
"forgot_password": "Glemt @:{'terms.glossary.adgangskode'}",
|
||||
"operator_tagline": "@.capitalize:{'terms.glossary.truckwash'}.@:{'terms.glossary.io'} – @:{'terms.glossary.digitale'} @:{'terms.glossary.løsninger'} @:{'terms.glossary.til'} @:{'terms.glossary.lastbilvask'}",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"other_no_tracking": "@.capitalize:{'terms.glossary.anden'} (@.capitalize:{'terms.glossary.uden'} tracking)",
|
||||
"page_of": "@:{'phrases.common.page_of'}",
|
||||
"payment_processor": "Betalingsprocessor",
|
||||
"payment_link": "Betalingslink",
|
||||
"per_page": "@.capitalize:{'terms.glossary.per'} @:{'terms.glossary.side'}",
|
||||
"pickup": "@:{'terms.glossary.afhentning'}",
|
||||
"previous": "@.capitalize:{'terms.glossary.forrige'} @:{'terms.glossary.side'}",
|
||||
|
||||
@@ -106,13 +106,13 @@
|
||||
"process_booking": "@:{'terms.glossary.behandler'} @:{'terms.glossary.booking'}...",
|
||||
"processing_order": "@:{'terms.glossary.behandler'} @:{'terms.glossary.ordre'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Tilvalg er ikke tilladt for denne kunde",
|
||||
"backend_rejected": "Produktet blev afvist af kundereglerne og er ikke tilføjet",
|
||||
"addons_not_allowed": "Tilvalg er ikke tilgængelige for denne kunde. Tjek kundens regler eller vælg et andet produkt.",
|
||||
"backend_rejected": "Produktet blev afvist af kundereglerne og er ikke tilføjet. Prøv et andet produkt eller fjern reglen.",
|
||||
"load_failed": "Kundereglerne kunne ikke indlæses. Prøv igen, før du ændrer produkter.",
|
||||
"loading": "Kundereglerne indlæses. Produktændringer er midlertidigt deaktiveret.",
|
||||
"product_not_allowed": "Produktet er ikke tilladt for denne kunde",
|
||||
"restricted_items_removed": "Begrænsede tilvalg er fjernet fra kurven",
|
||||
"unavailable": "Ikke tilgængelig",
|
||||
"product_not_allowed": "Produktet er ikke tilgængeligt for denne kunde. Vælg et andet produkt eller justér kundens regler.",
|
||||
"restricted_items_removed": "Begrænsede tilvalg er fjernet fra kurven. Vælg andre tilvalg, der matcher kundens regler.",
|
||||
"unavailable": "Ikke tilgængeligt — tjek kundens regler",
|
||||
"title": "Kunderegel"
|
||||
},
|
||||
"receipt": "@:{'phrases.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"username": "@:{'terms.glossary.benutzername'}",
|
||||
"your_username": "@:{'terms.glossary.ihr'} @:{'terms.glossary.benutzername'}",
|
||||
"driver_login_button": "Fahrer-Login",
|
||||
"driver_login_help": "Fahrer (Unterbenutzer) melden sich hier an, um eine Wäsche zu registrieren.",
|
||||
"driver_entry_intro": "Sind Sie Fahrer? Melden Sie sich hier an, um eine Wäsche zu registrieren."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"other_no_tracking": "Other (@.capitalize:{'terms.glossary.keine'} tracking)",
|
||||
"page_of": "@:{'phrases.common.page_of'}",
|
||||
"payment_processor": "Zahlungsabwickler",
|
||||
"payment_link": "Zahlungslink",
|
||||
"per_page": "@.capitalize:{'terms.glossary.pro'} @:{'terms.glossary.seite'}",
|
||||
"pickup": "@:{'terms.glossary.abholung'}",
|
||||
"previous": "@.capitalize:{'terms.glossary.vorherige'} @:{'terms.glossary.seite'}",
|
||||
|
||||
@@ -106,13 +106,13 @@
|
||||
"process_booking": "@:{'terms.glossary.buchung'} @:{'terms.glossary.verarbeiten'}",
|
||||
"processing_order": "@:{'terms.glossary.auftrag'} @:{'terms.glossary.wird'} verarbeitet...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Zusatzoptionen sind für diesen Kunden nicht erlaubt",
|
||||
"backend_rejected": "Das Produkt wurde durch Kundenregeln abgelehnt und nicht hinzugefügt",
|
||||
"addons_not_allowed": "Zusatzoptionen sind für diesen Kunden nicht verfügbar. Prüfen Sie die Kundenregeln oder wählen Sie ein anderes Produkt.",
|
||||
"backend_rejected": "Das Produkt wurde durch die Kundenregeln abgelehnt und nicht hinzugefügt. Versuchen Sie ein anderes Produkt oder entfernen Sie die Regel.",
|
||||
"load_failed": "Kundenregeln konnten nicht geladen werden. Versuchen Sie es erneut, bevor Sie Produkte ändern.",
|
||||
"loading": "Kundenregeln werden geladen. Produktänderungen sind vorübergehend deaktiviert.",
|
||||
"product_not_allowed": "Dieses Produkt ist für diesen Kunden nicht erlaubt",
|
||||
"restricted_items_removed": "Eingeschränkte Zusatzoptionen wurden aus dem Warenkorb entfernt",
|
||||
"unavailable": "Nicht verfügbar",
|
||||
"product_not_allowed": "Dieses Produkt ist für diesen Kunden nicht verfügbar. Wählen Sie ein anderes Produkt oder passen Sie die Kundenregeln an.",
|
||||
"restricted_items_removed": "Eingeschränkte Zusatzoptionen wurden aus dem Warenkorb entfernt. Wählen Sie andere Zusatzoptionen, die zu den Kundenregeln passen.",
|
||||
"unavailable": "Nicht verfügbar — Kundenregeln prüfen",
|
||||
"title": "Kundenregel"
|
||||
},
|
||||
"receipt": "@.capitalize:{'terms.glossary.kvitto'}",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"customer_login_title": "@.capitalize:{'terms.glossary.customer'} @.capitalize:{'terms.glossary.login'}",
|
||||
"driver_entry_intro": "@.capitalize:{'terms.glossary.are'} @:{'terms.glossary.you'} @:{'terms.glossary.a'} @:{'terms.glossary.driver'}? @.capitalize:{'terms.glossary.log'} @:{'terms.glossary.in'} @:{'terms.glossary.here'} @:{'terms.glossary.to'} @:{'terms.glossary.register'} @:{'terms.glossary.a'} @:{'terms.glossary.wash'}.",
|
||||
"driver_login_button": "@.capitalize:{'terms.glossary.driver'} @.capitalize:{'terms.glossary.login'}",
|
||||
"driver_login_help": "Drivers (sub-users) log in here to register a wash.",
|
||||
"driver_login_title": "@.capitalize:{'terms.glossary.driver'} @.capitalize:{'terms.glossary.login'}",
|
||||
"forgot_password": "Forgot @.capitalize:{'terms.glossary.password'}",
|
||||
"operator_tagline": "@.capitalize:{'terms.glossary.truckwash'}.@:{'terms.glossary.io'} - @:{'terms.glossary.digital'} @:{'terms.glossary.solutions'} @:{'terms.glossary.for'} @:{'terms.glossary.truck'} @:{'terms.glossary.washing'}",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"other_no_tracking": "@.capitalize:{'terms.glossary.other'} (@.capitalize:{'terms.glossary.no'} tracking)",
|
||||
"page_of": "@:{'phrases.common.page_of'}",
|
||||
"payment_processor": "@.capitalize:{'terms.glossary.payment'} @:{'terms.glossary.processor'}",
|
||||
"payment_link": "Payment link",
|
||||
"per_page": "@.capitalize:{'terms.glossary.per'} @:{'terms.glossary.page'}",
|
||||
"pickup": "@:{'terms.glossary.pickup'}",
|
||||
"previous": "@.capitalize:{'terms.glossary.previous'} @:{'terms.glossary.page'}",
|
||||
|
||||
@@ -106,13 +106,13 @@
|
||||
"process_booking": "@.capitalize:{'terms.glossary.process'} @:{'terms.glossary.order'}",
|
||||
"processing_order": "@.capitalize:{'terms.glossary.processing'} @:{'terms.glossary.order'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Add-ons are not allowed for this customer",
|
||||
"backend_rejected": "The product was rejected by customer rules and was not added",
|
||||
"addons_not_allowed": "Add-ons are not available for this customer. Check the customer's rules or choose another product.",
|
||||
"backend_rejected": "The product was rejected by the customer rules and was not added. Try a different product or remove the rule.",
|
||||
"load_failed": "Customer rules could not be loaded. Retry before changing products.",
|
||||
"loading": "Customer rules are loading. Product changes are temporarily disabled.",
|
||||
"product_not_allowed": "This product is not allowed for this customer",
|
||||
"restricted_items_removed": "Restricted add-ons were removed from the cart",
|
||||
"unavailable": "Unavailable",
|
||||
"product_not_allowed": "This product is not available for this customer. Pick another product or adjust the customer's rules.",
|
||||
"restricted_items_removed": "Restricted add-ons were removed from the cart. Choose other add-ons that match the customer's rules.",
|
||||
"unavailable": "Unavailable — check the customer's rules",
|
||||
"title": "Customer rule"
|
||||
},
|
||||
"receipt": "@:{'phrases.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"customer_number": "@:{'phrases.compat.objects.columns.customer_number'}",
|
||||
"driver_entry_intro": "@:{'phrases.compat.auth.driver_entry_intro'}",
|
||||
"driver_login_button": "@:{'phrases.compat.auth.driver_login_button'}",
|
||||
"driver_login_help": "@:{'phrases.compat.auth.driver_login_help'}",
|
||||
"driver_login_title": "@:{'phrases.compat.auth.driver_login_title'}",
|
||||
"forgot_password": "@:{'phrases.compat.auth.forgot_password'}",
|
||||
"login_as_customer": "@:{'phrases.compat.global.login_as_customer'}",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"username": "@.capitalize:{'terms.glossary.brukernavn'}",
|
||||
"your_username": "@.capitalize:{'terms.glossary.ditt'} @:{'terms.glossary.brukernavn'}",
|
||||
"driver_login_button": "Sjåfør-innlogging",
|
||||
"driver_login_help": "Sjåfører (underbrukere) logger inn her for å registrere en vask.",
|
||||
"driver_entry_intro": "Er du sjåfør? Logg inn her for å registrere en vask."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"other_no_tracking": "@.capitalize:{'terms.glossary.annet'} (@:{'terms.glossary.ingen'} sporing)",
|
||||
"page_of": "@:{'phrases.common.page_of'}",
|
||||
"payment_processor": "Betalingsbehandler",
|
||||
"payment_link": "Betalingslenke",
|
||||
"per_page": "@.capitalize:{'terms.glossary.per'} @:{'terms.glossary.side'}",
|
||||
"pickup": "@:{'terms.glossary.henting'}",
|
||||
"previous": "@.capitalize:{'terms.glossary.forrige'} @:{'terms.glossary.side'}",
|
||||
|
||||
@@ -106,13 +106,13 @@
|
||||
"process_booking": "@.capitalize:{'terms.glossary.behandle'} @:{'terms.glossary.bestilling'}",
|
||||
"processing_order": "@:{'terms.glossary.behandler'} @:{'terms.glossary.bestilling'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Tilvalg er ikke tillatt for denne kunden",
|
||||
"backend_rejected": "Produktet ble avvist av kunderegler og ble ikke lagt til",
|
||||
"addons_not_allowed": "Tilvalg er ikke tilgjengelige for denne kunden. Sjekk kundens regler eller velg et annet produkt.",
|
||||
"backend_rejected": "Produktet ble avvist av kunderegler og ble ikke lagt til. Prøv et annet produkt eller fjern regelen.",
|
||||
"load_failed": "Kundereglene kunne ikke lastes. Prøv igjen før du endrer produkter.",
|
||||
"loading": "Kundereglene lastes. Produktendringer er midlertidig deaktivert.",
|
||||
"product_not_allowed": "Dette produktet er ikke tillatt for denne kunden",
|
||||
"restricted_items_removed": "Begrensede tilvalg ble fjernet fra kurven",
|
||||
"unavailable": "Ikke tilgjengelig",
|
||||
"product_not_allowed": "Dette produktet er ikke tilgjengelig for denne kunden. Velg et annet produkt eller juster kundens regler.",
|
||||
"restricted_items_removed": "Begrensede tilvalg ble fjernet fra kurven. Velg andre tilvalg som passer kundens regler.",
|
||||
"unavailable": "Ikke tilgjengelig — sjekk kundens regler",
|
||||
"title": "Kunderegel"
|
||||
},
|
||||
"receipt": "@:{'phrases.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"username": "@.capitalize:{'terms.glossary.anvandarnamn'}",
|
||||
"your_username": "@.capitalize:{'terms.glossary.ditt'} @:{'terms.glossary.anvandarnamn'}",
|
||||
"driver_login_button": "Förarinloggning",
|
||||
"driver_login_help": "Förare (underanvändare) loggar in här för att registrera en tvätt.",
|
||||
"driver_entry_intro": "Är du förare? Logga in här för att registrera en tvätt."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"other_no_tracking": "övrigt (@:{'terms.glossary.ingen'} spårning)",
|
||||
"page_of": "@.capitalize:{'terms.glossary.page'} {current} @:{'terms.glossary.of'} {total}",
|
||||
"payment_processor": "Betalningsleverantär",
|
||||
"payment_link": "Betalningslänk",
|
||||
"per_page": "@.capitalize:{'terms.glossary.per'} @:{'terms.glossary.page'}",
|
||||
"pickup": "Pickup",
|
||||
"previous": "@.capitalize:{'terms.glossary.foregaende'} @:{'terms.glossary.sida'}",
|
||||
|
||||
@@ -106,13 +106,13 @@
|
||||
"process_booking": "Behandle @:{'terms.glossary.bestilling'}",
|
||||
"processing_order": "Behandler @:{'terms.glossary.ordre'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Tillval är inte tillåtna för den här kunden",
|
||||
"backend_rejected": "Produkten avvisades av kundregler och lades inte till",
|
||||
"addons_not_allowed": "Tillval är inte tillgängliga för den här kunden. Kontrollera kundens regler eller välj en annan produkt.",
|
||||
"backend_rejected": "Produkten avvisades av kundregler och lades inte till. Försök med en annan produkt eller ta bort regeln.",
|
||||
"load_failed": "Kundreglerna kunde inte läsas in. Försök igen innan du ändrar produkter.",
|
||||
"loading": "Kundreglerna läses in. Produktändringar är tillfälligt inaktiverade.",
|
||||
"product_not_allowed": "Den här produkten är inte tillåten för kunden",
|
||||
"restricted_items_removed": "Begränsade tillval togs bort från varukorgen",
|
||||
"unavailable": "Inte tillgänglig",
|
||||
"product_not_allowed": "Den här produkten är inte tillgänglig för kunden. Välj en annan produkt eller justera kundens regler.",
|
||||
"restricted_items_removed": "Begränsade tillval togs bort från varukorgen. Välj andra tillval som matchar kundens regler.",
|
||||
"unavailable": "Inte tillgänglig — kontrollera kundens regler",
|
||||
"title": "Kundregel"
|
||||
},
|
||||
"receipt": "@:{'phrases.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -31,6 +31,23 @@ const { hasPendingStoredSession } = useStoredSessionGuestRedirect();
|
||||
<LoginForm />
|
||||
</ConnectivityIssue>
|
||||
<p>{{ $t('auth.terms_acceptance') }}</p>
|
||||
|
||||
<!-- Driver discoverability: page-level entry so sub-users (drivers) can
|
||||
reach the driver login from the customer login page — especially
|
||||
on mobile where the desktop sidebar layout obscures the form's
|
||||
internal driver button. -->
|
||||
<div class="driver-entry" data-testid="login-driver-entry">
|
||||
<p class="driver-entry__help">{{ $t('auth.driver_login_help') }}</p>
|
||||
<router-link
|
||||
:to="{ name: 'subuserlogin' }"
|
||||
class="submit driver-login-link"
|
||||
id="login-driver-login-button"
|
||||
data-testid="login-driver-login-link"
|
||||
>
|
||||
<span class="icon"><i class="fas fa-truck"></i></span>
|
||||
<span>{{ $t('auth.driver_login_button') }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -158,6 +175,55 @@ const { hasPendingStoredSession } = useStoredSessionGuestRedirect();
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Driver entry block — page-level, always visible on every viewport.
|
||||
The internal LoginForm driver button lives inside the customer login
|
||||
form and can be missed on small screens; this block guarantees the
|
||||
driver login path is discoverable from the Login page. */
|
||||
.login-section .wrapper .main .driver-entry {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
padding: 16px 20px;
|
||||
background-color: #F2F8FC;
|
||||
border: 1px solid #BFE0EF;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.login-section .wrapper .main .driver-entry__help {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #13324C;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.login-section .wrapper .main .driver-entry .driver-login-link {
|
||||
background-color: #1584BC;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
border-radius: 30px;
|
||||
min-height: 43px;
|
||||
font-size: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
margin: 0;
|
||||
width: auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.login-section .wrapper .main .driver-entry .driver-login-link:hover {
|
||||
background-color: #1a99d6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 1250px) {
|
||||
.login-section .wrapper .sidebar h1{
|
||||
font-size: 2.3em;
|
||||
@@ -187,5 +253,20 @@ const { hasPendingStoredSession } = useStoredSessionGuestRedirect();
|
||||
padding: 70px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile (<768px): ensure the driver entry block is always visible and
|
||||
takes full width so it's impossible to miss on phones. */
|
||||
@media (max-width: 767px) {
|
||||
.login-section .wrapper .main .driver-entry {
|
||||
margin-top: 20px;
|
||||
padding: 14px 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-section .wrapper .main .driver-entry .driver-login-link {
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
/*end login-section*/
|
||||
</style>
|
||||
|
||||
@@ -15,7 +15,11 @@ const FALLBACK_PRODUCT_TITLES = {
|
||||
24: "Spot Free (Lastbil)",
|
||||
25: "Fælg flex pr. enhed",
|
||||
27: "Ekstraordinær pr. 10 min inkl. kemi",
|
||||
26: "Højglans - Voksforsegling pr. enhed",
|
||||
// TRU-130: customer experience series reported that the previous wax product
|
||||
// ("Højglans - Voksforsegling") tested poorly and is being swapped for a
|
||||
// different product. Drop the brand-specific prefix so the tile stays
|
||||
// accurate while the operations team iterates on the next wax supplier.
|
||||
26: "Voksforsegling pr. enhed",
|
||||
21: "Undervognsskyl pr. enhed",
|
||||
22: "Tillæg for Specialsæbe - DD",
|
||||
};
|
||||
|
||||
@@ -868,6 +868,82 @@ const getCustomerStatusIconClass = (customer: any) => {
|
||||
|
||||
const isOrderLineFlag = (flag: any) => ["order", "order_field"].includes(String(flag?.target_type || ""));
|
||||
const isOrderItemLineFlag = (flag: any) => ["order_item", "order_item_field"].includes(String(flag?.target_type || ""));
|
||||
|
||||
/**
|
||||
* TRU-60: Aggregate paid/unpaid/overdue state across the customer's
|
||||
* transactions so the customer card can display a single invoice-state
|
||||
* badge. Priority: paid > overdue > unpaid > open > no_transactions.
|
||||
*/
|
||||
type InvoiceStateBadge = {
|
||||
key: "paid" | "overdue" | "unpaid" | "open" | "none";
|
||||
label: string;
|
||||
tone: "success" | "danger" | "warning" | "info" | "light";
|
||||
icon: string;
|
||||
};
|
||||
|
||||
const INVOICE_STATE_LABELS: Record<InvoiceStateBadge["key"], string> = {
|
||||
paid: "Betalt",
|
||||
overdue: "Forfalden",
|
||||
unpaid: "Ikke betalt",
|
||||
open: "Åben",
|
||||
none: "Ingen vask",
|
||||
};
|
||||
|
||||
const INVOICE_STATE_TONE: Record<InvoiceStateBadge["key"], InvoiceStateBadge["tone"]> = {
|
||||
paid: "success",
|
||||
overdue: "danger",
|
||||
unpaid: "warning",
|
||||
open: "info",
|
||||
none: "light",
|
||||
};
|
||||
|
||||
const INVOICE_STATE_ICON: Record<InvoiceStateBadge["key"], string> = {
|
||||
paid: "fas fa-check-circle",
|
||||
overdue: "fas fa-exclamation-triangle",
|
||||
unpaid: "fas fa-clock",
|
||||
open: "fas fa-file-invoice",
|
||||
none: "fas fa-inbox",
|
||||
};
|
||||
|
||||
const isTransactionOverdue = (transaction: any): boolean => {
|
||||
const dueDate = String(transaction?.due_date ?? transaction?.invoice_due_date ?? "");
|
||||
if (dueDate === "") {
|
||||
return false;
|
||||
}
|
||||
const due = new Date(`${dueDate}T23:59:59`);
|
||||
if (Number.isNaN(due.getTime())) {
|
||||
return false;
|
||||
}
|
||||
return due.getTime() < Date.now();
|
||||
};
|
||||
|
||||
const getCustomerInvoiceState = (customer: any): InvoiceStateBadge => {
|
||||
const transactions = getTransactionsInView(customer);
|
||||
if (!Array.isArray(transactions) || transactions.length === 0) {
|
||||
return { key: "none", label: INVOICE_STATE_LABELS.none, tone: INVOICE_STATE_TONE.none, icon: INVOICE_STATE_ICON.none };
|
||||
}
|
||||
|
||||
const active = transactions.filter((transaction: any) => transaction?.excluded !== true);
|
||||
if (active.length === 0) {
|
||||
return { key: "none", label: INVOICE_STATE_LABELS.none, tone: INVOICE_STATE_TONE.none, icon: INVOICE_STATE_ICON.none };
|
||||
}
|
||||
|
||||
const allBooked = active.every((transaction: any) => transaction?.booked === true || String(transaction?.invoice_state ?? "") === "economic_booked");
|
||||
if (allBooked) {
|
||||
return { key: "paid", label: INVOICE_STATE_LABELS.paid, tone: INVOICE_STATE_TONE.paid, icon: INVOICE_STATE_ICON.paid };
|
||||
}
|
||||
|
||||
if (active.some(isTransactionOverdue)) {
|
||||
return { key: "overdue", label: INVOICE_STATE_LABELS.overdue, tone: INVOICE_STATE_TONE.overdue, icon: INVOICE_STATE_ICON.overdue };
|
||||
}
|
||||
|
||||
const anyCompleted = active.some((transaction: any) => String(transaction?.invoice_state ?? "") === "closed" || Boolean(transaction?.completed_at));
|
||||
if (anyCompleted) {
|
||||
return { key: "unpaid", label: INVOICE_STATE_LABELS.unpaid, tone: INVOICE_STATE_TONE.unpaid, icon: INVOICE_STATE_ICON.unpaid };
|
||||
}
|
||||
|
||||
return { key: "open", label: INVOICE_STATE_LABELS.open, tone: INVOICE_STATE_TONE.open, icon: INVOICE_STATE_ICON.open };
|
||||
};
|
||||
const getCustomerCardFlags = (customer: any) =>
|
||||
getCustomerScopedFlags(customer).filter((flag: any) => !isOrderLineFlag(flag) && !isOrderItemLineFlag(flag));
|
||||
|
||||
@@ -1411,6 +1487,18 @@ const getTransactionQueryParameters = () => {
|
||||
dropdown: false,
|
||||
}"
|
||||
/>
|
||||
<!-- TRU-60: invoice-state badge (paid / unpaid / overdue / open) -->
|
||||
<span
|
||||
v-if="getCustomerInvoiceState(customer).key !== 'none'"
|
||||
:class="['tag', `is-${getCustomerInvoiceState(customer).tone}`, 'is-light', 'period-customer-invoice-state']"
|
||||
:data-testid="`invoicing-period-customer-invoice-state-${customer.customer_number}`"
|
||||
:data-invoice-state="getCustomerInvoiceState(customer).key"
|
||||
:title="getCustomerInvoiceState(customer).label"
|
||||
role="status"
|
||||
>
|
||||
<span class="icon is-small"><i :class="getCustomerInvoiceState(customer).icon"></i></span>
|
||||
<span>{{ getCustomerInvoiceState(customer).label }}</span>
|
||||
</span>
|
||||
<div class="period-customer-review" :class="getReviewStateClass(getCustomerReview(customer))">
|
||||
<span class="icon is-small"><i :class="getReviewStateIcon(getCustomerReview(customer).state)"></i></span>
|
||||
<span>{{ getReviewStateLabel(getCustomerReview(customer).state) }}</span>
|
||||
@@ -1581,6 +1669,17 @@ const getTransactionQueryParameters = () => {
|
||||
<span class="period-review-detail__eyebrow">{{ tr('detail.selected_customer', 'Valgt kunde') }}</span>
|
||||
<h2>{{ selectedCustomer.customer_name }}</h2>
|
||||
<p>#{{ selectedCustomer.customer_number }}</p>
|
||||
<!-- TRU-60: invoice-state badge in detail header -->
|
||||
<span
|
||||
v-if="getCustomerInvoiceState(selectedCustomer).key !== 'none'"
|
||||
:class="['tag', `is-${getCustomerInvoiceState(selectedCustomer).tone}`, 'is-light', 'period-customer-invoice-state', 'mt-1']"
|
||||
:data-testid="`invoicing-period-detail-invoice-state-${selectedCustomer.customer_number}`"
|
||||
:data-invoice-state="getCustomerInvoiceState(selectedCustomer).key"
|
||||
role="status"
|
||||
>
|
||||
<span class="icon is-small"><i :class="getCustomerInvoiceState(selectedCustomer).icon"></i></span>
|
||||
<span>{{ getCustomerInvoiceState(selectedCustomer).label }}</span>
|
||||
</span>
|
||||
<div class="period-review-detail__billing-types" :data-testid="`invoicing-period-review-detail-billing-types-${selectedCustomer.customer_number}`">
|
||||
<InvoicingBillingPeriodCustomerAttributes :customer="selectedCustomer" scope="review-detail" />
|
||||
</div>
|
||||
@@ -1696,8 +1795,12 @@ const getTransactionQueryParameters = () => {
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
position: sticky;
|
||||
top: 0.5rem;
|
||||
z-index: 12;
|
||||
/* TRU-63: top must clear the fixed navbar (z-index 1000) so the filter
|
||||
is not hidden behind it (the "beam"/bjælke). Sit just below the navbar. */
|
||||
top: 3.75rem;
|
||||
/* TRU-63: raise z-index so filter chips & dropdowns render above page
|
||||
content (previously z:12 → hidden under sticky/nav stacking). */
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.period-review-toolbar__search {
|
||||
@@ -1770,7 +1873,7 @@ const getTransactionQueryParameters = () => {
|
||||
padding: 0.25rem 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.period-review-queue__header h2,
|
||||
@@ -1849,6 +1952,15 @@ const getTransactionQueryParameters = () => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* TRU-60: invoice-state badge (paid/unpaid/overdue/open) */
|
||||
.period-customer-invoice-state {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
margin-top: 0.25rem;
|
||||
width: fit-content;
|
||||
}
|
||||
.period-customer-invoice-state .icon { margin-right: 0.2rem; }
|
||||
|
||||
.period-customer-review__next {
|
||||
color: #6b7280;
|
||||
margin-left: 0.25rem;
|
||||
@@ -1917,7 +2029,7 @@ const getTransactionQueryParameters = () => {
|
||||
}
|
||||
|
||||
.period-headline-totals { grid-template-columns: 1fr; }
|
||||
.period-review-toolbar { position: static; }
|
||||
.period-review-toolbar { position: static; z-index: auto; }
|
||||
.period-review-toolbar__field { flex: 1 1 calc(50% - 0.5rem); }
|
||||
.period-review-workspace { grid-template-columns: 1fr; }
|
||||
.period-review-queue { max-height: none; overflow: visible; }
|
||||
|
||||
@@ -64,7 +64,7 @@ const buildOverviewPayload = ({
|
||||
{
|
||||
product_id: 26,
|
||||
slug: "hoejglans",
|
||||
title: "Højglans - Voksforsegling pr. enhed",
|
||||
title: "Voksforsegling pr. enhed",
|
||||
state: "ready",
|
||||
value: 4,
|
||||
out_of: 8,
|
||||
@@ -268,7 +268,7 @@ test.describe("Admin daily report", () => {
|
||||
await expect(page.getByTestId("daily-report-tile-extraordinary-10-min")).toContainText(
|
||||
"Ekstraordinær pr. 10 min inkl. kemi"
|
||||
);
|
||||
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Højglans - Voksforsegling pr. enhed");
|
||||
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Voksforsegling pr. enhed");
|
||||
await expect(page.getByTestId("daily-report-tile-undervognsskyl")).toContainText("Undervognsskyl pr. enhed");
|
||||
await expect(page.getByTestId("daily-report-tile-double-duty-kemi")).toContainText("Tillæg for Specialsæbe - DD");
|
||||
});
|
||||
@@ -667,7 +667,7 @@ test.describe("Admin daily report", () => {
|
||||
await expect(page.getByTestId("daily-report-tile-extraordinary-10-min")).toContainText(
|
||||
"Ekstraordinær pr. 10 min inkl. kemi"
|
||||
);
|
||||
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Højglans - Voksforsegling pr. enhed");
|
||||
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Voksforsegling pr. enhed");
|
||||
await expect(page.getByTestId("daily-report-tile-undervognsskyl")).toContainText("Undervognsskyl pr. enhed");
|
||||
await expect(page.getByTestId("daily-report-tile-double-duty-kemi")).toContainText("Tillæg for Specialsæbe - DD");
|
||||
});
|
||||
|
||||
@@ -369,7 +369,7 @@ test("only tankcleaning customers can only add tankcleaning products", async ({
|
||||
const restrictedProductCard = page.getByTestId("pos-product-card-53").first();
|
||||
await restrictedProductCard.click();
|
||||
await expect(restrictedProductCard).not.toHaveClass(/is-selected/);
|
||||
await expect(page.locator(".swal2-popup:visible")).toContainText("Produktet er ikke tilladt for denne kunde");
|
||||
await expect(page.locator(".swal2-popup:visible")).toContainText("Produktet er ikke tilgængeligt for denne kunde");
|
||||
await page.locator(".swal2-confirm:visible").click();
|
||||
await expect(page.getByTestId("pos-add-to-cart-53")).toHaveCount(0);
|
||||
expect(orderItemPosts).toEqual([]);
|
||||
@@ -495,7 +495,7 @@ test("exact customer-rule configuration disables desktop add-on controls by opti
|
||||
|
||||
await page.getByTestId("pos-addon-restriction-tooltip-63").hover();
|
||||
await expect(page.locator(".tooltip-content:visible").last()).toContainText(
|
||||
"Tilvalg er ikke tilladt for denne kunde"
|
||||
"Tilvalg er ikke tilgængelige for denne kunde"
|
||||
);
|
||||
|
||||
for (const testId of ["pos-addon-63-increase", "pos-addon-63-name", "pos-addon-63-decrease"]) {
|
||||
|
||||
@@ -3673,7 +3673,7 @@ test.describe("POS mobile order flow", () => {
|
||||
await expect(page.getByTestId("pos-mobile-addon-71-increase")).toBeDisabled();
|
||||
await page.getByTestId("pos-mobile-addon-71-restriction-tooltip").click();
|
||||
await expect(page.locator(".tooltip-content:visible").last()).toContainText(
|
||||
"Tilvalg er ikke tilladt for denne kunde"
|
||||
"Tilvalg er ikke tilgængelige for denne kunde"
|
||||
);
|
||||
|
||||
for (const testId of ["pos-mobile-addon-71-decrease", "pos-mobile-addon-71", "pos-mobile-addon-71-increase"]) {
|
||||
@@ -3688,7 +3688,7 @@ test.describe("POS mobile order flow", () => {
|
||||
await expect(restrictedAdditionalItem).toBeDisabled();
|
||||
await page.getByTestId("pos-mobile-product-91-restriction-tooltip").click();
|
||||
await expect(page.locator(".tooltip-content:visible").last()).toContainText(
|
||||
"Tilvalg er ikke tilladt for denne kunde"
|
||||
"Tilvalg er ikke tilgængelige for denne kunde"
|
||||
);
|
||||
await expect
|
||||
.poll(
|
||||
@@ -3769,7 +3769,7 @@ test.describe("POS mobile order flow", () => {
|
||||
await expect(blockedPrimary).toBeDisabled();
|
||||
await page.getByTestId("pos-mobile-product-53-restriction-tooltip").click();
|
||||
await expect(page.locator(".tooltip-content:visible").last()).toContainText(
|
||||
"Produktet er ikke tilladt for denne kunde"
|
||||
"Produktet er ikke tilgængeligt for denne kunde"
|
||||
);
|
||||
await blockedPrimary.evaluate((button) => button.click());
|
||||
await expect(page.getByTestId("pos-mobile-vehicle-selection")).toBeVisible();
|
||||
|
||||
@@ -106,6 +106,26 @@ describe("DepartmentDashboardDailyReportProductSales behavior", () => {
|
||||
expect(missingTileWrapper.text()).toContain("Tillæg for Specialsæbe - DD");
|
||||
});
|
||||
|
||||
it("falls back to a brand-agnostic wax title for product 26 (TRU-130)", () => {
|
||||
// TRU-130: the previous wax product ("Højglans - Voksforsegling") tested
|
||||
// poorly in the field and is being replaced. The fallback title should
|
||||
// therefore stay brand-agnostic so the tile keeps matching whatever
|
||||
// wax product the API eventually reports.
|
||||
dailyReportProductsState.value = {};
|
||||
|
||||
const waxWrapper = mount(DepartmentDashboardDailyReportProductSales, {
|
||||
props: {
|
||||
product_id: 26,
|
||||
subtitle: "Dagens salg af produktet",
|
||||
dataTestid: "daily-report-tile-hoejglans",
|
||||
},
|
||||
});
|
||||
|
||||
const rendered = waxWrapper.text();
|
||||
expect(rendered).toContain("Voksforsegling pr. enhed");
|
||||
expect(rendered).not.toContain("Højglans");
|
||||
});
|
||||
|
||||
it("shows a saved target percentage below the product percentage", () => {
|
||||
dailyReportProductsState.value = {
|
||||
24: {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { defineComponent, h, nextTick } from "vue";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
hasStoredSessionToken: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/services/sessionStorage.js", () => ({
|
||||
hasStoredSessionToken: mocks.hasStoredSessionToken,
|
||||
}));
|
||||
|
||||
vi.mock("@/composables/useStoredSessionGuestRedirect.js", () => ({
|
||||
useStoredSessionGuestRedirect: () => ({
|
||||
hasPendingStoredSession: mocks.hasStoredSessionToken(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/forms/auth/LoginForm.vue", () => ({
|
||||
default: defineComponent({
|
||||
name: "MockLoginForm",
|
||||
setup() {
|
||||
return () => h("form", { "data-testid": "login-form" });
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/views/errors/ConnectivityIssue.vue", () => ({
|
||||
default: defineComponent({
|
||||
name: "MockConnectivityIssue",
|
||||
setup(_, { slots }) {
|
||||
return () => h("div", { "data-testid": "connectivity-wrapper" }, slots.default?.());
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/global/BrandedLoadingScreen.vue", () => ({
|
||||
default: defineComponent({
|
||||
name: "MockBrandedLoadingScreen",
|
||||
props: {
|
||||
testId: { type: String, default: "branded-loading-screen" },
|
||||
},
|
||||
setup(props) {
|
||||
return () => h("div", { "data-testid": props.testId });
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
import Login from "@/views/auth/Login.vue";
|
||||
|
||||
const routerLinkStub = defineComponent({
|
||||
name: "RouterLinkStub",
|
||||
props: {
|
||||
to: {
|
||||
type: [String, Object],
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
setup(_, { slots, attrs }) {
|
||||
return () => h("a", attrs, slots.default?.());
|
||||
},
|
||||
});
|
||||
|
||||
const mountLogin = () =>
|
||||
mount(Login, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
stubs: {
|
||||
RouterLink: routerLinkStub,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe("Login.vue — driver entry block (TRU-61)", () => {
|
||||
beforeEach(() => {
|
||||
mocks.hasStoredSessionToken.mockReset();
|
||||
mocks.hasStoredSessionToken.mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
it("renders the page-level driver entry block on the customer login page", async () => {
|
||||
const wrapper = mountLogin();
|
||||
await nextTick();
|
||||
|
||||
const entry = wrapper.find('[data-testid="login-driver-entry"]');
|
||||
expect(entry.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("renders a router-link to the subuser login route inside the driver entry block", async () => {
|
||||
const wrapper = mountLogin();
|
||||
await nextTick();
|
||||
|
||||
const link = wrapper.find('[data-testid="login-driver-login-link"]');
|
||||
expect(link.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("renders the driver login button with the localized driver_login_button label", async () => {
|
||||
const wrapper = mountLogin();
|
||||
await nextTick();
|
||||
|
||||
const link = wrapper.find('[data-testid="login-driver-login-link"]');
|
||||
expect(link.text()).toContain("auth.driver_login_button");
|
||||
});
|
||||
|
||||
it("renders the driver login help text inside the entry block", async () => {
|
||||
const wrapper = mountLogin();
|
||||
await nextTick();
|
||||
|
||||
const entry = wrapper.find('[data-testid="login-driver-entry"]');
|
||||
expect(entry.text()).toContain("auth.driver_login_help");
|
||||
});
|
||||
|
||||
it("places the driver entry block in the main content area (not the sidebar)", async () => {
|
||||
const wrapper = mountLogin();
|
||||
await nextTick();
|
||||
|
||||
// The sidebar should NOT contain the driver entry — it must be in main
|
||||
const html = wrapper.html();
|
||||
const entryHtml = wrapper.find('[data-testid="login-driver-entry"]').html();
|
||||
// Find the sidebar's outer container and ensure the entry is not inside it
|
||||
const sidebarMatch = html.match(/<div class="sidebar">[\s\S]*?<\/div>\s*<div class="main">/);
|
||||
if (sidebarMatch) {
|
||||
expect(sidebarMatch[0]).not.toContain("login-driver-entry");
|
||||
}
|
||||
expect(entryHtml).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const SCRIPT_PATH = path.resolve("scripts/smoke-test-sergii.sh");
|
||||
const WORKFLOW_PATH = path.resolve(".github/workflows/deploy.yml");
|
||||
const EXISTING_SMOKE_PATH = path.resolve("scripts/smoke-test.sh");
|
||||
|
||||
describe("Sergii Review Batch smoke test (TRU-96)", () => {
|
||||
it("ships a Sergii-specific smoke test script with executable bits set", () => {
|
||||
expect(fs.existsSync(SCRIPT_PATH)).toBe(true);
|
||||
const stat = fs.statSync(SCRIPT_PATH);
|
||||
// owner-execute bit (0o100) is what `chmod +x` sets; we don't care about
|
||||
// the user the test runs as, just that the file is marked executable.
|
||||
expect(stat.mode & 0o111).not.toBe(0);
|
||||
});
|
||||
|
||||
it("covers every Sergii Review Batch project issue (TRU-68, TRU-72, TRU-75)", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
// TRU-68: customer email flow
|
||||
expect(contents).toContain("TRU-68");
|
||||
expect(contents).toMatch(/customer email flow/i);
|
||||
// TRU-72: pages 6-10 review
|
||||
expect(contents).toContain("TRU-72");
|
||||
expect(contents).toMatch(/pages 6-10/);
|
||||
// TRU-75: UVS option
|
||||
expect(contents).toContain("TRU-75");
|
||||
expect(contents).toMatch(/UVS/);
|
||||
});
|
||||
|
||||
it("probes the Sergii-touch surfaces that the Sergii batch changed (pages 6-10)", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
// Pages 6-10 — Sergii's batch touched customer, product, order, booking, invoicing.
|
||||
expect(contents).toMatch(/\/admin\/customer/);
|
||||
expect(contents).toMatch(/\/admin\/product/);
|
||||
expect(contents).toMatch(/\/admin\/order/);
|
||||
expect(contents).toMatch(/\/admin\/booking/);
|
||||
expect(contents).toMatch(/\/admin\/invoicing/);
|
||||
});
|
||||
|
||||
it("probes the Sergii-touch customer email flow surfaces (TRU-68)", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
expect(contents).toMatch(/\/api\/customer/);
|
||||
expect(contents).toMatch(/\/kundeoprettelse/);
|
||||
});
|
||||
|
||||
it("probes the Sergii-touch UVS self-serve surfaces (TRU-75)", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
expect(contents).toMatch(/\/self-serve\/program/);
|
||||
expect(contents).toMatch(/\/self-serve\/vehicle/);
|
||||
});
|
||||
|
||||
it("is wired into the pleno-vue deploy workflow alongside the generic smoke test", () => {
|
||||
const workflow = fs.readFileSync(WORKFLOW_PATH, "utf8");
|
||||
expect(workflow).toMatch(/smoke-test-sergii\.sh/);
|
||||
expect(workflow).toMatch(/Sergii Review Batch smoke test \(TRU-96\)/);
|
||||
// Generic smoke test still runs first.
|
||||
expect(workflow).toMatch(/smoke-test\.sh "\$SMOKE_BASE_URL"/);
|
||||
// Auto-rollback now also triggers on Sergii smoke failure.
|
||||
expect(workflow).toMatch(/steps\.smoke\.outcome == 'failure' \|\| steps\.smoke_sergii\.outcome == 'failure'/);
|
||||
// Slack status message distinguishes the two failure modes.
|
||||
expect(workflow).toMatch(/Generic smoke FAILED/);
|
||||
expect(workflow).toMatch(/Sergii Review Batch smoke FAILED/);
|
||||
});
|
||||
|
||||
it("does not duplicate the generic smoke test (it complements, not replaces)", () => {
|
||||
expect(fs.existsSync(EXISTING_SMOKE_PATH)).toBe(true);
|
||||
const generic = fs.readFileSync(EXISTING_SMOKE_PATH, "utf8");
|
||||
const sergii = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
// Generic covers login + /api/ping + /healthz; Sergii covers the Sergii
|
||||
// Review Batch surfaces. They are intentionally different sets.
|
||||
expect(generic).toMatch(/\/login/);
|
||||
expect(sergii).toMatch(/\/admin\/customer/);
|
||||
});
|
||||
|
||||
it("exits non-zero when any Sergii-touch surface fails, so auto-rollback can fire", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
expect(contents).toMatch(/FAIL=1/);
|
||||
expect(contents).toMatch(/exit 1/);
|
||||
expect(contents).toMatch(/Sergii Review Batch smoke test FAILED/);
|
||||
});
|
||||
|
||||
it("defaults to https://staging.truckwash.io so it is safe to run unattended", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
expect(contents).toMatch(/SMOKE_BASE_URL:-\$\{1:-https:\/\/staging\.truckwash\.io\}/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
/**
|
||||
* Tests for the XL Vask end-to-end implementation:
|
||||
* - TRU-60: badges (paid/unpaid/overdue) on superuser faktura periode
|
||||
* - TRU-71: replace "Stripe" / "Capture payment" wording
|
||||
* - TRU-81: clearer message when add-on products are not available
|
||||
* - TRU-63: z-index fix for filter behind the navbar
|
||||
*/
|
||||
|
||||
const I18N_DA = readFileSync(resolve(__dirname, "../../src/i18n/source/da/phrases/compat/pos/index.json"), "utf8");
|
||||
const I18N_EN = readFileSync(resolve(__dirname, "../../src/i18n/source/en/phrases/compat/pos/index.json"), "utf8");
|
||||
const I18N_DA_PAGINATION = readFileSync(
|
||||
resolve(__dirname, "../../src/i18n/source/da/phrases/compat/pagination/index.json"),
|
||||
"utf8"
|
||||
);
|
||||
const INVOICING_VIEW = readFileSync(
|
||||
resolve(
|
||||
__dirname,
|
||||
"../../src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
const INVOICE_PAGINATION = readFileSync(
|
||||
resolve(__dirname, "../../src/components/displays/pagination/models/SuperUserDashboard/InvoiceOrdersPagination.vue"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
// The Stripe invoice email template lives in the sibling `api` repository.
|
||||
// In the pleno-vue CI workflow the api repo is not checked out, so the file
|
||||
// is only present in local / monorepo-style checkouts. Load it lazily and
|
||||
// skip the related TRU-71 assertion gracefully when it is missing.
|
||||
const EMAIL_TEMPLATE_PATH = resolve(
|
||||
__dirname,
|
||||
"../../../api/services/nginx/app/modules/email/templates/email_template_stripe_invoice.php"
|
||||
);
|
||||
const HAS_EMAIL_TEMPLATE = existsSync(EMAIL_TEMPLATE_PATH);
|
||||
const EMAIL_TEMPLATE = HAS_EMAIL_TEMPLATE ? readFileSync(EMAIL_TEMPLATE_PATH, "utf8") : "";
|
||||
|
||||
describe("TRU-71: replace Stripe wording in user-facing strings", () => {
|
||||
it("adds the new payment_link i18n key in Danish", () => {
|
||||
expect(I18N_DA_PAGINATION).toMatch(/"payment_link":\s*"Betalingslink"/);
|
||||
});
|
||||
|
||||
it("renders the payment link label through the i18n key, not a hard-coded 'Stripe'", () => {
|
||||
// The filter option previously used the literal "Stripe" string
|
||||
// for the user-facing label. It must now resolve through the
|
||||
// pagination.payment_link translation key.
|
||||
expect(INVOICE_PAGINATION).toMatch(/t\("pagination\.payment_link"\)/);
|
||||
expect(INVOICE_PAGINATION).not.toMatch(/label:\s*"Stripe"/);
|
||||
});
|
||||
|
||||
it("rewrites the Stripe-invoice email body so it no longer mentions Stripe", () => {
|
||||
if (!HAS_EMAIL_TEMPLATE) {
|
||||
// The api repo is not checked out alongside pleno-vue in this CI job.
|
||||
// The Stripe-wording change is covered by the API repo's own tests.
|
||||
return;
|
||||
}
|
||||
expect(EMAIL_TEMPLATE).not.toMatch(/på Stripe/);
|
||||
expect(EMAIL_TEMPLATE).toMatch(/betalingslink/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TRU-81: clearer 'tilvalg ikke tilgængelig' message", () => {
|
||||
it("replaces the terse Danish 'Ikke tilgængelig' with an explanatory message", () => {
|
||||
expect(I18N_DA).toMatch(/"unavailable":\s*"Ikke tilgængeligt[^"]*tjek kundens regler"/);
|
||||
expect(I18N_DA).toMatch(/"addons_not_allowed":\s*"Tilvalg er ikke tilgængelige[^"]*"/);
|
||||
expect(I18N_DA).toMatch(/"product_not_allowed":\s*"Produktet er ikke tilgængeligt[^"]*"/);
|
||||
});
|
||||
|
||||
it("replaces the terse English 'Unavailable' with an explanatory message", () => {
|
||||
expect(I18N_EN).toMatch(/"unavailable":\s*"Unavailable\s*—\s*check[^"]*"/);
|
||||
expect(I18N_EN).toMatch(/"addons_not_allowed":\s*"Add-ons are not available[^"]*"/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TRU-60: badges on superuser faktura periode (paid/unpaid/overdue)", () => {
|
||||
it("renders an invoice-state badge for each customer in the review queue", () => {
|
||||
expect(INVOICING_VIEW).toMatch(/invoicing-period-customer-invoice-state-\$\{customer\.customer_number\}/);
|
||||
});
|
||||
|
||||
it("supports at least the four invoice-state keys (paid, unpaid, overdue, open)", () => {
|
||||
expect(INVOICING_VIEW).toMatch(/paid:\s*"Betalt"/);
|
||||
expect(INVOICING_VIEW).toMatch(/unpaid:\s*"Ikke betalt"/);
|
||||
expect(INVOICING_VIEW).toMatch(/overdue:\s*"Forfalden"/);
|
||||
expect(INVOICING_VIEW).toMatch(/open:\s*"Åben"/);
|
||||
});
|
||||
|
||||
it("treats a transaction as overdue when its due_date is in the past", () => {
|
||||
// The getCustomerInvoiceState helper checks transaction.due_date —
|
||||
// make sure the date is parsed and compared.
|
||||
expect(INVOICING_VIEW).toMatch(/isTransactionOverdue/);
|
||||
expect(INVOICING_VIEW).toMatch(/getTime\(\)\s*<\s*Date\.now\(\)/);
|
||||
});
|
||||
|
||||
it("shows the invoice-state badge in the review detail header too", () => {
|
||||
expect(INVOICING_VIEW).toMatch(/invoicing-period-detail-invoice-state-\$\{selectedCustomer\.customer_number\}/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TRU-63: filter is no longer hidden behind the navbar", () => {
|
||||
it("raises the period-review-toolbar z-index above the fixed navbar (z:1000)", () => {
|
||||
expect(INVOICING_VIEW).toMatch(/z-index:\s*1100/);
|
||||
});
|
||||
|
||||
it("offsets the sticky toolbar so it sits below the navbar instead of behind it", () => {
|
||||
expect(INVOICING_VIEW).toMatch(/top:\s*3\.75rem/);
|
||||
});
|
||||
});
|
||||