diff --git a/package-lock.json b/package-lock.json index 2c89d161..86b0e07a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -230,8 +230,8 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -261,7 +261,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -557,7 +556,6 @@ "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -1949,6 +1947,13 @@ "node": ">=0.12.0" } }, + "node_modules/@bubblewrap/core/node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT", + "optional": true + }, "node_modules/@bubblewrap/validator": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/@bubblewrap/validator/-/validator-1.22.0.tgz", @@ -1966,7 +1971,7 @@ "version": "2.11.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", - "dev": true, + "devOptional": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@creativebulma/bulma-badge": { @@ -2070,6 +2075,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -2118,6 +2124,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -2129,7 +2136,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2146,7 +2152,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2163,7 +2168,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2180,7 +2184,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2197,7 +2200,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2214,7 +2216,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2231,7 +2232,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2248,7 +2248,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2265,7 +2264,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2282,7 +2280,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2299,7 +2296,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2316,7 +2312,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2333,7 +2328,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2350,7 +2344,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2367,7 +2360,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2384,7 +2376,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2401,7 +2392,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2418,7 +2408,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2435,7 +2424,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2452,7 +2440,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2469,7 +2456,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2486,7 +2472,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2503,7 +2488,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2520,7 +2504,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2537,7 +2520,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2580,6 +2562,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" } @@ -2823,6 +2806,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" } @@ -2859,6 +2843,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" }, @@ -2871,6 +2856,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" }, @@ -2895,6 +2881,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" @@ -2938,6 +2925,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" }, @@ -3061,6 +3049,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" }, @@ -3073,6 +3062,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" }, @@ -3088,6 +3078,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" }, @@ -3222,7 +3213,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -3242,7 +3232,7 @@ "version": "0.3.11", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -3951,7 +3941,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3965,7 +3954,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3979,7 +3967,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3993,7 +3980,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4007,7 +3993,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4021,7 +4006,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4035,7 +4019,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4049,7 +4032,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4063,7 +4045,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4077,7 +4058,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4091,7 +4071,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4105,7 +4084,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4119,7 +4097,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4133,7 +4110,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4147,7 +4123,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4161,7 +4136,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4175,7 +4149,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4189,7 +4162,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4203,7 +4175,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4217,7 +4188,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4231,7 +4201,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4245,7 +4214,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4259,7 +4227,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4273,7 +4240,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4402,7 +4368,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/event-calendar__core": { @@ -5374,8 +5339,9 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5410,6 +5376,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5503,7 +5470,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", @@ -5627,6 +5595,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", @@ -5841,6 +5810,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5925,7 +5895,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", @@ -6183,6 +6154,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" }, @@ -6371,7 +6343,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/colors": { @@ -6445,7 +6417,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/copy-anything": { @@ -6597,6 +6568,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" } @@ -6785,6 +6757,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" @@ -7222,7 +7195,6 @@ "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -7267,7 +7239,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7545,7 +7516,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -7800,7 +7770,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -7894,7 +7863,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -8318,7 +8286,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", @@ -9223,7 +9192,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -9584,6 +9552,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -10148,7 +10126,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -10377,6 +10354,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", @@ -10730,8 +10708,8 @@ "version": "4.60.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10778,7 +10756,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10916,6 +10893,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", @@ -10935,8 +10913,9 @@ "version": "1.98.0", "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.98.0.tgz", "integrity": "sha512-Do7u6iRb6K+lrllcTkB1BXcHwOxcKe3rEfOF/GcCLE2w3WpddakRAosJOHFUR37DpsvimQXEt5abs3NzUjEIqg==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", @@ -11283,7 +11262,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -11576,7 +11555,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -11587,7 +11566,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -11997,7 +11976,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "sync-message-port": "^1.0.0" @@ -12010,7 +11989,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz", "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=16.0.0" @@ -12107,7 +12086,7 @@ "version": "5.46.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -12126,7 +12105,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tesseract.js": { @@ -12218,7 +12197,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -12677,15 +12655,15 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/vite": { "version": "7.1.11", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -13507,7 +13485,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -14079,6 +14056,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -14138,6 +14116,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", @@ -14825,6 +14804,7 @@ "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, diff --git a/src/components/displays/department/pos/steps/PosDepartmentStep1.vue b/src/components/displays/department/pos/steps/PosDepartmentStep1.vue index 06434de6..772214ef 100644 --- a/src/components/displays/department/pos/steps/PosDepartmentStep1.vue +++ b/src/components/displays/department/pos/steps/PosDepartmentStep1.vue @@ -23,6 +23,7 @@ import { POS_STEP_1_VERSION } from "@/config.js"; import SelectVehicleFormPOS from "@/components/forms/department/pos/SelectVehicleFormPOS.vue"; import PosLastScannedLicensePlatesV2 from "@/components/displays/department/pos/PosLastScannedLicensePlatesV2.vue"; import PosDesktopOrderBookingSelectorModal from "@/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue"; +import PosDesktopCustomerConflictModal from "@/components/displays/department/pos/steps/elements/PosDesktopCustomerConflictModal.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { parsePosRouteSearch } from "@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js"; import { @@ -67,6 +68,10 @@ const createEmptyDesktopStep1Context = () => ({ skippedBooking: false, requiresBookingSelection: false, bookingResolution: "none", + selectionSource: "none", + referenceSource: "empty", + requiresCustomerConflict: false, + customerConflict: null, committed: false, source: "initial", }); @@ -162,6 +167,14 @@ const setDesktopStep1Context = (context = {}) => { return; } + if (desktopStep1Context.value.requiresCustomerConflict) { + duplicateOrders.value = []; + duplicateWarningKey.value = ""; + duplicateDetailsExpanded.value = false; + desktopModalState.value = "customer_conflict"; + return; + } + if (desktopStep1Context.value.requiresBookingSelection) { duplicateOrders.value = []; duplicateWarningKey.value = ""; @@ -170,7 +183,7 @@ const setDesktopStep1Context = (context = {}) => { return; } - if (desktopModalState.value === "booking_selection") { + if (desktopModalState.value === "booking_selection" || desktopModalState.value === "customer_conflict") { desktopModalState.value = null; } }; @@ -282,7 +295,7 @@ const duplicateDetailsObjects = computed(() => { }); const isInlineDuplicateWarningVisible = computed(() => { - if (!isDesktopStep1Active.value || desktopModalState.value === "booking_selection") { + if (!isDesktopStep1Active.value || desktopModalState.value !== null) { return false; } @@ -415,6 +428,18 @@ const coordinateDesktopStep1 = async (options = {}) => { }; } + if (context.requiresCustomerConflict) { + duplicateOrders.value = []; + duplicateWarningKey.value = ""; + duplicateDetailsExpanded.value = false; + desktopModalState.value = "customer_conflict"; + pendingNextResolution.value = normalizedOptions.reason === "next"; + return { + canProceed: false, + context, + }; + } + if (context.requiresBookingSelection || context.bookingResolution === "selection_required") { duplicateOrders.value = []; duplicateWarningKey.value = ""; @@ -525,6 +550,60 @@ const handleBookingRefresh = async () => { } }; +const handleCustomerConflictKeep = async () => { + const context = await selectVehicleFormRef.value?.keepCustomerConflictChoice?.({ + source: "customer_conflict_keep", + committed: true, + }); + setDesktopStep1Context(context); + + desktopStep1CoordinationPromise = coordinateDesktopStep1({ + reason: "customer_conflict_keep", + finalizeReg1Input: false, + }); + const result = await desktopStep1CoordinationPromise; + + if (result.canProceed) { + await resumePendingNextStep(); + } +}; + +const handleCustomerConflictUseVehicle = async () => { + const context = await selectVehicleFormRef.value?.useVehicleCustomerConflictChoice?.({ + source: "customer_conflict_use_vehicle", + committed: true, + }); + setDesktopStep1Context(context); + + desktopStep1CoordinationPromise = coordinateDesktopStep1({ + reason: "customer_conflict_use_vehicle", + finalizeReg1Input: false, + }); + const result = await desktopStep1CoordinationPromise; + + if (result.canProceed) { + await resumePendingNextStep(); + } +}; + +const handleCustomerConflictCancel = async () => { + const context = await selectVehicleFormRef.value?.cancelCustomerConflictChoice?.({ + source: "customer_conflict_cancel", + committed: true, + }); + setDesktopStep1Context(context); + + desktopStep1CoordinationPromise = coordinateDesktopStep1({ + reason: "customer_conflict_cancel", + finalizeReg1Input: false, + }); + const result = await desktopStep1CoordinationPromise; + + if (result.canProceed) { + await resumePendingNextStep(); + } +}; + onMounted(() => { setDesktopStep1PreflightHandler(handleDesktopStep1Preflight); }); @@ -658,6 +737,16 @@ watch( @skip="handleBookingSkip" @refresh-bookings="handleBookingRefresh" /> + diff --git a/src/components/displays/department/pos/steps/elements/PosDesktopCustomerConflictModal.vue b/src/components/displays/department/pos/steps/elements/PosDesktopCustomerConflictModal.vue new file mode 100644 index 00000000..8042f57a --- /dev/null +++ b/src/components/displays/department/pos/steps/elements/PosDesktopCustomerConflictModal.vue @@ -0,0 +1,297 @@ + + + + + diff --git a/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupAddCustomer.vue b/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupAddCustomer.vue index ece53eb7..873556c3 100644 --- a/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupAddCustomer.vue +++ b/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupAddCustomer.vue @@ -1,235 +1,312 @@ \ No newline at end of file + diff --git a/src/components/displays/department/pos/steps/mobile/elements/posAddCustomerAutofill.js b/src/components/displays/department/pos/steps/mobile/elements/posAddCustomerAutofill.js new file mode 100644 index 00000000..4b23269d --- /dev/null +++ b/src/components/displays/department/pos/steps/mobile/elements/posAddCustomerAutofill.js @@ -0,0 +1,135 @@ +export const POS_ADD_CUSTOMER_AUTOFILL_FIELDS = [ + "companyPhone", + "invoiceEmail", + "contactEmail", + "contactPhone", +]; + +export function normalizePosAddCustomerValue(value) { + if (value === null || value === undefined) { + return ""; + } + + return String(value); +} + +export function createPosAddCustomerFields(overrides = {}) { + return { + cvr: "", + companyPhone: "", + invoiceEmail: "", + contactEmail: "", + contactPhone: "", + ...overrides, + }; +} + +export function createPosAddCustomerManualOverrides(overrides = {}) { + return { + companyPhone: false, + invoiceEmail: false, + contactEmail: false, + contactPhone: false, + ...overrides, + }; +} + +export function createPosAddCustomerRequestState(overrides = {}) { + return { + latestRequestedId: 0, + latestAppliedId: 0, + ...overrides, + }; +} + +export function nextPosAddCustomerRequestState(currentState = createPosAddCustomerRequestState()) { + const requestId = Number(currentState.latestRequestedId || 0) + 1; + + return { + requestId, + state: { + ...currentState, + latestRequestedId: requestId, + }, + }; +} + +export function resolvePosAddCustomerRequestState(currentState, requestId) { + const normalizedRequestId = Number(requestId || 0); + + if (!currentState || normalizedRequestId !== Number(currentState.latestRequestedId || 0)) { + return { + shouldApply: false, + state: currentState, + }; + } + + return { + shouldApply: true, + state: { + ...currentState, + latestAppliedId: normalizedRequestId, + }, + }; +} + +export function isValidPosAddCustomerSearchResult(searchResult, notFoundLabel = "") { + if (!searchResult || typeof searchResult !== "object") { + return false; + } + + const normalizedName = normalizePosAddCustomerValue(searchResult.name).trim(); + if (normalizedName === "") { + return false; + } + + return normalizedName !== normalizePosAddCustomerValue(notFoundLabel).trim(); +} + +export function buildPosAddCustomerAutofillValues(searchResult) { + return { + cvr: normalizePosAddCustomerValue(searchResult?.vat), + companyPhone: normalizePosAddCustomerValue(searchResult?.phone), + invoiceEmail: normalizePosAddCustomerValue(searchResult?.email), + contactEmail: normalizePosAddCustomerValue(searchResult?.email), + contactPhone: normalizePosAddCustomerValue(searchResult?.phone), + }; +} + +export function applyPosAddCustomerSearchResult({ + fields = createPosAddCustomerFields(), + manualOverrides = createPosAddCustomerManualOverrides(), + searchResult = null, + notFoundLabel = "", +} = {}) { + const nextFields = { + ...createPosAddCustomerFields(), + ...fields, + }; + + if (!isValidPosAddCustomerSearchResult(searchResult, notFoundLabel)) { + return nextFields; + } + + const autofillValues = buildPosAddCustomerAutofillValues(searchResult); + nextFields.cvr = autofillValues.cvr; + + for (const field of POS_ADD_CUSTOMER_AUTOFILL_FIELDS) { + if (!manualOverrides[field] || normalizePosAddCustomerValue(nextFields[field]) === "") { + nextFields[field] = autofillValues[field]; + } + } + + return nextFields; +} + +export function setPosAddCustomerManualOverride(manualOverrides, field, nextValue) { + if (!manualOverrides || !Object.prototype.hasOwnProperty.call(manualOverrides, field)) { + return manualOverrides; + } + + return { + ...manualOverrides, + [field]: normalizePosAddCustomerValue(nextValue).trim() !== "", + }; +} diff --git a/src/components/forms/department/pos/SelectVehicleFormPOS.vue b/src/components/forms/department/pos/SelectVehicleFormPOS.vue index e836e62f..2d75fa03 100644 --- a/src/components/forms/department/pos/SelectVehicleFormPOS.vue +++ b/src/components/forms/department/pos/SelectVehicleFormPOS.vue @@ -42,6 +42,10 @@ const bookingObject = ref(null); const bookingMatches = ref([]); const isUserCurrentlyInFocusReg1 = ref(false); const licensePlateReg1InputRef = ref(null); +const selectionSource = ref("none"); +const referenceSource = ref(String(reference.value ?? "").trim() === "" ? "empty" : "manual"); +const pendingCustomerConflict = ref(null); +const suppressedCustomerConflictKey = ref(""); let bookingResolutionPromise = Promise.resolve(null); @@ -57,6 +61,69 @@ const normalizePlateValue = (value) => .replace(/\s/g, "") .toUpperCase(); +const normalizeCustomerNumber = (value) => { + const parsedValue = Number.parseInt(String(value ?? ""), 10); + return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null; +}; + +const isBlankPosValue = (value) => String(value ?? "").trim() === ""; + +const getCustomerConflictKey = (conflict) => { + if (!conflict) { + return ""; + } + + return [ + normalizePlateValue(conflict.plate ?? conflict.vehicleReg), + normalizeCustomerNumber(conflict.currentCustomerNumber) ?? "", + normalizeCustomerNumber(conflict.matchedCustomerNumber) ?? "", + ].join("|"); +}; + +const setSelectionSource = (nextSource = "none") => { + const allowedSources = new Set(["none", "manual-customer", "vehicle-match", "booking-match"]); + selectionSource.value = allowedSources.has(nextSource) ? nextSource : "none"; +}; + +const setReferenceSource = (nextSource = "empty") => { + const allowedSources = new Set(["empty", "manual", "vehicle", "booking"]); + referenceSource.value = allowedSources.has(nextSource) ? nextSource : "empty"; +}; + +const setReferenceValue = (nextValue, source = "manual", options = {}) => { + const normalizedOptions = { + preserveManualReference: false, + ...options, + }; + + if (normalizedOptions.preserveManualReference && referenceSource.value === "manual" && source !== "manual") { + return reference.value; + } + + reference.value = String(nextValue ?? ""); + + if (isBlankPosValue(reference.value)) { + setReferenceSource("empty"); + return reference.value; + } + + setReferenceSource(source); + return reference.value; +}; + +const clearPendingCustomerConflict = (options = {}) => { + const normalizedOptions = { + preserveSuppressedKey: true, + ...options, + }; + + pendingCustomerConflict.value = null; + + if (!normalizedOptions.preserveSuppressedKey) { + suppressedCustomerConflictKey.value = ""; + } +}; + const getCurrentVehiclePlate = () => { return normalizePlateValue(reg_1.value || vehicleObject.value?.reg); }; @@ -86,38 +153,73 @@ const getBookingCustomerNumber = (booking) => { return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null; }; -const resetReferenceFromVehicle = () => { +const resetReferenceFromVehicle = (options = {}) => { if (vehicleObject.value?.reference) { - reference.value = vehicleObject.value.reference; + setReferenceValue(vehicleObject.value.reference, "vehicle", options); return; } - reference.value = ""; + setReferenceValue("", "empty", options); }; const getVehicleReferenceValue = () => { return String(vehicleObject.value?.reference ?? "").trim(); }; -const setVehicleObject = (emittedVehicleObject) => { +const setVehicleObject = (emittedVehicleObject, options = {}) => { + const normalizedOptions = { + preserveManualReference: true, + nextSelectionSource: null, + ...options, + }; + vehicleObject.value = emittedVehicleObject; + const matchedVehicleCustomerNumber = normalizeCustomerNumber(vehicleObject.value?.customer_id); + const selectedCustomerNumber = normalizeCustomerNumber(customer_id.value); + + if (normalizedOptions.nextSelectionSource) { + setSelectionSource(normalizedOptions.nextSelectionSource); + } else if (vehicleObject.value) { + if ( + selectionSource.value !== "manual-customer" || + (matchedVehicleCustomerNumber && + selectedCustomerNumber && + matchedVehicleCustomerNumber !== selectedCustomerNumber) + ) { + setSelectionSource("vehicle-match"); + } + } else if (!bookingObject.value && !normalizeCustomerNumber(customer_id.value)) { + setSelectionSource("none"); + } + if ( vehicleObject.value && vehicleObject.value.reference && normalizePlateValue(selectedOrderBookingPlate.value) !== normalizePlateValue(vehicleObject.value.reg) ) { - reference.value = vehicleObject.value.reference; + setReferenceValue(vehicleObject.value.reference, "vehicle", { + preserveManualReference: normalizedOptions.preserveManualReference, + }); } else if (!selectedOrderBookingId.value) { - reference.value = ""; + setReferenceValue("", "empty", { + preserveManualReference: normalizedOptions.preserveManualReference, + }); } }; -const setBookingObject = (emittedBookingObject) => { +const setBookingObject = (emittedBookingObject, options = {}) => { + const normalizedOptions = { + preserveManualReference: true, + ...options, + }; + bookingObject.value = emittedBookingObject; if (emittedBookingObject && emittedBookingObject.reference_number) { - reference.value = emittedBookingObject.reference_number; + setReferenceValue(emittedBookingObject.reference_number, "booking", { + preserveManualReference: normalizedOptions.preserveManualReference, + }); } }; @@ -153,7 +255,12 @@ const syncBookingRegistrationFields = async (booking) => { } }; -const applyBookingMetadata = async (booking) => { +const applyBookingMetadata = async (booking, options = {}) => { + const normalizedOptions = { + preserveManualReference: true, + selectionSource: "booking-match", + ...options, + }; const previousReference = String(reference.value ?? "").trim(); const previousNotes = String(order_notes.value ?? "").trim(); const previousPo = String(order_po.value ?? "").trim(); @@ -161,14 +268,21 @@ const applyBookingMetadata = async (booking) => { if (bookingCustomerNumber && Number.parseInt(String(customer_id.value || ""), 10) !== bookingCustomerNumber) { await searchAndSelectCustomer(bookingCustomerNumber); } + if (bookingCustomerNumber) { + setSelectionSource(normalizedOptions.selectionSource); + } const bookingReference = getBookingReferenceValue(booking); - const resolvedReference = bookingReference !== "" ? bookingReference : getVehicleReferenceValue(); if (bookingReference !== "") { - reference.value = bookingReference; + setReferenceValue(bookingReference, "booking", { + preserveManualReference: normalizedOptions.preserveManualReference, + }); } else { - resetReferenceFromVehicle(); + resetReferenceFromVehicle({ + preserveManualReference: normalizedOptions.preserveManualReference, + }); } + const resolvedReference = String(reference.value ?? "").trim(); const bookingNotes = getBookingNotesValue(booking); const bookingPo = getBookingPoValue(booking); @@ -195,6 +309,8 @@ const applyBookingMetadata = async (booking) => { const applySelectedOrderBooking = async (booking, options = {}) => { const normalizedOptions = { persistSelection: true, + preserveManualReference: true, + selectionSource: "booking-match", ...options, }; @@ -213,7 +329,10 @@ const applySelectedOrderBooking = async (booking, options = {}) => { ); } - await applyBookingMetadata(booking); + await applyBookingMetadata(booking, { + preserveManualReference: normalizedOptions.preserveManualReference, + selectionSource: normalizedOptions.selectionSource, + }); if (vehicleObject.value) { vehicleObject.value = { @@ -228,12 +347,14 @@ const continueWithoutBooking = async () => { const previousReference = String(reference.value ?? "").trim(); const previousNotes = String(order_notes.value ?? "").trim(); const previousPo = String(order_po.value ?? "").trim(); - const nextReference = getVehicleReferenceValue(); bookingObject.value = null; skipSelectedOrderBookingSelection(getCurrentVehiclePlate()); order_notes.value = ""; order_po.value = ""; - resetReferenceFromVehicle(); + resetReferenceFromVehicle({ + preserveManualReference: true, + }); + const nextReference = String(reference.value ?? "").trim(); if (vehicleObject.value) { vehicleObject.value = { @@ -310,6 +431,21 @@ const buildDesktopStep1Context = (options = {}) => { skippedBooking, requiresBookingSelection, bookingResolution, + selectionSource: selectionSource.value, + referenceSource: referenceSource.value, + requiresCustomerConflict: Boolean(pendingCustomerConflict.value), + customerConflict: pendingCustomerConflict.value + ? { + key: pendingCustomerConflict.value.key, + plate: pendingCustomerConflict.value.plate, + currentCustomerNumber: pendingCustomerConflict.value.currentCustomerNumber, + currentCustomerName: pendingCustomerConflict.value.currentCustomerName, + matchedCustomerNumber: pendingCustomerConflict.value.matchedCustomerNumber, + matchedCustomerName: pendingCustomerConflict.value.matchedCustomerName, + vehicleId: pendingCustomerConflict.value.vehicleId, + vehicleReg: pendingCustomerConflict.value.vehicleReg, + } + : null, committed: normalizedOptions.committed, source: normalizedOptions.source, }; @@ -329,6 +465,9 @@ const resolveBookingMatchesState = async (matches) => { clearSelectedOrderBookingSelection(); order_notes.value = ""; order_po.value = ""; + resetReferenceFromVehicle({ + preserveManualReference: true, + }); return emitDesktopStep1Context({ committed: false, source: "booking_state", @@ -340,7 +479,9 @@ const resolveBookingMatchesState = async (matches) => { clearSelectedOrderBookingSelection(); order_notes.value = ""; order_po.value = ""; - resetReferenceFromVehicle(); + resetReferenceFromVehicle({ + preserveManualReference: true, + }); return emitDesktopStep1Context({ committed: false, source: "booking_state", @@ -350,9 +491,14 @@ const resolveBookingMatchesState = async (matches) => { if (matches.length === 1) { const [singleBooking] = matches; if (Number(selectedOrderBookingId.value) !== Number(singleBooking.id)) { - await applySelectedOrderBooking(singleBooking); + await applySelectedOrderBooking(singleBooking, { + preserveManualReference: true, + }); } else { - await applySelectedOrderBooking(singleBooking, { persistSelection: false }); + await applySelectedOrderBooking(singleBooking, { + persistSelection: false, + preserveManualReference: true, + }); } return emitDesktopStep1Context({ committed: false, @@ -362,7 +508,10 @@ const resolveBookingMatchesState = async (matches) => { const persistedBooking = getSelectedVehiclePlateBooking(currentPlate, matches); if (persistedBooking) { - await applySelectedOrderBooking(persistedBooking, { persistSelection: false }); + await applySelectedOrderBooking(persistedBooking, { + persistSelection: false, + preserveManualReference: true, + }); return emitDesktopStep1Context({ committed: false, source: "booking_restore", @@ -389,6 +538,91 @@ watch(bookingMatches, (matches) => { bookingResolutionPromise = resolveBookingMatchesState(matches); }); +const handleReferenceInput = (event) => { + setReferenceValue(event?.target?.value ?? "", "manual"); +}; + +const handleManualCustomerSelection = () => { + clearPendingCustomerConflict({ + preserveSuppressedKey: false, + }); + setSelectionSource("manual-customer"); + emitDesktopStep1Context({ + committed: false, + source: "manual_customer", + }); +}; + +const handleManualCustomerSuggestionSelection = async (customerNumber) => { + clearPendingCustomerConflict({ + preserveSuppressedKey: false, + }); + setSelectionSource("manual-customer"); + await searchAndSelectCustomer(customerNumber); + emitDesktopStep1Context({ + committed: false, + source: "manual_customer_suggestion", + }); +}; + +const handleCustomerConflictUpdate = (conflict) => { + pendingCustomerConflict.value = conflict + ? { + ...conflict, + key: getCustomerConflictKey(conflict), + } + : null; + + emitDesktopStep1Context({ + committed: false, + source: conflict ? "customer_conflict" : "customer_conflict_clear", + }); +}; + +watch( + () => customer_id.value, + (nextCustomerId) => { + const normalizedCustomerNumber = normalizeCustomerNumber(nextCustomerId); + if (!normalizedCustomerNumber) { + if (!vehicleObject.value && !bookingObject.value) { + setSelectionSource("none"); + } + clearPendingCustomerConflict({ + preserveSuppressedKey: false, + }); + } + } +); + +watch( + [() => normalizePlateValue(reg_1.value), () => normalizeCustomerNumber(customer_id.value)], + ([currentPlate, currentCustomerNumber]) => { + if (pendingCustomerConflict.value) { + const currentConflictKey = getCustomerConflictKey({ + ...pendingCustomerConflict.value, + plate: currentPlate, + currentCustomerNumber, + }); + + if (currentConflictKey !== pendingCustomerConflict.value.key) { + pendingCustomerConflict.value = null; + } + } + + if (!suppressedCustomerConflictKey.value) { + return; + } + + const [suppressedPlate, suppressedCustomerNumber] = suppressedCustomerConflictKey.value.split("|"); + if ( + suppressedPlate !== currentPlate || + String(normalizeCustomerNumber(suppressedCustomerNumber) ?? "") !== String(currentCustomerNumber ?? "") + ) { + suppressedCustomerConflictKey.value = ""; + } + } +); + const ucFirst = (str) => { return str.charAt(0).toUpperCase() + str.slice(1); }; @@ -460,6 +694,78 @@ const handleLicensePlateCommit = async (context) => { emit("commit:desktopStep1", committedContext); }; +const keepCustomerConflictChoice = async (options = {}) => { + const normalizedOptions = { + source: "customer_conflict_keep", + committed: true, + ...options, + }; + + if (pendingCustomerConflict.value?.key) { + suppressedCustomerConflictKey.value = pendingCustomerConflict.value.key; + } + + clearPendingCustomerConflict(); + + return await finalizeDesktopStep1Context({ + source: normalizedOptions.source, + finalizeReg1Input: false, + committed: normalizedOptions.committed, + }); +}; + +const cancelCustomerConflictChoice = async (options = {}) => { + return await keepCustomerConflictChoice({ + source: options?.source || "customer_conflict_cancel", + committed: options?.committed ?? true, + }); +}; + +const useVehicleCustomerConflictChoice = async (options = {}) => { + const normalizedOptions = { + source: "customer_conflict_use_vehicle", + committed: true, + ...options, + }; + + const conflict = pendingCustomerConflict.value; + if (!conflict) { + return await finalizeDesktopStep1Context({ + source: normalizedOptions.source, + finalizeReg1Input: false, + committed: normalizedOptions.committed, + }); + } + + clearPendingCustomerConflict({ + preserveSuppressedKey: false, + }); + + if (referenceSource.value === "manual") { + setReferenceSource(isBlankPosValue(reference.value) ? "empty" : "vehicle"); + } + + const matchedCustomerNumber = normalizeCustomerNumber(conflict.matchedCustomerNumber); + if (matchedCustomerNumber && normalizeCustomerNumber(customer_id.value) !== matchedCustomerNumber) { + await searchAndSelectCustomer(matchedCustomerNumber); + } + + setVehicleObject(conflict.vehicle || null, { + preserveManualReference: false, + nextSelectionSource: "vehicle-match", + }); + setBookingMatches(conflict.bookingMatches || []); + setBookingObject((conflict.bookingMatches || []).length === 1 ? conflict.bookingMatches[0] : null, { + preserveManualReference: false, + }); + + return await finalizeDesktopStep1Context({ + source: normalizedOptions.source, + finalizeReg1Input: false, + committed: normalizedOptions.committed, + }); +}; + const applyBookingChoice = async (booking, options = {}) => { const normalizedOptions = { source: "booking_selection", @@ -481,7 +787,9 @@ const applyBookingChoice = async (booking, options = {}) => { }); } - await applySelectedOrderBooking(resolvedBooking); + await applySelectedOrderBooking(resolvedBooking, { + preserveManualReference: false, + }); return await finalizeDesktopStep1Context({ source: normalizedOptions.source, finalizeReg1Input: false, @@ -542,6 +850,9 @@ defineExpose({ applyBookingChoice, skipBookingChoice, refreshOrderBookingMatches, + keepCustomerConflictChoice, + cancelCustomerConflictChoice, + useVehicleCustomerConflictChoice, }); @@ -586,10 +897,13 @@ defineExpose({
@@ -643,7 +957,8 @@ defineExpose({ type="text" class="input has-sharp-edges" :tabindex="4" - v-model="reference" + :value="reference" + @input="handleReferenceInput" @keydown="keyDownNextInput($event, 'customer_id')" /> @@ -652,8 +967,8 @@ defineExpose({
{{ SessionUser.objects.global.language.select }} {{ SessionUser.objects.global.language.customer }}
- - + +
{{ SessionUser.objects.global.language.other }}
- + { return Number.isInteger(parsed) && parsed > 0 ? parsed : null; }; +const getCustomerConflictKey = (conflict) => { + if (!conflict) { + return ""; + } + + return [ + resolveBookingPlate(null, conflict.plate ?? conflict.vehicleReg), + normalizeCustomerNumber(conflict.currentCustomerNumber) ?? "", + normalizeCustomerNumber(conflict.matchedCustomerNumber) ?? "", + ].join("|"); +}; + +const hasManualCustomerSelection = () => { + return props.customerSelectionSource === "manual-customer" && normalizeCustomerNumber(customer_id.value) !== null; +}; + +const clearCustomerConflict = () => { + emit("update:customerConflict", null); +}; + +const buildCustomerConflictPayload = (vehicle, plateOverride = null) => { + const currentCustomerNumber = normalizeCustomerNumber(customer_id.value); + const matchedCustomerNumber = normalizeCustomerNumber(vehicle?.customer_id); + if (!currentCustomerNumber || !matchedCustomerNumber || currentCustomerNumber === matchedCustomerNumber) { + return null; + } + + const conflict = { + plate: resolveBookingPlate(vehicle, plateOverride), + currentCustomerNumber, + currentCustomerName: String(customer_name.value ?? "").trim(), + matchedCustomerNumber, + matchedCustomerName: String(vehicle?.customer_name ?? "").trim(), + vehicleId: vehicle?.id ?? null, + vehicleReg: resolveBookingPlate(vehicle, plateOverride), + vehicle, + bookingMatches: getBookingMatchesForSelection(vehicle, plateOverride), + }; + + return { + ...conflict, + key: getCustomerConflictKey(conflict), + }; +}; + const isVehicleCustomerAlreadySelected = (vehicle) => { const vehicleCustomerNumber = normalizeCustomerNumber(vehicle?.customer_id); return vehicleCustomerNumber !== null && vehicleCustomerNumber === normalizeCustomerNumber(customer_id.value); }; const shouldPreserveCustomerSelection = (plateValue) => { + if (hasManualCustomerSelection()) { + return true; + } + const normalizedPlate = String(plateValue ?? "") .trim() .toUpperCase(); @@ -288,16 +347,49 @@ const shouldPreserveCustomerSelection = (plateValue) => { }; let pendingVehicleCustomerLookup = null; -const syncMatchedVehicleSelection = (vehicle) => { +const syncMatchedVehicleSelection = (vehicle, options = {}) => { + const normalizedOptions = { + plateOverride: null, + allowCustomerConflict: true, + ...options, + }; + if (!vehicle) { - emitVehicleObject(null); - return; + clearCustomerConflict(); + emitVehicleObject(null, normalizedOptions.plateOverride); + return { + status: "cleared", + vehicle: null, + }; } - if (reg_1.value !== vehicle.reg) { - reg_1.value = vehicle.reg; + const resolvedPlate = resolveBookingPlate(vehicle, normalizedOptions.plateOverride); + if (reg_1.value !== resolvedPlate) { + reg_1.value = resolvedPlate; } + if (normalizedOptions.allowCustomerConflict && hasManualCustomerSelection()) { + const customerConflict = buildCustomerConflictPayload(vehicle, resolvedPlate); + if (customerConflict) { + if (props.suppressedCustomerConflictKey === customerConflict.key) { + clearCustomerConflict(); + emitVehicleObject(null, resolvedPlate); + return { + status: "suppressed_conflict", + conflict: customerConflict, + }; + } + + emit("update:customerConflict", customerConflict); + emitVehicleObject(null, resolvedPlate); + return { + status: "conflict", + conflict: customerConflict, + }; + } + } + + clearCustomerConflict(); getCustomerName(vehicle.customer_id); const vehicleCustomerNumber = normalizeCustomerNumber(vehicle.customer_id); @@ -314,7 +406,11 @@ const syncMatchedVehicleSelection = (vehicle) => { }); } - emitVehicleObject(vehicle); + emitVehicleObject(vehicle, resolvedPlate); + return { + status: "selected", + vehicle, + }; }; // Define reactive properties @@ -429,6 +525,7 @@ watch(reg_1, (newValue) => { selectedDropdownItem.value = -1; // Check if the new value is empty, if so, clear the vehicles_matching array if (!newValue) { + clearCustomerConflict(); unselectCustomerOnChange(); emitVehicleObject(null); vehicles_matching.value = []; @@ -441,6 +538,7 @@ watch(reg_1, (newValue) => { } if (!shouldKeepCustomerSelection) { + clearCustomerConflict(); unselectCustomerOnChange(); emitVehicleObject(null); } @@ -449,12 +547,15 @@ watch(reg_1, (newValue) => { }); const unselectCustomerOnChange = () => { + if (hasManualCustomerSelection()) { + return; + } clearCustomerSelection(); }; -const selectVehicle = (vehicle) => { +const selectVehicle = (vehicle, options = {}) => { console.log("Select using click", vehicle?.id ?? vehicle?.reg ?? null); - syncMatchedVehicleSelection(vehicle); + return syncMatchedVehicleSelection(vehicle, options); }; const getDropdownItemKey = (vehicle, index) => { @@ -540,11 +641,14 @@ const commitSelection = async (options = {}) => { } const bookingMatches = await ensureBookingMatchesForSelection(vehicle, plateOverride); + let selectionOutcome = null; showSelector.value = false; if (vehicle) { - selectVehicle(vehicle); + selectionOutcome = selectVehicle(vehicle, { + plateOverride, + }); } else { if (normalizedOptions.clearDropdownResultsWithoutVehicle) { vehicles_matching.value = []; @@ -553,10 +657,16 @@ const commitSelection = async (options = {}) => { if (bookingMatches.length === 0 && !hasSelectedCustomer) { clearCustomerSelection(); } + clearCustomerConflict(); emitVehicleObject(null, plateOverride); } - if (normalizedOptions.focusNextField && bookingMatches.length <= 1) { + if ( + normalizedOptions.focusNextField && + bookingMatches.length <= 1 && + selectionOutcome?.status !== "conflict" && + selectionOutcome?.status !== "suppressed_conflict" + ) { if (bookingMatches.length === 1 || !vehicleIndicatesBooking(vehicle)) { focusNextField(); } @@ -668,7 +778,10 @@ watch(vehicles_matching, (newValue) => { if (vehicle) { syncMatchedVehicleSelection(vehicle); } else if (getBookingMatchesForSelection(null, currentValue).length > 0) { + clearCustomerConflict(); emitVehicleObject(null, currentValue); + } else { + clearCustomerConflict(); } // Check if the selectedDropdownItem index is valid if (selectedDropdownItem.value >= newValue.length) { diff --git a/src/i18n/locales/da.json b/src/i18n/locales/da.json index cef05bdb..30ccaab6 100644 --- a/src/i18n/locales/da.json +++ b/src/i18n/locales/da.json @@ -372,6 +372,16 @@ "reference_label": "Reference", "services_label": "Ydelser" }, + "customer_conflict": { + "title": "Bekraeft kunde til nummerplade", + "help_text": "Den indtastede nummerplade tilhoerer en anden gemt kunde. Vaelg om den valgte kunde skal beholdes, eller om der skal skiftes til koeretoejets kunde.", + "warning_text": "Den valgte kunde og koeretoejets kunde matcher ikke.", + "current_customer_label": "Valgt kunde", + "matched_customer_label": "Koeretoejets kunde", + "keep_selected_customer": "Behold valgt kunde", + "use_vehicle_customer": "Brug koeretoejets kunde", + "cancel": "Annuller" + }, "orders": { "subtitle": "Administrer ordrer", "title": "Ordrer" diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 3c39562d..8a143d02 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -372,6 +372,16 @@ "reference_label": "Referenz", "services_label": "Leistungen" }, + "customer_conflict": { + "title": "Kunde fuer Kennzeichen bestaetigen", + "help_text": "Dieses Kennzeichen gehoert zu einem anderen gespeicherten Kunden. Waehlen Sie, ob der ausgewaehlte Kunde beibehalten oder zum Fahrzeugkunden gewechselt werden soll.", + "warning_text": "Der ausgewaehlte Kunde und der Fahrzeugkunde stimmen nicht ueberein.", + "current_customer_label": "Ausgewaehlter Kunde", + "matched_customer_label": "Fahrzeugkunde", + "keep_selected_customer": "Ausgewaehlten Kunden behalten", + "use_vehicle_customer": "Fahrzeugkunde verwenden", + "cancel": "Abbrechen" + }, "orders": { "subtitle": "?bersicht des Waschprotokolls", "title": "Bestellungen" diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index fcce0811..5e24e535 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -372,6 +372,16 @@ "reference_label": "Reference", "services_label": "Services" }, + "customer_conflict": { + "title": "Confirm customer for plate", + "help_text": "This typed plate belongs to another saved customer. Choose whether to keep the selected customer or switch to the vehicle customer.", + "warning_text": "The selected customer and the matched vehicle customer do not match.", + "current_customer_label": "Selected customer", + "matched_customer_label": "Vehicle customer", + "keep_selected_customer": "Keep selected customer", + "use_vehicle_customer": "Use vehicle customer", + "cancel": "Cancel" + }, "orders": { "subtitle": "Overview of wash log", "title": "Wash Log" diff --git a/src/i18n/locales/no.json b/src/i18n/locales/no.json index 310a46e8..a23e4fd6 100644 --- a/src/i18n/locales/no.json +++ b/src/i18n/locales/no.json @@ -372,6 +372,16 @@ "reference_label": "Referanse", "services_label": "Tjenester" }, + "customer_conflict": { + "title": "Bekreft kunde for skilt", + "help_text": "Det innskrevne skiltet tilhoerer en annen lagret kunde. Velg om den valgte kunden skal beholdes eller om det skal byttes til kjoretoykunden.", + "warning_text": "Den valgte kunden og kjoretoykunden matcher ikke.", + "current_customer_label": "Valgt kunde", + "matched_customer_label": "Kjoretoykunde", + "keep_selected_customer": "Behold valgt kunde", + "use_vehicle_customer": "Bruk kjoretoykunde", + "cancel": "Avbryt" + }, "orders": { "subtitle": "Oversikt over vaskelogg", "title": "Ordrer" diff --git a/src/i18n/locales/sv.json b/src/i18n/locales/sv.json index b7b88646..8e808aa8 100644 --- a/src/i18n/locales/sv.json +++ b/src/i18n/locales/sv.json @@ -372,6 +372,16 @@ "reference_label": "Referens", "services_label": "Tjänster" }, + "customer_conflict": { + "title": "Bekrafta kund for registreringsnummer", + "help_text": "Det angivna registreringsnumret tillhor en annan sparad kund. Valj om den valda kunden ska behallas eller om bytet ska goras till fordonskunden.", + "warning_text": "Den valda kunden och fordonskunden matchar inte.", + "current_customer_label": "Vald kund", + "matched_customer_label": "Fordonskund", + "keep_selected_customer": "Behall vald kund", + "use_vehicle_customer": "Anvand fordonskund", + "cancel": "Avbryt" + }, "orders": { "subtitle": "översikt över tvättlogg", "title": "Ordrar" diff --git a/src/views/pages/auth/PasswordResetConfirmationPage.vue b/src/views/pages/auth/PasswordResetConfirmationPage.vue index 579e5c97..a012b1d8 100644 --- a/src/views/pages/auth/PasswordResetConfirmationPage.vue +++ b/src/views/pages/auth/PasswordResetConfirmationPage.vue @@ -205,9 +205,7 @@ onBeforeUnmount(() => { .login-section .wrapper .main p{ max-width: 400px; - //text-align: center; font-size: 16px; - //color: #13324C; } .login-section .wrapper .main a{ @@ -259,4 +257,4 @@ onBeforeUnmount(() => { padding: 70px 10px; } } - \ No newline at end of file + diff --git a/src/views/pages/auth/PasswordResetPage.vue b/src/views/pages/auth/PasswordResetPage.vue index c192d83b..c13267f1 100644 --- a/src/views/pages/auth/PasswordResetPage.vue +++ b/src/views/pages/auth/PasswordResetPage.vue @@ -159,9 +159,7 @@ onBeforeUnmount(() => { .login-section .wrapper .main p{ max-width: 400px; - //text-align: center; font-size: 16px; - //color: #13324C; } .login-section .wrapper .main a{ @@ -212,4 +210,4 @@ onBeforeUnmount(() => { padding: 70px 10px; } } - \ No newline at end of file + diff --git a/tests/e2e/admin-pos-orders.spec.ts b/tests/e2e/admin-pos-orders.spec.ts index ac2294d7..7ff83e15 100644 --- a/tests/e2e/admin-pos-orders.spec.ts +++ b/tests/e2e/admin-pos-orders.spec.ts @@ -53,6 +53,17 @@ async function createDisposableOrder(page: Page) { }; } +async function selectStepOneCustomer( + page: Page, + customerNumber: number | string, + expectedName: string | RegExp = /\(TEST\) Pleno Vognmandsforretning/ +) { + await page.locator("#pos_select_customer_input").fill(String(customerNumber)); + await expect(page.locator(".customer-drop-down-select").first()).toBeVisible(); + await page.locator(".customer-drop-down-select").first().click(); + await expect(page.locator(".field.has-addons input[disabled]").last()).toHaveValue(expectedName); +} + async function clickVisibleTestId(page: Page, testId: string) { const locator = page.getByTestId(testId); const count = await locator.count(); @@ -275,6 +286,47 @@ function createRequiredWarningsPosFixture() { }); } +function createCustomerConflictPosFixture() { + const baseFixture = createPosFixture(); + const defaultCustomer = baseFixture.customersByNumber[12345679]; + const vehicleCustomerNumber = 22334455; + const vehicleCustomerName = "(TEST) Conflict Fleet"; + + return createPosFixture({ + customersByNumber: { + [vehicleCustomerNumber]: { + ...defaultCustomer, + id: 3, + customerNumber: vehicleCustomerNumber, + economic_customer: vehicleCustomerNumber, + name: vehicleCustomerName, + email: "conflict@example.com", + mobilePhone: "22334455", + }, + }, + vehicles: [ + ...baseFixture.vehicles.filter((vehicle) => vehicle.reg !== "CONFLICT1"), + { + id: 7701, + reg: "CONFLICT1", + customer_id: vehicleCustomerNumber, + customer_name: vehicleCustomerName, + type: 53, + status: "verified", + barred: false, + wash_subscription: false, + addons: { + enabled: 0, + available: 0, + list: [], + }, + reference: "CONFLICT-VEHICLE-REF", + last_order_id: null, + }, + ], + }); +} + test.describe("Admin POS Orders - desktop settings", () => { test.beforeEach(async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only order settings coverage"); @@ -1426,6 +1478,113 @@ test.describe("Admin POS Orders - desktop required warning states", () => { }); }); +test.describe("Admin POS Orders - desktop step 1 customer and vehicle ownership", () => { + test.beforeEach(async ({ page }, testInfo) => { + test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only step 1 customer ownership coverage"); + }); + + test("preserves a manually selected required-reference customer and manual reference while editing reg_1", async ({ + page, + }) => { + await mockApi(page, { + authenticated: true, + permissions: POS_PERMISSIONS, + edgeGateways: false, + pos: createRequiredWarningsPosFixture(), + }); + await primeOperatorSession(page, "pos-orders-manual-customer-reg-edit-token"); + + await page.goto("/admin/12/modules/pos?step=1"); + await expect(page.getByTestId("pos-step-1")).toBeVisible(); + + await page.locator("#reg_1").fill("MAN"); + await selectStepOneCustomer(page, 12345679); + + const referenceInput = page.locator("#reference"); + await referenceInput.fill("MANUAL-REF-001"); + + await page.locator("#reg_1").fill("MANUAL1"); + await expect(page.locator(".pos-selected-customer__title").filter({ hasText: /\(TEST\) Pleno/ }).first()).toBeVisible(); + await expect(referenceInput).toHaveValue("MANUAL-REF-001"); + + await page.locator("#reg_1").fill(""); + await expect(page.locator(".pos-selected-customer__title").filter({ hasText: /\(TEST\) Pleno/ }).first()).toBeVisible(); + await expect(referenceInput).toHaveValue("MANUAL-REF-001"); + + await page.locator("#reg_1").fill("MANUAL2"); + await expect(page.locator(".pos-selected-customer__title").filter({ hasText: /\(TEST\) Pleno/ }).first()).toBeVisible(); + await expect(referenceInput).toHaveValue("MANUAL-REF-001"); + }); + + test("asks whether to keep the selected customer when an exact plate belongs to another customer", async ({ + page, + }) => { + const fixture = createCustomerConflictPosFixture(); + + await mockApi(page, { + authenticated: true, + permissions: POS_PERMISSIONS, + edgeGateways: false, + pos: fixture, + }); + await primeOperatorSession(page, "pos-orders-customer-conflict-keep-token"); + + await page.goto("/admin/12/modules/pos?step=1"); + await expect(page.getByTestId("pos-step-1")).toBeVisible(); + + await page.locator("#reg_1").fill("CON"); + await selectStepOneCustomer(page, 12345679); + await page.locator("#reference").fill("KEEP-REF"); + + await page.locator("#reg_1").fill("CONFLICT1"); + const activeConflictModal = page.locator('[data-testid="pos-desktop-customer-conflict-modal"].is-active'); + await expect(activeConflictModal).toBeVisible(); + + await activeConflictModal.getByTestId("pos-desktop-customer-conflict-keep").click(); + await expect(activeConflictModal).toHaveCount(0); + + await expect(page.locator(".pos-selected-customer__title").filter({ hasText: /\(TEST\) Pleno/ }).first()).toBeVisible(); + await expect(page.locator("#reference")).toHaveValue("KEEP-REF"); + await expect(page.locator("#reg_1")).toHaveValue("CONFLICT1"); + + await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click(); + await expect(page).toHaveURL(/\/admin\/12\/modules\/pos\?id=\d+&customer_id=12345679&step=2/); + }); + + test("can switch to the matched vehicle customer from the conflict modal", async ({ page }) => { + const fixture = createCustomerConflictPosFixture(); + + await mockApi(page, { + authenticated: true, + permissions: POS_PERMISSIONS, + edgeGateways: false, + pos: fixture, + }); + await primeOperatorSession(page, "pos-orders-customer-conflict-use-vehicle-token"); + + await page.goto("/admin/12/modules/pos?step=1"); + await expect(page.getByTestId("pos-step-1")).toBeVisible(); + + await page.locator("#reg_1").fill("CON"); + await selectStepOneCustomer(page, 12345679); + await page.locator("#reference").fill("MANUAL-REF"); + + await page.locator("#reg_1").fill("CONFLICT1"); + const activeConflictModal = page.locator('[data-testid="pos-desktop-customer-conflict-modal"].is-active'); + await expect(activeConflictModal).toBeVisible(); + + await activeConflictModal.getByTestId("pos-desktop-customer-conflict-use-vehicle").click(); + await expect(activeConflictModal).toHaveCount(0); + + await expect(page.locator(".pos-selected-customer__title").filter({ hasText: /\(TEST\) Conflict Fleet/ }).first()).toBeVisible(); + await expect(page.locator("#reference")).toHaveValue("CONFLICT-VEHICLE-REF"); + await expect(page.locator("#reg_1")).toHaveValue("CONFLICT1"); + + await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click(); + await expect(page).toHaveURL(/\/admin\/12\/modules\/pos\?id=\d+&customer_id=22334455&step=2/); + }); +}); + test.describe("Admin POS Orders - desktop attachment discovery", () => { test.beforeEach(async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only attachment discovery coverage"); diff --git a/tests/e2e/pos-mobile-order-flow.spec.js b/tests/e2e/pos-mobile-order-flow.spec.js index 688764d0..f7d9693d 100644 --- a/tests/e2e/pos-mobile-order-flow.spec.js +++ b/tests/e2e/pos-mobile-order-flow.spec.js @@ -139,6 +139,14 @@ async function selectCustomerFromPopup(page, customerNumber = REGULAR_CUSTOMER_I await expect(page.getByTestId("pos-mobile-customer-popup")).toBeHidden({ timeout: 10_000 }); } +async function openAddCustomerPopup(page) { + const popup = page.locator('[data-testid="pos-mobile-popup"][data-popup-id="select_customer"]'); + + await expect(page.getByTestId("pos-mobile-customer-popup")).toBeVisible({ timeout: 10_000 }); + await popup.getByText("Ny kunde", { exact: true }).click(); + await expect(page.getByTestId("pos-mobile-add-customer-popup")).toBeVisible({ timeout: 10_000 }); +} + async function waitForStepReset(page) { await expect .poll( @@ -404,6 +412,152 @@ test.describe("POS mobile order flow", () => { await expect(page.getByTestId("pos-mobile-customer-popup")).toBeVisible({ timeout: 10_000 }); }); + test("add-customer popup preserves manual edits while refreshing untouched fields from a later CVR lookup", async ({ + page, + }) => { + const fixture = createMobilePosFixture({ + cvrSearchResponses: { + "41004355": { + data: { + vat: 41004355, + status: "Normal", + name: "Truckwash ApS", + address: "Letland Alle 2", + zipcode: 2630, + city: "Taastrup", + phone: "21754690", + email: "mikkel@truckwash.dk", + }, + }, + "43423010": { + data: { + vat: 43423010, + status: "Normal", + name: "Wash Group ApS", + address: "Nordhavn 4", + zipcode: 2100, + city: "Kobenhavn O", + phone: "42331128", + email: "billing@wash-group.test", + }, + }, + }, + }); + + await setupMobilePosPage(page, fixture, { + token: "mobile-add-customer-reset-fix-token", + seedState: { + customerId: null, + reg: "FREE123", + reference: "ADD-CUSTOMER-REF", + includePrimaryItem: false, + vehicleType: null, + lastOrderId: null, + }, + route: { + step: 1, + }, + }); + + await page.getByTestId("pos-mobile-next-step").click(); + await openAddCustomerPopup(page); + + const searchInput = page.getByTestId("pos-mobile-add-customer-search-input"); + const companyPhone = page.getByTestId("pos-mobile-add-customer-company-phone"); + const invoiceEmail = page.getByTestId("pos-mobile-add-customer-invoice-email"); + const contactEmail = page.getByTestId("pos-mobile-add-customer-contact-email"); + const contactPhone = page.getByTestId("pos-mobile-add-customer-contact-phone"); + const submitButton = page.getByTestId("pos-mobile-add-customer-submit"); + + await searchInput.fill("41004355"); + await expect(companyPhone).toHaveValue("21754690", { timeout: 10_000 }); + await expect(invoiceEmail).toHaveValue("mikkel@truckwash.dk"); + await expect(contactEmail).toHaveValue("mikkel@truckwash.dk"); + await expect(contactPhone).toHaveValue("21754690"); + await expect(submitButton).toBeVisible({ timeout: 10_000 }); + + await companyPhone.fill("55550000"); + await contactEmail.fill("dispatch@truckwash.test"); + + await searchInput.fill("43423010"); + + await expect(companyPhone).toHaveValue("55550000", { timeout: 10_000 }); + await expect(contactEmail).toHaveValue("dispatch@truckwash.test"); + await expect(invoiceEmail).toHaveValue("billing@wash-group.test"); + await expect(contactPhone).toHaveValue("42331128"); + await expect(submitButton).toBeVisible({ timeout: 10_000 }); + }); + + test("add-customer popup ignores stale delayed CVR responses after a newer lookup resolves", async ({ page }) => { + const fixture = createMobilePosFixture({ + cvrSearchResponses: { + "41004355": { + delayMs: 700, + data: { + vat: 41004355, + status: "Normal", + name: "Truckwash ApS", + address: "Letland Alle 2", + zipcode: 2630, + city: "Taastrup", + phone: "21754690", + email: "mikkel@truckwash.dk", + }, + }, + "43423010": { + data: { + vat: 43423010, + status: "Normal", + name: "Wash Group ApS", + address: "Nordhavn 4", + zipcode: 2100, + city: "Kobenhavn O", + phone: "42331128", + email: "billing@wash-group.test", + }, + }, + }, + }); + + await setupMobilePosPage(page, fixture, { + token: "mobile-add-customer-stale-search-token", + seedState: { + customerId: null, + reg: "FREE123", + reference: "ADD-CUSTOMER-RACE-REF", + includePrimaryItem: false, + vehicleType: null, + lastOrderId: null, + }, + route: { + step: 1, + }, + }); + + await page.getByTestId("pos-mobile-next-step").click(); + await openAddCustomerPopup(page); + + const searchInput = page.getByTestId("pos-mobile-add-customer-search-input"); + const companyPhone = page.getByTestId("pos-mobile-add-customer-company-phone"); + const invoiceEmail = page.getByTestId("pos-mobile-add-customer-invoice-email"); + const contactPhone = page.getByTestId("pos-mobile-add-customer-contact-phone"); + + await searchInput.fill("41004355"); + await page.waitForTimeout(350); + await searchInput.fill("43423010"); + + await expect(companyPhone).toHaveValue("42331128", { timeout: 10_000 }); + await expect(invoiceEmail).toHaveValue("billing@wash-group.test"); + await expect(contactPhone).toHaveValue("42331128"); + + await page.waitForTimeout(900); + + await expect(companyPhone).toHaveValue("42331128"); + await expect(invoiceEmail).toHaveValue("billing@wash-group.test"); + await expect(contactPhone).toHaveValue("42331128"); + await expect.poll(() => fixture.requestCounters.cvrSearchGet, { timeout: 10_000 }).toBe(2); + }); + test("transaction history keeps each wash visually separated with a border", async ({ page }) => { const todayDate = new Date().toISOString().split("T")[0]; const fixture = createMobilePosFixture({ diff --git a/tests/e2e/support/mobilePos.js b/tests/e2e/support/mobilePos.js index 55fb27c6..a5d1ad10 100644 --- a/tests/e2e/support/mobilePos.js +++ b/tests/e2e/support/mobilePos.js @@ -280,6 +280,8 @@ function createRequestCounters(overrides = {}) { attachmentsDelete: 0, attachmentUpload: 0, attachmentDownload: 0, + cvrSearchGet: 0, + customerRegistrationPost: 0, lprPost: 0, ...overrides, }; @@ -297,6 +299,8 @@ function createRequestLog(overrides = {}) { bookingOrderAssignments: [], attachmentUploads: [], attachmentDeletes: [], + cvrSearches: [], + customerRegistrations: [], ...overrides, }; } @@ -444,6 +448,32 @@ function buildDefaultFixture() { { id: 41, name: productById[41].name, price: productById[41].price, quantity: 1 }, ], }); + const defaultCvrSearchResponses = { + "41004355": { + data: { + vat: 41004355, + status: "Normal", + name: "Truckwash ApS", + address: "Letland Alle 2", + zipcode: 2630, + city: "Taastrup", + phone: "21754690", + email: "mikkel@truckwash.dk", + }, + }, + "43423010": { + data: { + vat: 43423010, + status: "Normal", + name: "Wash Group ApS", + address: "Nordhavn 4", + zipcode: 2100, + city: "Kobenhavn O", + phone: "42331128", + email: "billing@wash-group.test", + }, + }, + }; return { departmentId: DEFAULT_DEPARTMENT_ID, @@ -622,6 +652,13 @@ function buildDefaultFixture() { 8102: nonWashBooking, 8103: safetySealBooking, }, + cvrSearchResponses: defaultCvrSearchResponses, + customerRegistrationResponse: { + status: 200, + data: { + success: true, + }, + }, pendingBookings: null, bookingStatusById: { [DEFAULT_BOOKING_ID]: "pending", @@ -653,6 +690,8 @@ function normalizeFixture(fixture) { fixture.attachmentsByOrderId = mergeObjectMaps({}, fixture.attachmentsByOrderId); fixture.paymentIntentsByOrderId = mergeObjectMaps({}, fixture.paymentIntentsByOrderId); fixture.bookingsById = mergeObjectMaps({}, fixture.bookingsById); + fixture.cvrSearchResponses = mergeObjectMaps({}, fixture.cvrSearchResponses); + fixture.customerRegistrationResponse = clone(fixture.customerRegistrationResponse || { status: 200, data: { success: true } }); fixture.deletedOrderIds = Array.isArray(fixture.deletedOrderIds) ? fixture.deletedOrderIds : []; fixture.markCompletedOrderIds = Array.isArray(fixture.markCompletedOrderIds) ? fixture.markCompletedOrderIds : []; @@ -695,6 +734,8 @@ export function createMobilePosFixture(overrides = {}) { attachmentsByOrderId: mergeObjectMaps(base.attachmentsByOrderId, overrides.attachmentsByOrderId), paymentIntentsByOrderId: mergeObjectMaps(base.paymentIntentsByOrderId, overrides.paymentIntentsByOrderId), bookingsById: mergeObjectMaps(base.bookingsById, overrides.bookingsById), + cvrSearchResponses: mergeObjectMaps(base.cvrSearchResponses, overrides.cvrSearchResponses), + customerRegistrationResponse: clone(overrides.customerRegistrationResponse ?? base.customerRegistrationResponse), bookingStatusById: mergeObjectMaps(base.bookingStatusById, overrides.bookingStatusById), deletedOrderIds: clone(overrides.deletedOrderIds ?? base.deletedOrderIds), markCompletedOrderIds: clone(overrides.markCompletedOrderIds ?? base.markCompletedOrderIds), @@ -1298,6 +1339,90 @@ export async function mockMobilePosApi(page, fixture) { return; } + if (pathname.endsWith("/cvr/search") && method === "GET") { + recordCounter(fixture, "cvrSearchGet"); + const query = String(parsedUrl.searchParams.get("query") || "").trim(); + recordLog(fixture, "cvrSearches", { query }); + + const responseConfig = fixture.cvrSearchResponses?.[query] || null; + const delayMs = Number(responseConfig?.delayMs ?? 0); + const status = Number(responseConfig?.status ?? 200); + + if (delayMs > 0) { + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + + if (status >= 400) { + await route.fulfill( + json( + { + success: false, + data: { + message: responseConfig?.message || "Unable to load CVR details", + }, + }, + status + ) + ); + return; + } + + await route.fulfill( + json({ + success: true, + data: clone(responseConfig?.data ?? null), + }) + ); + return; + } + + if (pathname.endsWith("/auth/register/cvr") && method === "POST") { + recordCounter(fixture, "customerRegistrationPost"); + recordLog(fixture, "customerRegistrations", body); + + const responseConfig = fixture.customerRegistrationResponse || {}; + const status = Number(responseConfig.status ?? 200); + + if (status >= 400) { + await route.fulfill( + json( + { + success: false, + data: { + message: responseConfig.message || "Unable to register customer", + }, + }, + status + ) + ); + return; + } + + const customerNumber = toPositiveInteger(body.companyPhone); + if (customerNumber) { + fixture.customersByNumber[customerNumber] = createCustomer(customerNumber, { + name: body?.searchResult?.name || `Customer ${customerNumber}`, + address: body?.searchResult?.address || "Demo Street 1", + zip: String(body?.searchResult?.zipcode || "2630"), + city: body?.searchResult?.city || "Taastrup", + mobilePhone: String(body.contactPhone || body.companyPhone || ""), + email: body.contactEmail || body.invoiceEmail || "", + corporateIdentificationNumber: String(body.cvr || "").padStart(8, "0"), + economic_customer: customerNumber, + }); + fixture.customerAttributesByNumber[customerNumber] = fixture.customerAttributesByNumber[customerNumber] || []; + fixture.customerNotesByNumber[customerNumber] = fixture.customerNotesByNumber[customerNumber] || []; + } + + await route.fulfill( + json({ + success: true, + data: clone(responseConfig.data ?? { success: true }), + }) + ); + return; + } + if (pathname.endsWith("/users/customer") && method === "GET") { recordCounter(fixture, "usersCustomerGet"); const customerNumber = diff --git a/tests/unit/pos-mobile-add-customer-autofill.spec.js b/tests/unit/pos-mobile-add-customer-autofill.spec.js new file mode 100644 index 00000000..fa60f2e6 --- /dev/null +++ b/tests/unit/pos-mobile-add-customer-autofill.spec.js @@ -0,0 +1,147 @@ +import { describe, expect, it } from "vitest"; + +import { + applyPosAddCustomerSearchResult, + createPosAddCustomerFields, + createPosAddCustomerManualOverrides, + createPosAddCustomerRequestState, + nextPosAddCustomerRequestState, + resolvePosAddCustomerRequestState, + setPosAddCustomerManualOverride, +} from "@/components/displays/department/pos/steps/mobile/elements/posAddCustomerAutofill.js"; + +function createSearchResult(overrides = {}) { + return { + vat: 41004355, + name: "Truckwash ApS", + phone: "21754690", + email: "mikkel@truckwash.dk", + ...overrides, + }; +} + +describe("POS mobile add-customer autofill", () => { + it("fills all eligible fields on the initial successful lookup", () => { + const nextFields = applyPosAddCustomerSearchResult({ + fields: createPosAddCustomerFields(), + manualOverrides: createPosAddCustomerManualOverrides(), + searchResult: createSearchResult(), + notFoundLabel: "Not found", + }); + + expect(nextFields).toEqual({ + cvr: "41004355", + companyPhone: "21754690", + invoiceEmail: "mikkel@truckwash.dk", + contactEmail: "mikkel@truckwash.dk", + contactPhone: "21754690", + }); + }); + + it("updates only untouched fields when a later lookup resolves", () => { + const initialFields = applyPosAddCustomerSearchResult({ + fields: createPosAddCustomerFields(), + manualOverrides: createPosAddCustomerManualOverrides(), + searchResult: createSearchResult(), + notFoundLabel: "Not found", + }); + + const nextFields = applyPosAddCustomerSearchResult({ + fields: { + ...initialFields, + companyPhone: "55550000", + }, + manualOverrides: createPosAddCustomerManualOverrides({ + companyPhone: true, + }), + searchResult: createSearchResult({ + vat: 43423010, + name: "Wash Group ApS", + phone: "42331128", + email: "billing@wash-group.test", + }), + notFoundLabel: "Not found", + }); + + expect(nextFields).toEqual({ + cvr: "43423010", + companyPhone: "55550000", + invoiceEmail: "billing@wash-group.test", + contactEmail: "billing@wash-group.test", + contactPhone: "42331128", + }); + }); + + it("preserves manually edited fields across later autofill passes", () => { + const manualOverrides = createPosAddCustomerManualOverrides({ + companyPhone: true, + contactEmail: true, + }); + + const nextFields = applyPosAddCustomerSearchResult({ + fields: createPosAddCustomerFields({ + cvr: "41004355", + companyPhone: "99998888", + invoiceEmail: "old-invoice@example.com", + contactEmail: "dispatch@example.com", + contactPhone: "21754690", + }), + manualOverrides, + searchResult: createSearchResult({ + vat: 43423010, + phone: "42331128", + email: "billing@wash-group.test", + }), + notFoundLabel: "Not found", + }); + + expect(nextFields.companyPhone).toBe("99998888"); + expect(nextFields.contactEmail).toBe("dispatch@example.com"); + expect(nextFields.invoiceEmail).toBe("billing@wash-group.test"); + expect(nextFields.contactPhone).toBe("42331128"); + expect(nextFields.cvr).toBe("43423010"); + }); + + it("refills a field after the operator clears a manual override", () => { + const withManualEdit = setPosAddCustomerManualOverride( + createPosAddCustomerManualOverrides({ + companyPhone: true, + }), + "companyPhone", + "" + ); + + expect(withManualEdit.companyPhone).toBe(false); + + const nextFields = applyPosAddCustomerSearchResult({ + fields: createPosAddCustomerFields({ + companyPhone: "", + }), + manualOverrides: withManualEdit, + searchResult: createSearchResult({ + phone: "42331128", + }), + notFoundLabel: "Not found", + }); + + expect(nextFields.companyPhone).toBe("42331128"); + }); + + it("rejects slower stale responses after a newer lookup wins", () => { + const initialState = createPosAddCustomerRequestState(); + const firstRequest = nextPosAddCustomerRequestState(initialState); + const secondRequest = nextPosAddCustomerRequestState(firstRequest.state); + + const staleResolution = resolvePosAddCustomerRequestState(secondRequest.state, firstRequest.requestId); + const latestResolution = resolvePosAddCustomerRequestState(secondRequest.state, secondRequest.requestId); + + expect(staleResolution.shouldApply).toBe(false); + expect(staleResolution.state).toEqual(secondRequest.state); + + expect(latestResolution.shouldApply).toBe(true); + expect(latestResolution.state).toEqual({ + latestRequestedId: secondRequest.requestId, + latestAppliedId: secondRequest.requestId, + }); + }); +}); diff --git a/vite.config.js b/vite.config.js index ea6d9d1a..deb2df70 100644 --- a/vite.config.js +++ b/vite.config.js @@ -7,6 +7,21 @@ import { VitePWA } from 'vite-plugin-pwa' import VueJsx from '@vitejs/plugin-vue-jsx' import { execSync } from 'node:child_process' +const BUEFY_CSS_REPLACEMENTS = new Map([ + [ + '@media screen and (max-width: calc(var(--bulma-sidebar-mobile-breakpoint) - 1px))', + '@media screen and (max-width: 768px)' + ], + [ + '@media screen and (max-width: var(--bulma-steps-mobile-max-width)-1px)', + '@media screen and (max-width: 768px)' + ], + [ + 'var(--bulma-$steps-divider-height)', + 'var(--bulma-steps-divider-height)' + ] +]) + function getGitCommit() { try { return execSync('git rev-parse --short HEAD').toString().trim() @@ -15,6 +30,36 @@ function getGitCommit() { } } +function patchBuefyCssMediaQuery() { + return { + name: 'patch-buefy-css-media-query', + enforce: 'pre', + transform(code, id) { + if (!id.includes('buefy/dist/css/buefy.css')) { + return null + } + + let nextCode = code + let didPatch = false + + for (const [invalidSnippet, fixedSnippet] of BUEFY_CSS_REPLACEMENTS.entries()) { + if (!nextCode.includes(invalidSnippet)) { + continue + } + + nextCode = nextCode.replaceAll(invalidSnippet, fixedSnippet) + didPatch = true + } + + if (!didPatch) { + return null + } + + return nextCode + } + } +} + export default defineConfig(({ mode }) => { const isProd = mode === 'production' const isPlaywrightRuntime = process.env.PLAYWRIGHT === '1' @@ -55,6 +100,7 @@ export default defineConfig(({ mode }) => { return { base, plugins: [ + patchBuefyCssMediaQuery(), vue(), VueJsx(), !isProd && !isPlaywrightRuntime && vueDevTools(),