Merge pull request #35 from copenhagentruckwash/buefy-integration
buefy-integration
@@ -26,6 +26,7 @@
|
||||
"@vueuse/integrations": "^13.7.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.7.8",
|
||||
"buefy": "^3.0.3",
|
||||
"bulma": "^1.0.2",
|
||||
"bulma-block-list": "^1.1.0",
|
||||
"bulma-calendar": "^7.1.1",
|
||||
@@ -902,6 +903,7 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
|
||||
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -2896,6 +2898,7 @@
|
||||
"version": "6.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.17.tgz",
|
||||
"integrity": "sha512-0W7lnIrv18ruJ5zeWBeNZXO8qCWlzxDdp9COFEsZnyNjiEhUVnrW/dPbjRKYpL0edGG0/Lhs0ghp1z/5ekt8ZA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"preact": "~10.12.1"
|
||||
}
|
||||
@@ -4012,6 +4015,7 @@
|
||||
"version": "0.22.12",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz",
|
||||
"integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/core": "^0.22.12"
|
||||
}
|
||||
@@ -4540,6 +4544,7 @@
|
||||
"version": "0.22.12",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz",
|
||||
"integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -4551,6 +4556,7 @@
|
||||
"version": "0.22.12",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz",
|
||||
"integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -4573,6 +4579,7 @@
|
||||
"version": "0.22.12",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz",
|
||||
"integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12",
|
||||
"tinycolor2": "^1.6.0"
|
||||
@@ -4613,6 +4620,7 @@
|
||||
"version": "0.22.12",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz",
|
||||
"integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -4853,6 +4861,7 @@
|
||||
"version": "0.22.12",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz",
|
||||
"integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -4864,6 +4873,7 @@
|
||||
"version": "0.22.12",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz",
|
||||
"integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -4878,6 +4888,7 @@
|
||||
"version": "0.22.12",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz",
|
||||
"integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -8249,6 +8260,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8272,6 +8284,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -14254,6 +14267,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001688",
|
||||
"electron-to-chromium": "^1.5.73",
|
||||
@@ -14267,6 +14281,18 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/buefy": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buefy/-/buefy-3.0.3.tgz",
|
||||
"integrity": "sha512-//QfIDgbhmpn+mFszD/lEwhlphA+hQavJED2XwjlzqS/bpxYjk/Rnj36X08QVbqiH3lt4a+G357uzsZczc7JTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bulma": "^1.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
@@ -14323,9 +14349,11 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
|
||||
},
|
||||
"node_modules/bulma": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.2.tgz",
|
||||
"integrity": "sha512-D7GnDuF6seb6HkcnRMM9E739QpEY9chDzzeFrHMyEns/EXyDJuQ0XA0KxbBl/B2NTsKSoDomW61jFGFaAxhK5A=="
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
|
||||
"integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/bulma-block-list": {
|
||||
"version": "1.1.0",
|
||||
@@ -14540,7 +14568,8 @@
|
||||
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
|
||||
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/chardet": {
|
||||
"version": "0.7.0",
|
||||
@@ -14551,6 +14580,7 @@
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz",
|
||||
"integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
@@ -15427,6 +15457,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -15592,6 +15623,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz",
|
||||
"integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==",
|
||||
"deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-sfc": "2.7.16",
|
||||
"csstype": "^3.1.0"
|
||||
@@ -16561,6 +16593,7 @@
|
||||
"integrity": "sha512-/XxRRR90gNSuNf++w1jOQjhC5LE9Ixf/iAQctVb/miEI3dwzPZTuG27/omoh5REfSLDoPXofM84vAH/ULtz35g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/snapshot": "^3.2.4",
|
||||
"deep-eql": "^5.0.2",
|
||||
@@ -18055,7 +18088,8 @@
|
||||
"node_modules/idb-keyval": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz",
|
||||
"integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="
|
||||
"integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
@@ -18918,6 +18952,7 @@
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
@@ -19927,6 +19962,7 @@
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
@@ -21232,6 +21268,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",
|
||||
@@ -21916,6 +21953,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
|
||||
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -22128,6 +22166,7 @@
|
||||
"version": "1.81.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.81.1.tgz",
|
||||
"integrity": "sha512-VNLgf4FC5yFyKwAumAAwwNh8X4SevlVREq3Y8aDZIkm0lI/zO1feycMXQ4hn+eB6FVhRbleSQ1Yb/q8juSldTA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
@@ -22148,6 +22187,7 @@
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.81.0.tgz",
|
||||
"integrity": "sha512-uZQ2Faxb1oWBHpeSSzjxnhClbMb3QadN0ql0ZFNuqWOLUxwaVhrMlMhPq6TDPbbfDUjihuwrMCuy695Bgna5RA==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.0.0",
|
||||
"buffer-builder": "^0.2.0",
|
||||
@@ -24847,6 +24887,7 @@
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
||||
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
@@ -25034,6 +25075,7 @@
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
||||
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
@@ -26371,6 +26413,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode development",
|
||||
"preview": "vite preview",
|
||||
"test:mobile": "wdio run wdio.conf.js --exclude safari",
|
||||
"test:android": "wdio run wdio.conf.js --spec test/specs/*.spec.js",
|
||||
@@ -30,6 +31,7 @@
|
||||
"@vueuse/integrations": "^13.7.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.7.8",
|
||||
"buefy": "^3.0.3",
|
||||
"bulma": "^1.0.2",
|
||||
"bulma-block-list": "^1.1.0",
|
||||
"bulma-calendar": "^7.1.1",
|
||||
|
||||
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,46 @@
|
||||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
|
||||
#################################################################################
|
||||
# WARNING: Do not store sensitive information in this file, #
|
||||
# as its contents will be included in the Qodana report. #
|
||||
#################################################################################
|
||||
version: "1.0"
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
#exclude:
|
||||
# - name: <SomeDisabledInspectionId>
|
||||
# paths:
|
||||
# - <path/where/not/run/inspection>
|
||||
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
# Quality gate. Will fail the CI/CD pipeline if any condition is not met
|
||||
# severityThresholds - configures maximum thresholds for different problem severities
|
||||
# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code
|
||||
# Code Coverage is available in Ultimate and Ultimate Plus plans
|
||||
#failureConditions:
|
||||
# severityThresholds:
|
||||
# any: 15
|
||||
# critical: 5
|
||||
# testCoverageThresholds:
|
||||
# fresh: 70
|
||||
# total: 50
|
||||
|
||||
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||
linter: jetbrains/qodana-js:2025.3
|
||||
@@ -5,6 +5,7 @@ import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
// Set the favicon to the /favicon.ico file
|
||||
import { useHead } from "@vueuse/head";
|
||||
import favicon from '@/assets/favicon.ico';
|
||||
import VersionCheck from "@/components/global/VersionCheck.vue";
|
||||
|
||||
useHead({
|
||||
link: [
|
||||
@@ -23,6 +24,7 @@ onMounted(() => {
|
||||
</header>
|
||||
<main>
|
||||
<DefaultPageWrapper>
|
||||
<VersionCheck/>
|
||||
<router-view />
|
||||
</DefaultPageWrapper>
|
||||
</main>
|
||||
|
||||
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 468 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 514 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 111 KiB |
@@ -5,6 +5,7 @@ import { showPopper, popperBoxProductAddonHtml, removePopperIfOpen } from "@/com
|
||||
import Swal from "sweetalert2";
|
||||
import CustomerProductDiscountDisplay
|
||||
from "@/components/displays/department/pos/displays/CustomerProductDiscountDisplay.vue";
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
|
||||
const expandIcon = ref(null);
|
||||
|
||||
@@ -29,6 +30,10 @@ const props = defineProps({
|
||||
customerDiscounts: Array,
|
||||
customerAttributes: Array,
|
||||
compact: Boolean,
|
||||
showPrices: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
const isAddonSelected = (productId, addonId) => {
|
||||
@@ -200,7 +205,12 @@ const orderByOrderPriority = (addons) => {
|
||||
<!-- Title -->
|
||||
<div class="p-2">
|
||||
<p class="title is-6 mb-0 text-automatic-shrink" style="font-size: small">{{ name }}</p>
|
||||
<p class="subtitle is-6 mb-0 has-text-grey-light" style="font-size: smaller"><small>579 kr</small></p>
|
||||
<template v-if="showPrices">
|
||||
<p class="subtitle is-6 mb-0 has-text-grey-light" style="font-size: smaller"><small>{{ price }} kr</small></p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="subtitle is-6 mb-0 has-text-grey-light" style="font-size: smaller"><br/></p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Price -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import {defineProps, defineEmits, onMounted, computed} from 'vue';
|
||||
import { useSlots } from "vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||
@@ -7,6 +7,7 @@ import Swal from "sweetalert2";
|
||||
import { ref } from "vue";
|
||||
import CustomerModal from "@/components/displays/modals/CustomerModal.vue";
|
||||
import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue";
|
||||
import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
||||
const emit = defineEmits(['deleted']);
|
||||
const emitDeleted = () => {
|
||||
emit('deleted');
|
||||
@@ -36,9 +37,54 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
reg_2: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
order_booking_id: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
customer_number: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
refreshFunction: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
console.warn("Refresh function is not defined");
|
||||
}
|
||||
},
|
||||
department_id: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
attachments: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
displayActionsDirectly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const customerModalVisible = ref(false);
|
||||
const userIdFromCustomerNumber = ref(null);
|
||||
const getUserId = () => {
|
||||
if (!!props.customer_number) {
|
||||
SessionUser.adminUser.customers.fromCustomerNumber.getUserId(props.customer_number).then((response) => {
|
||||
userIdFromCustomerNumber.value = response.data.data.user_id;
|
||||
console.warn(userIdFromCustomerNumber.value);
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
} else if (!!props.user_id) {
|
||||
userIdFromCustomerNumber.value = props.user_id;
|
||||
} else {
|
||||
userIdFromCustomerNumber.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
const showSetCustomerPassword = (userId) => {
|
||||
Swal.fire({
|
||||
@@ -85,163 +131,651 @@ const redirectSuperUserInvoiceCollectionPage = (invoiceCollectionId) => {
|
||||
window.open(`/superuser/invoices/${invoiceCollectionId}`, '_blank');
|
||||
};
|
||||
|
||||
const redirectDepartmentOrderPage = async (orderId) => {
|
||||
const redirectDepartmentOrderPage = async (orderId, newTab = false) => {
|
||||
// Send the user to the order page (In a new tab)
|
||||
// Get the department id from the order
|
||||
await SessionUser.objects.orders.functions.get_department_id(props.order_id).then((response) => {
|
||||
console.warn(response);
|
||||
// Get the department id from the response
|
||||
// Send the user to the order page (In a new tab)
|
||||
window.open(`/admin/${response}/modules/pos/orders/${orderId}`, '_blank');
|
||||
SessionUser.functions.redirectTo.department(response, 'modules/pos/orders/' + orderId, !!newTab);
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const hasUser = () => {
|
||||
if (userIdFromCustomerNumber.value) {
|
||||
return true;
|
||||
}
|
||||
let hasUser = false;
|
||||
if (!!props.user_id) {
|
||||
hasUser = true;
|
||||
userIdFromCustomerNumber.value = props.user_id;
|
||||
}
|
||||
if (!!props.customer_number) {
|
||||
hasUser = true;
|
||||
getUserId();
|
||||
}
|
||||
return hasUser;
|
||||
}
|
||||
|
||||
const attachmentsFromOrder = ref([]);
|
||||
const attachmentsFromOrderError = ref(null);
|
||||
onMounted(() => {
|
||||
if (props.order_id) {
|
||||
SessionUser.objects.orders.functions.fetchAttachments(props.order_id).then((response) => {
|
||||
/**
|
||||
* [
|
||||
* {
|
||||
* "id": 4396,
|
||||
* "object_type": "`orders`",
|
||||
* "object_id": 30242,
|
||||
* "content": {
|
||||
* "image": null,
|
||||
* "document": "pdf_6912ea1886f15.pdf",
|
||||
* "relation": null,
|
||||
* "other": "wash_certificate"
|
||||
* },
|
||||
* "created_at": "2025-11-11 08:47:36",
|
||||
* "updated_at": null,
|
||||
* "deleted_at": null
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
// Validate the response is an array
|
||||
if (!Array.isArray(response)) {
|
||||
response = [];
|
||||
}
|
||||
attachmentsFromOrder.value = response;
|
||||
}).catch((error) => {
|
||||
const parsedErrorMessage = SessionUser.functions.parseErrorMessage(error);
|
||||
console.warn('Error fetching attachments from order:', error, parsedErrorMessage);
|
||||
// Check if there's an error message in the response
|
||||
attachmentsFromOrderError.value = `ERROR: ${parsedErrorMessage || 'Failed to fetch attachments'}`;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const onShowImpersonationQRCode = (src, directLink) => {
|
||||
Swal.fire({
|
||||
title: 'Scan QR-koden for at logge ind som bruger',
|
||||
html: `<img src="${src}" alt="QR-kode" />`,
|
||||
showCloseButton: true,
|
||||
showConfirmButton: !!directLink,
|
||||
showCancelButton: true,
|
||||
focusConfirm: false,
|
||||
confirmButtonText: 'Kopiér link',
|
||||
cancelButtonText: 'Luk',
|
||||
preConfirm: () => {
|
||||
navigator.clipboard.writeText(directLink);
|
||||
Swal.showValidationMessage('Link kopieret til udklipsholderen');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const onClickShowImpersonationQRCode = async (userId) => {
|
||||
// Get the session token
|
||||
const newToken = await SessionUser.superUser.intimidate.getImpersonationToken(userId);
|
||||
// Create the impersonation link
|
||||
const link = `${window.location.protocol}//${window.location.host}/login/qr?token=${newToken}`;
|
||||
// Generate a QR code for the impersonation link
|
||||
const response = await SessionUser.superUser.intimidate.showImpersonationQRCode(link);
|
||||
// Show the QR code in a modal
|
||||
onShowImpersonationQRCode(response.value, link);
|
||||
}
|
||||
|
||||
const defaultActions = computed(() => {
|
||||
return [
|
||||
/**
|
||||
* Example action
|
||||
{
|
||||
icon: 'fas fa-eye',
|
||||
label: 'View Order',
|
||||
clickAction: () => {
|
||||
console.warn('View Order clicked');
|
||||
},
|
||||
showFunction: () => {
|
||||
return true; // Logic to determine if action should be shown
|
||||
},
|
||||
disabled: false,
|
||||
template: 'success' // Optional: Set the template to 'success' or 'danger'
|
||||
isLabel: false // Optional: Set to true to render the label as a button
|
||||
}
|
||||
*/
|
||||
/** Bookings actions */
|
||||
{
|
||||
label: 'Booking',
|
||||
isLabel: true,
|
||||
showFunction: () => {
|
||||
return !!props.order_booking_id;
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-check-circle',
|
||||
label: 'Marker som gennemført',
|
||||
template: 'success',
|
||||
clickAction: () => {
|
||||
return SessionUser.objects.order_bookings.functions.showCompleteConfirmationModal(props.order_booking_id, () => {
|
||||
props.refreshFunction();
|
||||
});
|
||||
},
|
||||
showFunction: () => {
|
||||
return !!props.order_booking_id && SessionUser.canAccessAdmin() && !props.order_id;
|
||||
},
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-external-link-alt',
|
||||
label: 'Se booking i ny fane',
|
||||
clickAction: () => {
|
||||
return SessionUser.functions.redirectTo.department(props.department_id, 'modules/bookings/order/' + props.order_booking_id, true);
|
||||
},
|
||||
showFunction: () => {
|
||||
return !!props.order_booking_id;
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-edit',
|
||||
label: 'Ændr tilknytning',
|
||||
clickAction: () => {
|
||||
return SessionUser.objects.order_bookings.showEditObjectFieldForm(props.order_booking_id, 'order_id', props.order_id, () => {
|
||||
props.refreshFunction();
|
||||
}, {
|
||||
filters: {
|
||||
...(props.customer_number ? {customer_id: props.customer_number} : {}),
|
||||
...(props.department_id ? {department_id: props.department_id} : {})
|
||||
},
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: 100
|
||||
}
|
||||
});
|
||||
},
|
||||
showFunction: () => {
|
||||
return !!props.order_booking_id && SessionUser.canAccessAdmin();
|
||||
},
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-trash-alt',
|
||||
label: 'Slet booking',
|
||||
clickAction: () => {
|
||||
return SessionUser.objects.order_bookings.functions.showDeleteConfirmationModal(props.order_booking_id, () => {
|
||||
props.refreshFunction();
|
||||
});
|
||||
},
|
||||
showFunction: () => {
|
||||
return !!props.order_booking_id && SessionUser.canAccessAdmin() && !props.order_id;
|
||||
},
|
||||
disabled: false,
|
||||
template: 'danger'
|
||||
},
|
||||
/** Orders actions */
|
||||
{
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.orders.meta.labels.single),
|
||||
isLabel: true,
|
||||
showFunction: () => {
|
||||
return !!props.order_id;
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-external-link-alt',
|
||||
label: 'Se ordre i ny fane',
|
||||
clickAction: async () => {
|
||||
return redirectDepartmentOrderPage(props.order_id, true);
|
||||
},
|
||||
showFunction: () => {
|
||||
return !!props.order_id && (SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser());
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-paperclip',
|
||||
label: 'Vedhæft vaskecertifikat',
|
||||
clickAction: () => {
|
||||
return SessionUser.objects.orders.functions.showAttachWashCertificateForm(props.order_id, () => {
|
||||
props.refreshFunction();
|
||||
});
|
||||
},
|
||||
showFunction: () => {
|
||||
return !!props.order_id && (SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser());
|
||||
},
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-user-edit',
|
||||
label: 'Skift kunde',
|
||||
clickAction: () => {
|
||||
return SessionUser.objects.orders.functions.showChangeCustomerForm(props.order_id);
|
||||
},
|
||||
showFunction: () => {
|
||||
return !!props.order_id && SessionUser.canAccessSuperUser();
|
||||
},
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-file-invoice-dollar',
|
||||
label: 'Skift faktura samling',
|
||||
clickAction: () => {
|
||||
return SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm(props.order_id);
|
||||
},
|
||||
showFunction: () => {
|
||||
return !!props.order_id && SessionUser.canAccessSuperUser();
|
||||
},
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-download',
|
||||
label: 'Download faktura',
|
||||
clickAction: () => {
|
||||
return SessionUser.objects.collectedOrderInvoices.functions.download(props.invoice_collection_id);
|
||||
},
|
||||
showFunction: () => {
|
||||
return true;
|
||||
},
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-trash-alt',
|
||||
label: 'Slet ordre',
|
||||
clickAction: () => {
|
||||
return SessionUser.objects.orders.functions.showDeleteConfirmationModal(props.order_id, () => {
|
||||
emitDeleted();
|
||||
});
|
||||
},
|
||||
showFunction: () => {
|
||||
return !!props.order_id && (SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser());
|
||||
},
|
||||
disabled: false,
|
||||
template: 'danger'
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-file-invoice-dollar',
|
||||
label: 'Vis faktura samling i ny fane',
|
||||
clickAction: () => {
|
||||
return redirectSuperUserInvoiceCollectionPage(props.invoice_collection_id);
|
||||
},
|
||||
showFunction: () => {
|
||||
return !!props.invoice_collection_id && SessionUser.canAccessSuperUser();
|
||||
},
|
||||
},
|
||||
/** Vehicle actions */
|
||||
{
|
||||
label: props.reg_1 && props.reg_2 ? `${SessionUser.objects.vehicles.meta.labels.multiple}` : `${SessionUser.objects.vehicles.meta.labels.single}`,
|
||||
isLabel: true,
|
||||
showFunction: () => {
|
||||
return (!!props.reg_1 || !!props.reg_2) && SessionUser.canAccessSuperUser();
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-car',
|
||||
label: `Se køretøj ${props.reg_1 ? props.reg_1 : '-'} i ny fane`,
|
||||
clickAction: () => {
|
||||
return SessionUser.functions.redirectTo.superUser('/vehicles/' + props.reg_1, true);
|
||||
},
|
||||
showFunction: () => {
|
||||
return !!props.reg_1 && SessionUser.canAccessSuperUser();
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-car',
|
||||
label: `Se køretøj ${props.reg_2 ? props.reg_2 : '-'} i ny fane`,
|
||||
clickAction: () => {
|
||||
return SessionUser.functions.redirectTo.superUser('/vehicles/' + props.reg_2, true);
|
||||
},
|
||||
showFunction: () => {
|
||||
return !!props.reg_2 && SessionUser.canAccessSuperUser();
|
||||
},
|
||||
},
|
||||
/** User actions */
|
||||
{ label: SessionUser.objects.global.language.customer,
|
||||
isLabel: true,
|
||||
showFunction: () => {
|
||||
return hasUser() && SessionUser.canAccessSuperUser();
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-user-edit',
|
||||
label: 'Se kunde i ny fane',
|
||||
clickAction: () => {
|
||||
return SessionUser.functions.redirectTo.superUser('/users/' + userIdFromCustomerNumber.value, true);
|
||||
},
|
||||
showFunction: () => {
|
||||
return hasUser() && SessionUser.canAccessSuperUser() && userIdFromCustomerNumber.value;
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-user-shield',
|
||||
label: 'Log ind som kunde',
|
||||
clickAction: () => {
|
||||
return SessionUser.superUser.intimidate.intimidateUser(userIdFromCustomerNumber.value);
|
||||
},
|
||||
showFunction: () => {
|
||||
return hasUser() && SessionUser.canAccessSuperUser();
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-qrcode',
|
||||
label: 'Vis QR-kode',
|
||||
clickAction: () => {
|
||||
return onClickShowImpersonationQRCode(userIdFromCustomerNumber.value);
|
||||
},
|
||||
showFunction: () => {
|
||||
return hasUser() && SessionUser.canAccessSuperUser();
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-key',
|
||||
label: 'Skift kodeord',
|
||||
clickAction: () => {
|
||||
return showSetCustomerPassword(userIdFromCustomerNumber.value);
|
||||
},
|
||||
showFunction: () => {
|
||||
return hasUser() && SessionUser.canAccessSuperUser();
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-user',
|
||||
label: 'Vis kunde',
|
||||
clickAction: () => {
|
||||
customerModalVisible.value = true;
|
||||
},
|
||||
showFunction: () => {
|
||||
return hasUser() && SessionUser.canAccessSuperUser();
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown is-right is-hoverable">
|
||||
<div class="dropdown-trigger">
|
||||
<button class="button is-small is-dark" aria-haspopup="true" aria-controls="dropdown-menu">
|
||||
<span class="icon">
|
||||
<i :class="props.icon"></i>
|
||||
</span>
|
||||
<span v-if="props.label.length > 0" class="ml-2">{{ props.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<!-- Actions (If defined) -->
|
||||
<template v-if="useSlots().actions">
|
||||
<slot name="actions"></slot>
|
||||
<!-- If there's a user_id, show the user actions -->
|
||||
<template v-if="props.user_id">
|
||||
<!-- Label -->
|
||||
<ActionSettingsWheelItemLabel
|
||||
:label="SessionUser.objects.global.language.customer"
|
||||
<div>
|
||||
<template v-if="displayActionsDirectly">
|
||||
<!-- Error -->
|
||||
<template v-if="attachmentsFromOrderError">
|
||||
<div class="message is-danger">
|
||||
<div class="message-body">
|
||||
{{ attachmentsFromOrderError }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-for="(action, index) in [ ...defaultActions, ...(useSlots().actions ? [] : []) ]" :key="index">
|
||||
<ActionSettingsWheelItem
|
||||
v-if="(!action.showFunction || action.showFunction()) && (!action.isLabel || action.isLabel === false)"
|
||||
:icon="action.icon"
|
||||
:label="action.label"
|
||||
:template="action.template"
|
||||
:click-action="action.clickAction"
|
||||
:show-function="action.showFunction"
|
||||
:disabled="action.disabled"
|
||||
/>
|
||||
<ActionSettingsWheelItemLabel
|
||||
v-else-if="action.isLabel && action.isLabel === true && (!action.showFunction || action.showFunction())"
|
||||
:label="action.label"
|
||||
/>
|
||||
</template>
|
||||
<!-- If there are attachments, show the attachments -->
|
||||
<template v-if="attachmentsFromOrder.length > 0">
|
||||
<ActionSettingsWheelItemLabel :label="'Vedhæftede filer'"/>
|
||||
<ActionSettingsWheelItem
|
||||
v-for="attachment in attachmentsFromOrder"
|
||||
:key="attachment.id"
|
||||
:icon="'fas fa-paperclip'"
|
||||
:label="`Åbn vedhæftede fil #${attachment.id}`"
|
||||
:click-action="() => SessionUser.objects.orders.functions.downloadAttachment(props.order_id, attachment.id, true)
|
||||
.catch((error) => {
|
||||
Swal.fire({
|
||||
title: 'Fejl',
|
||||
text: 'Der opstod en fejl under download af vedhæftede fil.',
|
||||
icon: 'error',
|
||||
});
|
||||
})"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<div class="dropdown is-right is-hoverable" v-else>
|
||||
<div class="dropdown-trigger">
|
||||
<button class="button is-small is-dark" aria-haspopup="true" aria-controls="dropdown-menu">
|
||||
<span class="icon">
|
||||
<i :class="props.icon"></i>
|
||||
</span>
|
||||
<span v-if="props.label.length > 0" class="ml-2">{{ props.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<!-- Actions (If defined) -->
|
||||
<template v-if="useSlots().actions">
|
||||
<slot name="actions"></slot>
|
||||
<!-- If there is an order_booking_id, show the order booking actions -->
|
||||
<template v-if="props.order_booking_id">
|
||||
<ActionSettingsWheelItemLabel :label="'Booking'"/>
|
||||
<ActionSettingsWheelItem
|
||||
v-if="(SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()) && !props.order_id"
|
||||
:icon="'fas fa-check-circle'"
|
||||
:label="'Marker som gennemført'"
|
||||
template="success"
|
||||
:click-action="() => SessionUser.objects.order_bookings.functions.showCompleteConfirmationModal(props.order_booking_id, () => {
|
||||
props.refreshFunction();
|
||||
})"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
:icon="'fas fa-external-link-alt'"
|
||||
:label="'Se booking i ny fane'"
|
||||
:click-action="() => SessionUser.functions.redirectTo.department(props.department_id, 'modules/bookings/order/' + props.order_booking_id, true)"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
v-if="SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()"
|
||||
:icon="'fas fa-edit'"
|
||||
:label="(!props.order_id) ? `Tilknyt ${SessionUser.objects.orders.meta.labels.single.toLowerCase()}` : `Ændr ${SessionUser.objects.orders.meta.labels.single.toLowerCase()}`"
|
||||
:click-action="() => SessionUser.objects.order_bookings.showEditObjectFieldForm(props.order_booking_id, 'order_id', props.order_id, () => {
|
||||
props.refreshFunction();
|
||||
}, {
|
||||
filters: {
|
||||
...(customer_number ? {customer_id: props.customer_number} : {}),
|
||||
...(props.department_id ? {department_id: props.department_id} : {})
|
||||
},
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: 100
|
||||
}
|
||||
})"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
:icon="'fas fa-trash-alt'"
|
||||
:label="'Slet booking'"
|
||||
template="danger"
|
||||
v-show="!props.order_id"
|
||||
:click-action="() => SessionUser.objects.order_bookings.functions.showDeleteConfirmationModal(props.order_booking_id, () => {
|
||||
props.refreshFunction();
|
||||
})"
|
||||
/>
|
||||
</template>
|
||||
<!-- If there's an order_id, show the order actions -->
|
||||
<template v-if="props.order_id">
|
||||
<!-- Label -->
|
||||
<action-settings-wheel-item-label
|
||||
:label="SessionUser.functions.ucFirst(SessionUser.objects.orders.meta.labels.single)"
|
||||
/>
|
||||
<!-- Go to the order page, in a new tab -->
|
||||
<action-settings-wheel-item
|
||||
v-if="SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()"
|
||||
:label="`Se ${SessionUser.objects.orders.meta.labels.single.toLowerCase()} i ny fane`"
|
||||
icon="fas fa-external-link-alt"
|
||||
:click-action="async () => redirectDepartmentOrderPage(props.order_id, true)"
|
||||
:disabled="false"
|
||||
/>
|
||||
<!-- Go to the user/order/:order_id page, in a new tab -->
|
||||
<action-settings-wheel-item
|
||||
v-if="!SessionUser.canAccessAdmin() && !SessionUser.canAccessSuperUser()"
|
||||
:label="`Se ${SessionUser.objects.orders.meta.labels.single.toLowerCase()} i ny fane`"
|
||||
icon="fas fa-external-link-alt"
|
||||
:click-action="() => SessionUser.functions.redirectTo.user('/orders/' + props.order_id, true)"
|
||||
:disabled="false"
|
||||
/>
|
||||
<!-- Attach wash certificate -->
|
||||
<action-settings-wheel-item
|
||||
v-if="SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()"
|
||||
label="Vedhæft vaskecertifikat"
|
||||
icon="fas fa-paperclip"
|
||||
:click-action="() => SessionUser.objects.orders.functions.showAttachWashCertificateForm(props.order_id, () => {
|
||||
props.refreshFunction();
|
||||
})"
|
||||
:disabled="false"
|
||||
/>
|
||||
<!-- Change the customer (of the order) -->
|
||||
<action-settings-wheel-item
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
label="Skift kunde"
|
||||
icon="fas fa-user-edit"
|
||||
:click-action="() => SessionUser.objects.orders.functions.showChangeCustomerForm(props.order_id)"
|
||||
:disabled="false"
|
||||
/>
|
||||
<!-- Change the invoice collection (of the order) -->
|
||||
<action-settings-wheel-item
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
label="Skift faktura samling"
|
||||
icon="fas fa-file-invoice-dollar"
|
||||
:click-action="() => SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm(props.order_id)"
|
||||
:disabled="false"
|
||||
/>
|
||||
<!-- Delete the order -->
|
||||
<action-settings-wheel-item
|
||||
v-if="SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()"
|
||||
:label="`Slet ${SessionUser.objects.orders.meta.labels.single.toLowerCase()}`"
|
||||
template="danger"
|
||||
icon="fas fa-trash-alt"
|
||||
:click-action="() => SessionUser.objects.orders.functions.showDeleteConfirmationModal(props.order_id, () => {
|
||||
emitDeleted();
|
||||
})"
|
||||
:disabled="false"
|
||||
/>
|
||||
</template>
|
||||
<!-- If there is an invoice collection id, show the invoice actions -->
|
||||
<template v-if="props.invoice_collection_id">
|
||||
<!-- Label -->
|
||||
<action-settings-wheel-item-label
|
||||
:label="SessionUser.objects.collectedOrderInvoices.meta.title"
|
||||
v-show="SessionUser.canAccessSuperUser()"
|
||||
/>
|
||||
<!-- Se bruger -->
|
||||
<ActionSettingsWheelItem
|
||||
label="Se kunde i ny fane"
|
||||
icon="fas fa-user-edit"
|
||||
:click-action="() => SessionUser.functions.redirectTo.superUser('/users/' + props.user_id, true)"
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
/>
|
||||
<!-- Log ind som kunde -->
|
||||
<ActionSettingsWheelItem
|
||||
label="Log ind som kunde"
|
||||
icon="fas fa-user-shield"
|
||||
:click-action="() => SessionUser.superUser.intimidate.intimidateUser(props.user_id)"
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
/>
|
||||
/>
|
||||
<!-- Show the invoice collection, in a new tab -->
|
||||
<action-settings-wheel-item
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
label="Vis faktura samling i ny fane"
|
||||
icon="fas fa-file-invoice-dollar"
|
||||
:click-action="() => redirectSuperUserInvoiceCollectionPage(props.invoice_collection_id)"
|
||||
/>
|
||||
</template>
|
||||
<!-- If there's a user_id, show the user actions -->
|
||||
<template v-if="hasUser()">
|
||||
<!-- Label -->
|
||||
<ActionSettingsWheelItemLabel
|
||||
:label="SessionUser.objects.global.language.customer"
|
||||
v-show="SessionUser.canAccessSuperUser()"
|
||||
/>
|
||||
<!-- Se bruger -->
|
||||
<ActionSettingsWheelItem
|
||||
label="Se kunde i ny fane"
|
||||
icon="fas fa-user-edit"
|
||||
:click-action="() => SessionUser.functions.redirectTo.superUser('/users/' + userIdFromCustomerNumber, true)"
|
||||
v-if="SessionUser.canAccessSuperUser() && userIdFromCustomerNumber"
|
||||
/>
|
||||
<!-- Log ind som kunde -->
|
||||
<ActionSettingsWheelItem
|
||||
label="Log ind som bruger"
|
||||
icon="fas fa-user-shield"
|
||||
:click-action="() => SessionUser.superUser.intimidate.intimidateUser(userIdFromCustomerNumber)"
|
||||
v-if="SessionUser.canAccessSuperUser() && userIdFromCustomerNumber"
|
||||
/>
|
||||
<!-- Log ind som kunde (QR) -->
|
||||
<ActionSettingsWheelItem
|
||||
@click="onClickShowImpersonationQRCode(userIdFromCustomerNumber)"
|
||||
icon="fas fa-user-shield"
|
||||
label="Log ind som bruger (QR-kode)"
|
||||
v-if="SessionUser.canAccessSuperUser() && userIdFromCustomerNumber"
|
||||
/>
|
||||
|
||||
<!-- Set password -->
|
||||
<ActionSettingsWheelItem
|
||||
label="Skift kodeord"
|
||||
icon="fas fa-key"
|
||||
:click-action="() => showSetCustomerPassword(props.user_id)"
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
/>
|
||||
<!-- Show customer modal -->
|
||||
<ActionSettingsWheelItem
|
||||
label="Vis kunde"
|
||||
icon="fas fa-user"
|
||||
:click-action="() => customerModalVisible = true"
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
/>
|
||||
</template>
|
||||
<!-- If there's an order_id, show the order actions -->
|
||||
<template v-if="props.order_id">
|
||||
<!-- Label, only if the user is a superuser (Otherwise, the label is redundant) -->
|
||||
<action-settings-wheel-item-label
|
||||
:label="SessionUser.functions.ucFirst(SessionUser.objects.orders.meta.labels.single)"
|
||||
v-show="SessionUser.canAccessSuperUser()"
|
||||
/>
|
||||
<!-- Go to the order page, in a new tab -->
|
||||
<action-settings-wheel-item
|
||||
v-if="SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()"
|
||||
label="Se transaktion i ny fane"
|
||||
icon="fas fa-external-link-alt"
|
||||
:click-action="async () => redirectDepartmentOrderPage(props.order_id)"
|
||||
:disabled="false"
|
||||
/>
|
||||
<!-- Go to the user/order/:order_id page, in a new tab -->
|
||||
<action-settings-wheel-item
|
||||
v-if="!SessionUser.canAccessAdmin() && !SessionUser.canAccessSuperUser()"
|
||||
label="Se transaktion i ny fane"
|
||||
icon="fas fa-external-link-alt"
|
||||
:click-action="() => SessionUser.functions.redirectTo.user('/orders/' + props.order_id, true)"
|
||||
:disabled="false"
|
||||
<!-- Set password -->
|
||||
<ActionSettingsWheelItem
|
||||
label="Skift kodeord"
|
||||
icon="fas fa-key"
|
||||
:click-action="() => showSetCustomerPassword(userIdFromCustomerNumber)"
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
/>
|
||||
<!-- Show customer modal -->
|
||||
<ActionSettingsWheelItem
|
||||
label="Vis kunde"
|
||||
icon="fas fa-user"
|
||||
:click-action="() => customerModalVisible = true"
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
/>
|
||||
</template>
|
||||
<!-- If there is a reg_1, show the reg_1 actions -->
|
||||
<template v-if="props.reg_1 || props.reg_2">
|
||||
<!-- Label -->
|
||||
<action-settings-wheel-item-label
|
||||
:label="props.reg_1 && props.reg_2 ? `${SessionUser.objects.vehicles.meta.labels.multiple}` : `${SessionUser.objects.vehicles.meta.labels.single}`"
|
||||
v-show="SessionUser.canAccessSuperUser()"
|
||||
/>
|
||||
<!-- Show the vehicle, in a new tab -->
|
||||
<action-settings-wheel-item
|
||||
:label="`Se ${SessionUser.objects.vehicles.meta.labels.single.toLowerCase()} ${props.reg_1} i ny fane`"
|
||||
icon="fas fa-car"
|
||||
v-if="props.reg_1 && SessionUser.canAccessSuperUser()"
|
||||
:click-action="() => SessionUser.functions.redirectTo.superUser('/vehicles/' + props.reg_1, true)"
|
||||
/>
|
||||
<action-settings-wheel-item
|
||||
:label="`Se ${SessionUser.objects.vehicles.meta.labels.single.toLowerCase()} ${props.reg_2} i ny fane`"
|
||||
icon="fas fa-car"
|
||||
v-if="props.reg_2 && SessionUser.canAccessSuperUser()"
|
||||
:click-action="() => SessionUser.functions.redirectTo.superUser('/vehicles/' + props.reg_2, true)"
|
||||
/>
|
||||
<!-- If there are any attachments, show the attachments actions -->
|
||||
<template v-if="attachmentsFromOrder.length > 0">
|
||||
<action-settings-wheel-item-label
|
||||
:label="'Vedhæftede filer'"
|
||||
/>
|
||||
<!-- Delete the order -->
|
||||
<action-settings-wheel-item
|
||||
v-if="SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()"
|
||||
label="Slet transaktion"
|
||||
icon="fas fa-trash-alt"
|
||||
:click-action="() => SessionUser.objects.orders.functions.showDeleteConfirmationModal(props.order_id, () => {
|
||||
emitDeleted();
|
||||
})"
|
||||
:disabled="false"
|
||||
/>
|
||||
<!-- Change the customer (of the order) -->
|
||||
<action-settings-wheel-item
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
label="Skift kunde"
|
||||
icon="fas fa-user-edit"
|
||||
:click-action="() => SessionUser.objects.orders.functions.showChangeCustomerForm(props.order_id)"
|
||||
:disabled="false"
|
||||
/>
|
||||
<!-- Change the invoice collection (of the order) -->
|
||||
<action-settings-wheel-item
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
label="Skift faktura samling"
|
||||
icon="fas fa-file-invoice-dollar"
|
||||
:click-action="() => SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm(props.order_id)"
|
||||
:disabled="false"
|
||||
/>
|
||||
<template v-for="(attachment, index) in attachmentsFromOrder" :key="index">
|
||||
<action-settings-wheel-item
|
||||
:label="`${attachment.content?.document || attachment.content?.other}`"
|
||||
icon="fas fa-paperclip"
|
||||
:click-action="() => SessionUser.objects.orders.functions.downloadAttachment(props.order_id, attachment.id, true)
|
||||
.catch((error) => {
|
||||
Swal.fire({
|
||||
title: 'Fejl',
|
||||
text: 'Der opstod en fejl under download af vedhæftede fil.',
|
||||
icon: 'error',
|
||||
});
|
||||
})"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<!-- Collected order invoice actions -->
|
||||
<template v-if="props.invoice_collection_id">
|
||||
<ActionSettingsWheelItemLabel
|
||||
:label="SessionUser.objects.collectedOrderInvoices.meta.title"
|
||||
v-show="props.invoice_collection_id"
|
||||
/>
|
||||
<!-- Download invoice -->
|
||||
<ActionSettingsWheelItem
|
||||
label="Download faktura"
|
||||
icon="fas fa-download"
|
||||
:click-action="() => SessionUser.objects.collectedOrderInvoices.functions.download(props.invoice_collection_id)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<!-- If there is an invoice collection id, show the invoice actions -->
|
||||
<template v-if="props.invoice_collection_id">
|
||||
<!-- Label -->
|
||||
<action-settings-wheel-item-label
|
||||
:label="SessionUser.objects.collectedOrderInvoices.meta.title"
|
||||
v-show="SessionUser.canAccessSuperUser()"
|
||||
/>
|
||||
<!-- Show the invoice collection, in a new tab -->
|
||||
<action-settings-wheel-item
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
label="Vis faktura samling i ny fane"
|
||||
icon="fas fa-file-invoice-dollar"
|
||||
:click-action="() => redirectSuperUserInvoiceCollectionPage(props.invoice_collection_id)"
|
||||
/>
|
||||
<!-- If no actions are defined, then show that no actions are defined -->
|
||||
<template v-else>
|
||||
<div class="dropdown-item">
|
||||
<p class="has-text-centered">Ingen handlinger defineret</p>
|
||||
</div>
|
||||
</template>
|
||||
<!-- If there is a reg_1, show the reg_1 actions -->
|
||||
<template v-if="props.reg_1">
|
||||
<!-- Label -->
|
||||
<action-settings-wheel-item-label
|
||||
:label="SessionUser.objects.vehicles.meta.labels.single"
|
||||
v-show="SessionUser.canAccessSuperUser()"
|
||||
/>
|
||||
<!-- Show the vehicle, in a new tab -->
|
||||
<action-settings-wheel-item
|
||||
label="Vis køretøj i ny fane"
|
||||
icon="fas fa-car"
|
||||
v-if="SessionUser.canAccessSuperUser()"
|
||||
:click-action="() => SessionUser.functions.redirectTo.superUser('/vehicles/' + props.reg_1, true)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<!-- If no actions are defined, then show that no actions are defined -->
|
||||
<template v-else>
|
||||
<div class="dropdown-item">
|
||||
<p class="has-text-centered">Ingen handlinger defineret</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CustomerModal
|
||||
v-if="customerModalVisible"
|
||||
:user_id="props.user_id"
|
||||
:user_id="userIdFromCustomerNumber"
|
||||
@closeModal="customerModalVisible = false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { IS_DEV } from '@/main';
|
||||
import { ref, computed, defineProps, defineEmits, watch } from 'vue';
|
||||
import { SessionUser } from '@/components/session/token/SessionUser.vue';
|
||||
import {BField, BSelect} from "buefy";
|
||||
|
||||
const props = defineProps({
|
||||
selection: {
|
||||
@@ -39,6 +40,7 @@ const props = defineProps({
|
||||
showMonthSelector?: boolean, // Whether to show the month select
|
||||
showMultipleMonthWarning?: boolean, // Whether to show a warning if the selection spans multiple months
|
||||
showYearSelector?: boolean, // Whether to show the year selector
|
||||
showShortCuts?: boolean, // Whether to show the shortcuts
|
||||
},
|
||||
required: false,
|
||||
default: () => ({
|
||||
@@ -87,6 +89,7 @@ const visibility = computed(() => {
|
||||
showYearSelector: true,
|
||||
showDailySelector: false,
|
||||
showWeeklySelector: false,
|
||||
showShortCuts: true,
|
||||
// Override with props visibility settings
|
||||
...props.visibility,
|
||||
};
|
||||
@@ -315,14 +318,104 @@ const weeklyShortcuts: shortcut[] = [
|
||||
},
|
||||
];
|
||||
const shortcuts = ref<shortcut[]>([
|
||||
...(visibility.value.showDailySelector ? dailyShortcuts : []),
|
||||
...(visibility.value.showWeeklySelector ? weeklyShortcuts : []),
|
||||
...((visibility.value.showDailySelector || visibility.value.showShortCuts) ? dailyShortcuts : []),
|
||||
...((visibility.value.showWeeklySelector || visibility.value.showShortCuts) ? weeklyShortcuts : []),
|
||||
]);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="level is-mobile is-align-items-center">
|
||||
<!-- Mobile date selector -->
|
||||
<div class="columns is-mobile is-multiline" v-show="SessionUser.functions.device.isMobile()">
|
||||
<!-- Date from & Date to -->
|
||||
<div class="column is-half">
|
||||
<label class="label is-small" v-if="visibility.showStartDate">{{ SessionUser.objects.global.language.text.date_from }}</label>
|
||||
<input
|
||||
type="date"
|
||||
class="input"
|
||||
v-model="props.selection.startDate.toISOString().split('T')[0]"
|
||||
@change="handleStartDateChange"
|
||||
v-bind:disabled="props.isDisabled || props.isReadonly"
|
||||
:class="{ 'is-danger': !isSelectionValid }"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<label class="label is-small" v-if="visibility.showEndDate">{{ SessionUser.objects.global.language.text.date_to }}</label>
|
||||
<input
|
||||
type="date"
|
||||
class="input"
|
||||
v-model="props.selection.endDate.toISOString().split('T')[0]"
|
||||
@change="handleEndDateChange"
|
||||
v-bind:disabled="props.isDisabled || props.isReadonly"
|
||||
:class="{ 'is-danger': !isSelectionValid }"
|
||||
/>
|
||||
</div>
|
||||
<!-- Month selector -->
|
||||
<div class="column is-half" v-if="visibility.showMonthSelector">
|
||||
<div class="field has-addons is-centered">
|
||||
<p class="control is-expanded">
|
||||
<span class="select is-fullwidth">
|
||||
<select
|
||||
class="input"
|
||||
v-model="currentMonthSelection"
|
||||
@change="setMonth(currentMonthSelection)"
|
||||
v-bind:disabled="props.isDisabled || props.isReadonly"
|
||||
>
|
||||
<!-- Default option: Custom -->
|
||||
<option value="" disabled selected>Select Month</option>
|
||||
<option v-for="month in months" :key="month.value" :value="month.value">
|
||||
{{ SessionUser.functions.ucFirst(month.label) }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Year selector -->
|
||||
<div class="column is-half" v-if="visibility.showYearSelector">
|
||||
<div class="field has-addons is-centered">
|
||||
<p class="control is-expanded">
|
||||
<span class="select is-fullwidth">
|
||||
<select
|
||||
class="input"
|
||||
:value="startDate.getFullYear()"
|
||||
@change="(e) => { const year = parseInt(e.target.value); if (!isNaN(year)) { const newStartDate = new Date(startDate); newStartDate.setFullYear(year); startDate = newStartDate; const newEndDate = new Date(endDate); newEndDate.setFullYear(year); endDate = newEndDate; isPendingEmit = true; if (props.autoEmitChange) { updateSelection(); } } }"
|
||||
v-bind:disabled="props.isDisabled || props.isReadonly"
|
||||
>
|
||||
<!-- Generate a range of years from current year - 10 to current year + 10 -->
|
||||
<option v-for="year in Array.from({ length: 21 }, (_, i) => new Date().getFullYear() - 10 + i)" :key="year" :value="year">
|
||||
{{ year }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button is-static">
|
||||
{{ SessionUser.objects.global.language.text.year }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Shortcuts -->
|
||||
<div class="column is-full">
|
||||
<b-field label="Periode">
|
||||
<b-select placeholder="Vælg periode"
|
||||
:expandable="true"
|
||||
:model-value="shortcuts.find(s => (s.getRange().startDate.toISOString().split('T')[0] === startDate.toISOString().split('T')[0]) && (s.getRange().endDate.toISOString().split('T')[0] === endDate.toISOString().split('T')[0]))?.label || null"
|
||||
:expanded="true"
|
||||
@update:modelValue="(value) => { const selectedShortcut = shortcuts.find(s => s.label === value); if (selectedShortcut) { const range = selectedShortcut.getRange(); startDate = range.startDate; endDate = range.endDate; isPendingEmit = true; if (props.autoEmitChange) { updateSelection(); } } }" v-bind:disabled="props.isDisabled || props.isReadonly">
|
||||
<option
|
||||
v-for="(option, index) in shortcuts"
|
||||
:key="index"
|
||||
:value="option.label"
|
||||
>{{ option.label }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Desktop date selector -->
|
||||
<div class="level is-align-items-center" v-show="!SessionUser.functions.device.isMobile()">
|
||||
<div class="level-left">
|
||||
<!-- Start Date Input -->
|
||||
<div class="level-item" v-if="visibility.showStartDate">
|
||||
@@ -350,7 +443,7 @@ const shortcuts = ref<shortcut[]>([
|
||||
/>
|
||||
</div>
|
||||
<!-- Display a message if neither start nor end date is shown -->
|
||||
<div class="level-item" v-if="!visibility.showStartDate && !visibility.showEndDate">
|
||||
<div class="level-item" v-if="!visibility.showStartDate && !visibility.showEndDate && visibility.showToLabel">
|
||||
<span class="label">No date selection available</span>
|
||||
</div>
|
||||
<!-- Month and Year selectors -->
|
||||
@@ -438,7 +531,7 @@ const shortcuts = ref<shortcut[]>([
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar is-white">
|
||||
<div class="navbar is-white" v-show="!SessionUser.functions.device.isMobile()">
|
||||
<div class="navbar-menu">
|
||||
<!-- Date shortcuts -->
|
||||
<div class="navbar-start">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import {defineProps, ref} from 'vue';
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import {BTooltip} from "buefy";
|
||||
const props = defineProps({
|
||||
object: Object,
|
||||
loadList: Function,
|
||||
@@ -8,6 +9,12 @@ const props = defineProps({
|
||||
column: String,
|
||||
parseFunction: Function,
|
||||
virtualColumn: Boolean,
|
||||
hoverText: {
|
||||
type: String,
|
||||
default: () => {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
permissionCheckFunction: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
@@ -20,6 +27,23 @@ const props = defineProps({
|
||||
default: () => {
|
||||
return null; // Example: <td>
|
||||
}
|
||||
},
|
||||
// Theme can be 'divided' or 'simple'
|
||||
theme: {
|
||||
type: String,
|
||||
validator(value, props) {
|
||||
return ['divided', 'simple'].includes(value);
|
||||
},
|
||||
default: () => {
|
||||
return 'simple';
|
||||
}
|
||||
},
|
||||
// Hover HTML content
|
||||
hoverHtml: {
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -52,19 +76,62 @@ const isLoading = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="props.componentWrapper ? props.componentWrapper : 'td'">
|
||||
<span v-if="!props.virtualColumn">{{ parseFunction ? parseFunction(props.object[props.column]) : props.object[props.column] }}</span>
|
||||
<span v-else>{{ parseFunction ? parseFunction(props.object) : props.object }}</span>
|
||||
<span class="ml-2" v-if="props.permissionCheckFunction()">
|
||||
<button class="button is-small is-transparent is-text" style="text-decoration-line: none;" @click="clickEdit" v-if="props.editFunction" :class="{'is-loading': isLoading}" :disabled="isLoading">
|
||||
<span class="icon">
|
||||
<i class="fas fa-edit"></i>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
<component :is="props.componentWrapper ? props.componentWrapper : 'td'"
|
||||
@click="clickEdit"
|
||||
class="hover-illustration"
|
||||
:class="{'is-clickable': props.permissionCheckFunction() && props.editFunction}">
|
||||
<div>
|
||||
|
||||
<!-- Divided display ( value | edit button ) -->
|
||||
<template v-if="props.theme === 'divided'">
|
||||
<nav class="level is-mobile" style="min-width: -webkit-fill-available; width: -webkit-fill-available; overflow: visible; max-width: -webkit-fill-available;">
|
||||
<div class="level-left" style="overflow: hidden; max-width: calc(100% - 2.5em); text-overflow: ellipsis; white-space: nowrap;">
|
||||
<div class="level-item" :title="props.hoverText ? props.hoverText : ( !props.virtualColumn ? (parseFunction ? parseFunction(props.object[props.column]) : props.object[props.column]) : (parseFunction ? parseFunction(props.object) : props.object) )">
|
||||
<span class="" v-if="!props.virtualColumn">{{ parseFunction ? parseFunction(props.object[props.column]) : props.object[props.column] }}</span>
|
||||
<span class="" v-else>{{ parseFunction ? parseFunction(props.object) : props.object }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<span class="ml-2 is-float-right" v-if="props.permissionCheckFunction()">
|
||||
<button class="button is-small is-transparent is-text" style="text-decoration-line: none;" @click="clickEdit" v-if="props.editFunction" :class="{'is-loading': isLoading}" :disabled="isLoading">
|
||||
<span class="icon">
|
||||
<i class="fas fa-edit"></i>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
<!-- Simple display ( value ) with edit button on hover -->
|
||||
<template v-else>
|
||||
<!-- Hover Edit icon -->
|
||||
<b-tooltip
|
||||
type="is-light"
|
||||
position="is-bottom"
|
||||
:multilined="$slots.hoverHtml"
|
||||
square>
|
||||
<!-- Value Display -->
|
||||
<span class="" v-if="!props.virtualColumn">{{ parseFunction ? parseFunction(props.object[props.column]) : props.object[props.column] }}</span>
|
||||
<span class="" v-else>{{ parseFunction ? parseFunction(props.object) : props.object }}</span>
|
||||
<template v-slot:content>
|
||||
<slot name="hoverHtml">
|
||||
<div v-html="props.hoverText" v-if="props.hoverText"></div>
|
||||
<div v-else>
|
||||
{{ parseFunction ? parseFunction(props.object[props.column]) : props.object[props.column]}}
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.hover-illustration:hover {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -57,38 +57,42 @@ const modules = [
|
||||
},
|
||||
count: ref(0)
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Notifikationer",
|
||||
description: "Gå til notifikationer",
|
||||
icon: "fas fa-bell",
|
||||
onClick: () => {
|
||||
// Get the departmentId from the URL
|
||||
const departmentId = router.currentRoute.value.params.departmentId;
|
||||
// Redirect to the notifications
|
||||
router.push(`/admin/${departmentId}/modules/notifications`);
|
||||
},
|
||||
onClickCount: () => {
|
||||
this.onClick();
|
||||
},
|
||||
count: ref(0)
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Tidsbookinger",
|
||||
description: "Gå til tidsbookinger",
|
||||
icon: "fas fa-clock",
|
||||
onClick: () => {
|
||||
// Get the departmentId from the URL
|
||||
const departmentId = router.currentRoute.value.params.departmentId;
|
||||
// Redirect to the time bookings
|
||||
router.push(`/admin/${departmentId}/modules/time-bookings`);
|
||||
},
|
||||
onClickCount: () => {
|
||||
this.onClick();
|
||||
},
|
||||
count: ref(0)
|
||||
}
|
||||
/**
|
||||
* {
|
||||
* id: 4,
|
||||
* name: "Notifikationer",
|
||||
* description: "Gå til notifikationer",
|
||||
* icon: "fas fa-bell",
|
||||
* onClick: () => {
|
||||
* // Get the departmentId from the URL
|
||||
* const departmentId = router.currentRoute.value.params.departmentId;
|
||||
* // Redirect to the notifications
|
||||
* router.push(`/admin/${departmentId}/modules/notifications`);
|
||||
* },
|
||||
* onClickCount: () => {
|
||||
* this.onClick();
|
||||
* },
|
||||
* count: ref(0)
|
||||
* },
|
||||
*/
|
||||
/**
|
||||
* {
|
||||
* id: 5,
|
||||
* name: "Tidsbookinger",
|
||||
* description: "Gå til tidsbookinger",
|
||||
* icon: "fas fa-clock",
|
||||
* onClick: () => {
|
||||
* // Get the departmentId from the URL
|
||||
* const departmentId = router.currentRoute.value.params.departmentId;
|
||||
* // Redirect to the time bookings
|
||||
* router.push(`/admin/${departmentId}/modules/time-bookings`);
|
||||
* },
|
||||
* onClickCount: () => {
|
||||
* this.onClick();
|
||||
* },
|
||||
* count: ref(0)
|
||||
* }
|
||||
*/
|
||||
];
|
||||
|
||||
// Get the departmentId from the URL
|
||||
|
||||
@@ -71,8 +71,8 @@ const hasUpdatePermission = () => {
|
||||
:permission-check-function="hasUpdatePermission"
|
||||
/>
|
||||
<!-- Actions -->
|
||||
<td class="is-float-right">
|
||||
<ActionSettingsWheelButton>
|
||||
<td>
|
||||
<ActionSettingsWheelButton class="is-float-right">
|
||||
<template #actions>
|
||||
<RequiresPermission :permission="'department_notification_sms_delete'">
|
||||
<template #default>
|
||||
@@ -99,7 +99,7 @@ const hasUpdatePermission = () => {
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="12">{{SessionUser.objects.global.language.showing}} {{ objects.length }} {{SessionUser.objects.department_notification_sms.meta.labels.multiple}}</td>
|
||||
<td colspan="6">{{SessionUser.objects.global.language.showing}} {{ objects.length }} {{SessionUser.objects.department_notification_sms.meta.labels.multiple}}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
@@ -40,10 +40,10 @@ const pendingBookings = ref([]);
|
||||
// Get the department booking list
|
||||
const loadPendingBookings = () => {
|
||||
SessionUser.request(
|
||||
SessionUser.objects.bookings.meta.endpoint,
|
||||
SessionUser.objects.order_bookings.meta.endpoint,
|
||||
'GET',
|
||||
{
|
||||
filters: 'department:' + department_id.value + ',status:pending',
|
||||
filters: 'department:' + department_id.value + ',order_id:is null',
|
||||
page: 1,
|
||||
limit: 100,
|
||||
}
|
||||
@@ -72,12 +72,12 @@ const getVehiclePlateBooking = (vehiclePlate) => {
|
||||
return null;
|
||||
}
|
||||
// Find the booking for the given vehicle plate
|
||||
return pendingBookings.value.find(booking => booking.regNrTraekker === vehiclePlate || booking.regNrTrailer === vehiclePlate);
|
||||
return pendingBookings.value.find(booking => booking.reg_1 === vehiclePlate || booking.reg_2 === vehiclePlate);
|
||||
};
|
||||
|
||||
const doesVehiclePlateHaveBooking = (vehiclePlate) => {
|
||||
// Check if the vehicle has a booking in the pending bookings list
|
||||
return pendingBookings.value.some(booking => booking.regNrTraekker === vehiclePlate || booking.regNrTrailer === vehiclePlate);
|
||||
return pendingBookings.value.some(booking => booking.reg_1 === vehiclePlate || booking.reg_2 === vehiclePlate);
|
||||
};
|
||||
|
||||
|
||||
@@ -681,7 +681,7 @@ const isDropdownContentLoaded = (scan) => {
|
||||
<tr v-if="scansInfinityScroll !== null && scansInfinityScroll.length === 0">
|
||||
<td colspan="3">
|
||||
<div class="has-text-centered">
|
||||
Ingen flere nummerplader
|
||||
Ingen nummerplader blev fundet
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -695,6 +695,7 @@ const isDropdownContentLoaded = (scan) => {
|
||||
<span class="icon">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span> </span>
|
||||
<span>{{ SessionUser.objects.global.language.nothing_left_to_show }}</span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -702,12 +703,6 @@ const isDropdownContentLoaded = (scan) => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- If there's no scans, and the infinity scroll is also empty or null -->
|
||||
<div class="panel-block" v-if="list.length === 0 && getDepartment() !== '' && (scansInfinityScroll === null || scansInfinityScroll.length === 0)">
|
||||
<p>
|
||||
Ingen nummerplader blev fundet
|
||||
</p>
|
||||
</div>
|
||||
<!-- Reload button -->
|
||||
<div class="panel-block" v-if="list.length === 0 && getDepartment() !== '' && scansInfinityScroll === null">
|
||||
<button class="button is-dark is-fullwidth" @click="loadList">
|
||||
|
||||
@@ -5,23 +5,38 @@ import productImage02 from "/src/assets/piktogrammer/02.png";
|
||||
import productImage03 from "/src/assets/piktogrammer/03.png"; // Replaced by 3
|
||||
import productImage3 from "/src/assets/piktogrammer/3.png"; // Sættevognstræk
|
||||
import productImage04 from "/src/assets/piktogrammer/04.png";
|
||||
import productImage05 from "/src/assets/piktogrammer/05.png";
|
||||
import productImage06 from "/src/assets/piktogrammer/06.png";
|
||||
import productImage05 from "/src/assets/piktogrammer/05.png"; // Deprecated - use 5
|
||||
import productImage5 from "/src/assets/piktogrammer/5.png"; // Forvogn
|
||||
import productImage06 from "/src/assets/piktogrammer/06.png"; // Deprecated - use 6
|
||||
import productImage6 from "/src/assets/piktogrammer/6.png"; // Forvogn med hænger
|
||||
import productImage07 from "/src/assets/piktogrammer/07.png";
|
||||
import productImage08 from "/src/assets/piktogrammer/08.png";
|
||||
import productImage09 from "/src/assets/piktogrammer/09.png";
|
||||
import productImage10 from "/src/assets/piktogrammer/10.png";
|
||||
import productImage11 from "/src/assets/piktogrammer/11.png";
|
||||
import productImage12 from "/src/assets/piktogrammer/12.png";
|
||||
import productImage13 from "/src/assets/piktogrammer/13.png";
|
||||
import productImage14 from "/src/assets/piktogrammer/14.png";
|
||||
import productImage15 from "/src/assets/piktogrammer/15.png";
|
||||
import productImage16 from "/src/assets/piktogrammer/16.png"; // Autocamper / Campingvogn
|
||||
import productImage18 from "/src/assets/piktogrammer/18.png";
|
||||
import productImage17 from "/src/assets/piktogrammer/17.png"; // Bus
|
||||
import productImage18 from "/src/assets/piktogrammer/18.png"; // Entreprenør forvogn
|
||||
import productImage19 from "/src/assets/piktogrammer/19.png"; // Entreprenør forvogn med hænger
|
||||
import productImage20 from "/src/assets/piktogrammer/20.png"; // Entreprenør sættevogn
|
||||
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 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
|
||||
import productImage44 from "/src/assets/piktogrammer/44.png"; // "Christmas Tree"
|
||||
import productImage45 from "/src/assets/piktogrammer/45.png"; // "Christmas Tree" - Sticker roll
|
||||
import productImage46 from "/src/assets/piktogrammer/46.png"; // Kødkroge, vask
|
||||
import productImage47 from "/src/assets/piktogrammer/47.png"; // Kødkroge med mærker, vask
|
||||
import productImage48 from "/src/assets/piktogrammer/48.png"; // E2
|
||||
import productImage49 from "/src/assets/piktogrammer/49.png"; // Interior wash forvogn hænger
|
||||
import productImage56 from "/src/assets/piktogrammer/56.png"; // Safety Seal
|
||||
// Get the picture of the product
|
||||
export const getPicture = (piktogram) => {
|
||||
@@ -46,9 +61,11 @@ export const getPicture = (piktogram) => {
|
||||
case "04":
|
||||
return productImage04;
|
||||
case "05":
|
||||
return productImage05;
|
||||
case "5": // Forvogn
|
||||
return productImage5;
|
||||
case "06":
|
||||
return productImage06;
|
||||
case "6": // Forvogn med hænger
|
||||
return productImage6;
|
||||
case "07":
|
||||
return productImage07;
|
||||
case "08":
|
||||
@@ -57,16 +74,40 @@ export const getPicture = (piktogram) => {
|
||||
return productImage09;
|
||||
case "10":
|
||||
return productImage10;
|
||||
case "11":
|
||||
return productImage11;
|
||||
case "12":
|
||||
return productImage12;
|
||||
case "13":
|
||||
return productImage13;
|
||||
case "14":
|
||||
return productImage14;
|
||||
case "15":
|
||||
return productImage15;
|
||||
case "16":
|
||||
return productImage16;
|
||||
case "17":
|
||||
return productImage17;
|
||||
case "18":
|
||||
return productImage18;
|
||||
case "19":
|
||||
return productImage19;
|
||||
case "20":
|
||||
return productImage20;
|
||||
case "21":
|
||||
return productImage21;
|
||||
case "23":
|
||||
return productImage23;
|
||||
case "24":
|
||||
return productImage24;
|
||||
case "25":
|
||||
return productImage25;
|
||||
case "26":
|
||||
return productImage26;
|
||||
case "27":
|
||||
return productImage27;
|
||||
case "39":
|
||||
return productImage39;
|
||||
case "43":
|
||||
return productImage43;
|
||||
case "44":
|
||||
@@ -79,6 +120,8 @@ export const getPicture = (piktogram) => {
|
||||
return productImage47;
|
||||
case "48":
|
||||
return productImage48;
|
||||
case "49":
|
||||
return productImage49;
|
||||
case "56":
|
||||
return productImage56;
|
||||
default:
|
||||
|
||||
@@ -39,7 +39,7 @@ const right_tabs = [
|
||||
<div class="columns">
|
||||
<!-- Choose products -->
|
||||
<div class="column is-8-desktop pl-0 pr-3">
|
||||
<SelectProductsFormPOS class="mt-3" category="null" />
|
||||
<SelectProductsFormPOS class="mt-3" category="null" :isCustomerBooking="false"/>
|
||||
<PosNotes :notes="notes" class="mt-3" :isAddFormVisible="false" :isOldNotesVisible="true"/>
|
||||
</div>
|
||||
<!-- Current order items -->
|
||||
|
||||
@@ -31,7 +31,10 @@ import PosDepartmentStepMobileAttachments
|
||||
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileAttachments.vue";
|
||||
import PosDepartmentStep1MobileTransactionHistory
|
||||
from "@/components/displays/department/pos/steps/mobile/views/PosDepartmentStep1MobileTransactionHistory.vue";
|
||||
import {attachments} from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
||||
import {
|
||||
attachments,
|
||||
popups
|
||||
} from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
||||
// Debug mode flag
|
||||
const debug_mode = ref(false);
|
||||
// Debug array to store request results
|
||||
@@ -169,6 +172,7 @@ const onAutomaticSelection = (object: PosSearchResult | null) => {
|
||||
reference: object?.reference || null,
|
||||
last_order_id: object?.lastOrderId || null,
|
||||
wash_subscription: object?.washSubscription,
|
||||
booking_id: object?.bookingId || null,
|
||||
} as PosVehicle;
|
||||
switch (vehicles.activeVehicleIndex.value) {
|
||||
case 1:
|
||||
@@ -269,7 +273,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<!-- Take picture button (When attachment view is active) -->
|
||||
<div class="is-flex is-justify-content-center">
|
||||
<PosDepartmentStepMobileButtonNextStep :isDark="true" v-if="views.attachmentView.value" style="width: 100%;" class="mb-2" :customDisabled="false" :customAction="() => { attachments.takePicture(); }">
|
||||
<PosDepartmentStepMobileButtonNextStep :isWhite="false" v-if="views.attachmentView.value" style="width: 100%;" class="mb-2" :customDisabled="false" :customAction="() => { attachments.takePicture(); }" :buttonClasses="['has-background-primary-dark', 'has-text-black']">
|
||||
<span class="is-float-left has-text-white">{{ SessionUser.objects.global.language.take_picture }}</span>
|
||||
<span class="is-float-right has-text-white">
|
||||
<!-- Picture icon -->
|
||||
@@ -281,9 +285,9 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<div class="is-flex is-justify-content-center">
|
||||
<!-- Next step button (If not in attachment view) -->
|
||||
<PosDepartmentStepMobileButtonNextStep style="width: 100%;" v-if="!views.attachmentView.value"/>
|
||||
<PosDepartmentStepMobileButtonNextStep style="width: 100%;" v-if="!views.attachmentView.value" :buttonClasses="['has-background-primary', 'has-text-black']" :isWhite="false"/>
|
||||
<!-- Next step button (If in attachment view) -->
|
||||
<PosDepartmentStepMobileButtonNextStep style="width: 100%;" v-if="views.attachmentView.value" :customDisabled="false" :customAction="() => { views.attachmentView.value = false; }">
|
||||
<PosDepartmentStepMobileButtonNextStep style="width: 100%;" v-if="views.attachmentView.value" :customDisabled="false" :customAction="() => { views.attachmentView.value = false; }" :buttonClasses="['has-background-primary', 'has-text-black']" :isWhite="false">
|
||||
<span class="is-float-left has-text-dark">{{ SessionUser.objects.global.language.close }}</span>
|
||||
<span class="is-float-right has-text-dark">
|
||||
<!-- x icon -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { setTransparency, setBackgroundColor, backgroundColors } from "@/components/viewport/page/headers/ViewportHeaderSettings.vue";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import {onMounted, onUnmounted, ref, watch} from "vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import PosDepartmentStepMobile2Product
|
||||
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Product.vue";
|
||||
@@ -31,6 +31,7 @@ import PosDepartmentStepMobile2AdditionalItems
|
||||
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2AdditionalItems.vue";
|
||||
import PosDepartmentStepMobile2Customer
|
||||
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Customer.vue";
|
||||
import { pendingBookings, getVehiclePlateBooking } from "@/components/shop/POSDepartmentProcess.vue";
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
@@ -58,6 +59,11 @@ onMounted(() => {
|
||||
fetchLastOrder(1);
|
||||
fetchLastOrder(2);
|
||||
fetchLastOrder(3);
|
||||
|
||||
// Watch for scroll events to handle long press detection
|
||||
window.addEventListener('scroll', onScroll, true);
|
||||
// Try to auto-apply pending booking when Step 2 opens
|
||||
applyPendingBookingFromSelection();
|
||||
});
|
||||
|
||||
const applyPotentialCustomerNumberChange = (oldId, newId) => {
|
||||
@@ -74,6 +80,128 @@ watch(customer_id, (newId, oldId) => {
|
||||
|
||||
const lastFetchedPrimaryItemProduct = ref<PosProduct | null>(null); // This is used to make sure all the addons are fetched when the last order is selected.
|
||||
|
||||
// Keep track so we only apply a booking once (idempotent)
|
||||
const lastAppliedBookingId = ref<number | null>(null);
|
||||
|
||||
const getSelectedPendingBooking = () => {
|
||||
// Prefer explicit booking id if present
|
||||
const bookingId = metadata.getBookingId?.() ?? metadata.bookingId?.value ?? null;
|
||||
let booking = null as any;
|
||||
if (bookingId) {
|
||||
booking = pendingBookings.value?.find?.((b: any) => b?.id === bookingId) || null;
|
||||
if (booking) return booking;
|
||||
}
|
||||
// Fallback: try match current primary vehicle registration
|
||||
const reg = vehicles?.vehicle_1?.value?.reg || null;
|
||||
if (reg) {
|
||||
try {
|
||||
booking = getVehiclePlateBooking(reg);
|
||||
} catch (e) {
|
||||
booking = null;
|
||||
}
|
||||
}
|
||||
return booking || null;
|
||||
}
|
||||
|
||||
const fetchProductById = async (id: number): Promise<PosProduct> => {
|
||||
// Fetch product with final price for current department/customer. Fallback to a minimal shape.
|
||||
try {
|
||||
// Note: metadata.getCustomerId exists in flow; fallback to global customer_id if missing
|
||||
const custId = (metadata.getCustomerId && metadata.getCustomerId()) || (customer_id?.value ? parseInt(customer_id.value as any) : null);
|
||||
const product = await SessionUser.objects.products.get.single(id, {
|
||||
department_id: department_id.value,
|
||||
customer_id: custId,
|
||||
category_id: null,
|
||||
final_price: true
|
||||
});
|
||||
return product as PosProduct;
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch product', id, e);
|
||||
// Minimal fallback; name/price will be set by caller when available
|
||||
return { id, name: 'Produkt', price: 0, subscription_allowed: false } as PosProduct;
|
||||
}
|
||||
}
|
||||
|
||||
const applyPendingBookingFromSelection = async () => {
|
||||
const booking: any = getSelectedPendingBooking();
|
||||
if (!booking) return;
|
||||
if (lastAppliedBookingId.value === booking.id) return; // avoid duplicate application
|
||||
|
||||
try {
|
||||
// Update metadata booking id if not set
|
||||
if (!metadata.getBookingId || !metadata.getBookingId()) {
|
||||
metadata.setBookingId && metadata.setBookingId(booking.id);
|
||||
}
|
||||
|
||||
// Set PO number on the order if provided by the booking
|
||||
if (booking.po && typeof booking.po === 'string' && booking.po.trim() !== '') {
|
||||
try {
|
||||
await SessionUser.objects.orders.set.po(order_id.value, booking.po);
|
||||
console.warn('Applied PO from booking:', booking.po);
|
||||
} catch (e) {
|
||||
console.error('Failed to set PO on order from booking', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Map booking items to primary + addons
|
||||
const items: any[] = Array.isArray(booking.items) ? booking.items : [];
|
||||
if (!items.length) {
|
||||
lastAppliedBookingId.value = booking.id;
|
||||
return;
|
||||
}
|
||||
|
||||
// Primary product is the first item
|
||||
const primaryRaw = items[0];
|
||||
const primaryId = parseInt(primaryRaw?.id);
|
||||
if (!primaryId || isNaN(primaryId)) {
|
||||
lastAppliedBookingId.value = booking.id;
|
||||
return;
|
||||
}
|
||||
const primaryProduct = await fetchProductById(primaryId);
|
||||
// Prefer booking price if provided, otherwise keep fetched price
|
||||
if (typeof primaryRaw.price === 'number') primaryProduct.price = primaryRaw.price;
|
||||
primaryProduct.name = primaryRaw?.name || primaryProduct.name;
|
||||
primaryProduct.quantity = 1; // Primary item quantity is handled as 1 in order creation
|
||||
// Apply as primary item
|
||||
transactionItems.setPrimaryItem(primaryProduct as any);
|
||||
|
||||
// Remaining items become addons
|
||||
const addonRawList = items.slice(1);
|
||||
const addonProducts = await Promise.all(addonRawList.map(async (raw: any) => {
|
||||
const id = parseInt(raw?.id);
|
||||
if (!id || isNaN(id)) return null;
|
||||
const p = await fetchProductById(id);
|
||||
if (typeof raw.price === 'number') p.price = raw.price;
|
||||
p.name = raw?.name || p.name;
|
||||
p.quantity = raw?.quantity || 1;
|
||||
return p;
|
||||
}));
|
||||
const preparedAddons = addonProducts
|
||||
.filter(p => !!p)
|
||||
.map(p => transactionItems.convertProductToAddon(p as any, { quantity: (p as any)?.quantity || 1 }));
|
||||
|
||||
if (!transactionItems.primaryItem.value) {
|
||||
// Safety: if primary was cleared/changed while awaiting fetches, set again
|
||||
transactionItems.setPrimaryItem(primaryProduct as any);
|
||||
}
|
||||
// Attach addons to primary
|
||||
if (transactionItems.primaryItem.value) {
|
||||
// Keep any existing addons that may have been set previously only if they are different.
|
||||
transactionItems.primaryItem.value.addons = preparedAddons as any;
|
||||
primaryProduct.addons = preparedAddons as any;
|
||||
}
|
||||
|
||||
lastAppliedBookingId.value = booking.id;
|
||||
//console.warn('Applied pending booking to cart (primary + addons):', booking.id, primaryProduct, preparedAddons);
|
||||
//console.warn('Current transaction items after applying booking:', transactionItems.primaryItem.value);
|
||||
lastFetchedPrimaryItemProduct.value = primaryProduct; // Update last fetched primary item
|
||||
// Hide the vehicle selection as booking has been applied
|
||||
vehicleSelection.value = false;
|
||||
} catch (e) {
|
||||
console.error('Failed to apply pending booking to Step 2', e);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPrimaryItemProduct = () => {
|
||||
if (!vehicles.vehicle_1.value.type && !transactionItems.primaryItem.value) {
|
||||
vehicleSelection.value = true; // Force the user to select a product.
|
||||
@@ -158,6 +286,7 @@ const layout = {
|
||||
const primaryProduct = ref(null);
|
||||
const pointerDownTime = ref<Date>(null);
|
||||
const pointerUpTime = ref<Date>(null);
|
||||
const pointerDownScrollPosition = ref(0); // Track the scroll position when the pointer is pressed down (To prevent triggering when scrolling with touch)
|
||||
const pointerDown = () => {
|
||||
// Record the time when the pointer is pressed down
|
||||
const instanceTime = new Date();
|
||||
@@ -184,6 +313,19 @@ const pointerUp = () => {
|
||||
pointerUpTime.value = new Date();
|
||||
};
|
||||
|
||||
const onScroll = (event: Event) => {
|
||||
// If the user scrolls while the pointer is down, cancel the long press detection
|
||||
if (pointerDownTime.value) {
|
||||
const target = event.target as HTMLElement;
|
||||
const currentScrollPosition = target.scrollTop;
|
||||
// If the scroll position has changed significantly, cancel the long press
|
||||
if (Math.abs(currentScrollPosition - pointerDownScrollPosition.value) > 10) {
|
||||
pointerDownTime.value = null;
|
||||
pointerUpTime.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onBeforeComplete = () => {
|
||||
// This function is called before the step is completed
|
||||
console.log('On before complete called');
|
||||
@@ -195,56 +337,6 @@ const onBeforeComplete = () => {
|
||||
}
|
||||
// If the transaction items contain a wash certificate, create a booking for it
|
||||
const hasWashCertificate = transactionItems.containsWashCertificate();
|
||||
let bookingPromise = Promise.resolve(true);
|
||||
if (hasWashCertificate && !metadata.getBookingId()) {
|
||||
// Create a booking for the wash
|
||||
//SessionUser.objects.forms.functions.submit(
|
||||
bookingPromise = <Promise<any>>SessionUser.objects.forms.functions.submit(
|
||||
{
|
||||
/**
|
||||
* {
|
||||
* "id": "BOOK_WASH",
|
||||
* "BOOK_WASH_field_department_id": 2,
|
||||
* "BOOK_WASH_field_wash_type": [
|
||||
* 3
|
||||
* ],
|
||||
* "BOOK_WASH_field_customer_number": 12345678,
|
||||
* "BOOK_WASH_field_contact_email": "example@truckwash.dk",
|
||||
* "BOOK_WASH_field_reference": "example",
|
||||
* "BOOK_WASH_field_registration_number_tractor": "example123",
|
||||
* "BOOK_WASH_field_registration_number_trailer": "example456",
|
||||
* "BOOK_WASH_field_wants_wash_certificate": true,
|
||||
* "BOOK_WASH_field_wants_wash_certificate_email": "example@truckwash.dk",
|
||||
* "BOOK_WASH_field_date": "2025-09-23",
|
||||
* "BOOK_WASH_field_wants_pickup": false,
|
||||
* "BOOK_WASH_field_notes": "Dette er et exempel."
|
||||
* }
|
||||
*/
|
||||
id: 'BOOK_WASH',
|
||||
BOOK_WASH_field_department_id: department_id.value,
|
||||
BOOK_WASH_field_wash_type: [3],
|
||||
BOOK_WASH_field_customer_number: customer_id.value,
|
||||
BOOK_WASH_field_contact_email: getCustomerEmail() || '',
|
||||
BOOK_WASH_field_reference: reference.value || '',
|
||||
BOOK_WASH_field_registration_number_tractor: vehicles.vehicle_1.value?.reg || '',
|
||||
BOOK_WASH_field_registration_number_trailer: vehicles.vehicle_2.value?.reg || 'INGEN',
|
||||
BOOK_WASH_field_wants_wash_certificate: true,
|
||||
BOOK_WASH_field_wants_wash_certificate_email: getCustomerEmail() || '',
|
||||
BOOK_WASH_field_date: new Date().toISOString().split('T')[0], // Set the date to today
|
||||
BOOK_WASH_field_wants_pickup: false,
|
||||
BOOK_WASH_field_notes: notes.value || '',
|
||||
}
|
||||
).then(response => {
|
||||
console.warn('Booking created for wash certificate:', response.data.data);
|
||||
// Attach the booking ID to the order metadata
|
||||
metadata.setBookingId(response.data.data.id);
|
||||
SessionUser.objects.orders.set.booking_id(order_id, response.data.data.id);
|
||||
resolve(true);
|
||||
}).catch(error => {
|
||||
console.error('Error creating booking for wash certificate:', error);
|
||||
reject(new Error('Error creating booking for wash certificate'));
|
||||
});
|
||||
}
|
||||
// Add the order items to the transaction
|
||||
SessionUser.objects.global.add.object(
|
||||
'/order/items',
|
||||
@@ -296,7 +388,7 @@ const onBeforeComplete = () => {
|
||||
// Combine addon and additional item promises
|
||||
addonPromises.push(...additionalPromises);
|
||||
// IF the booking is being created, wait for it to finish first
|
||||
return bookingPromise.then(() => Promise.all(addonPromises));
|
||||
return Promise.all(addonPromises);
|
||||
}).then(() => {
|
||||
// After successfully adding the order item, resolve the promise
|
||||
resolve(true);
|
||||
@@ -307,6 +399,14 @@ const onBeforeComplete = () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Re-try applying booking whenever booking id or vehicle reg changes
|
||||
watch(() => metadata.bookingId?.value, () => {
|
||||
applyPendingBookingFromSelection();
|
||||
});
|
||||
watch(() => vehicles?.vehicle_1?.value?.reg, () => {
|
||||
applyPendingBookingFromSelection();
|
||||
});
|
||||
|
||||
// Watch for changes in the primary item and update the vehicle selection if necessary
|
||||
// Utility: map source addons and carry over quantities from previous addons by product.id
|
||||
const mapAddonsWithQuantity = (sourceAddons = [], previousAddons = []) =>
|
||||
@@ -399,6 +499,8 @@ watch(() => vehicles.vehicle_1.value.reference, (newValue, oldValue) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -406,10 +508,12 @@ watch(() => vehicles.vehicle_1.value.reference, (newValue, oldValue) => {
|
||||
<PosDepartmentStep2MobileVehicleSelection @close="vehicleSelection = false"/>
|
||||
<!-- Buttons -->
|
||||
<PosDepartmentStepMobileFixedBottomControl>
|
||||
<!-- Buttons -->
|
||||
<!-- Next button -->
|
||||
<PosDepartmentStepMobileButtonNextStep :isDark="true" :customAction="() => { vehicleSelection = false; fetchPrimaryItemProduct(); }">
|
||||
<span class="is-float-left has-text-white">{{ SessionUser.objects.global.language.next }}</span>
|
||||
<span class="is-float-right has-text-white">
|
||||
<PosDepartmentStepMobileButtonNextStep :isWhite="false" :customAction="() => { vehicleSelection = false; fetchPrimaryItemProduct(); }" :buttonClasses="['has-background-primary', 'has-text-black']" v-show="vehicles.vehicle_1.value.type">
|
||||
<span class="is-float-left " v-if="false">{{ SessionUser.objects.global.language.next }}</span>
|
||||
<span class="is-float-left">{{ primaryItem ? primaryItem.name : SessionUser.objects.global.language.next }}</span>
|
||||
<span class="is-float-right ">
|
||||
<!-- Arrow right icon -->
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</span>
|
||||
@@ -422,7 +526,8 @@ watch(() => vehicles.vehicle_1.value.reference, (newValue, oldValue) => {
|
||||
<template v-else>
|
||||
<div class="is-flex is-flex-direction-column is-justify-content-space-between is-gap-2">
|
||||
<!-- Customer selection -->
|
||||
<PosDepartmentStepMobile2Customer subtitle="" :label="customer_name"/>
|
||||
<h3 class="is-size-5 has-text-weight-bold">{{ customer_name }}</h3>
|
||||
<PosDepartmentStepMobile2Customer subtitle="" :label="customer_name" v-show="false"/>
|
||||
<!-- Registration numbers -->
|
||||
<PosDepartmentStepMobile2RegistrationNumbers :classes="layout.classes" />
|
||||
<!-- Product -->
|
||||
@@ -441,7 +546,7 @@ watch(() => vehicles.vehicle_1.value.reference, (newValue, oldValue) => {
|
||||
@click="lastOrders.select(1)"
|
||||
:classes="layout.classes" :label="'ordered on...'" :subtitle="'Subtitle'" :lastOrder="lastOrders.get(1)"/>
|
||||
<!-- Addons -->
|
||||
<PosDepartmentStepMobile2Addons :addons="transactionItems.primaryItem.value?.addons || []" :label="'Add-ons'"/>
|
||||
<PosDepartmentStepMobile2Addons :addons="transactionItems.primaryItem.value?.addons || []" :label="'Add-ons'" :compact="false"/>
|
||||
<!-- Notes -->
|
||||
<ControlField>
|
||||
<ControlFieldInputLabel label="Notes" :classes="layout.classes" :optional="true"/>
|
||||
@@ -457,9 +562,9 @@ watch(() => vehicles.vehicle_1.value.reference, (newValue, oldValue) => {
|
||||
<!-- Buttons -->
|
||||
<PosDepartmentStepMobileFixedBottomControl>
|
||||
<!-- Complete button -->
|
||||
<PosDepartmentStepMobileButtonNextStep :isDark="true" :onBeforeStep="() => onBeforeComplete()">
|
||||
<span class="is-float-left has-text-white">Gennemfør</span>
|
||||
<span class="is-float-right has-text-white">{{ SessionUser.functions.currency.toLocal(transactionItems.getTransactionTotal())}}</span>
|
||||
<PosDepartmentStepMobileButtonNextStep :isDark="false" :onBeforeStep="() => onBeforeComplete()" :buttonClasses="['has-background-primary', 'has-text-white']">
|
||||
<span class="is-float-left">Afslut</span>
|
||||
<span class="is-float-right">{{ SessionUser.functions.currency.toLocal(transactionItems.getTransactionTotal())}}</span>
|
||||
</PosDepartmentStepMobileButtonNextStep>
|
||||
<div>
|
||||
<PosDepartmentStepMobileButtonClearAll/>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useGeolocation } from "@vueuse/core";
|
||||
import { watch } from "vue";
|
||||
import { watch, defineEmits } from "vue";
|
||||
import { locations } from "../objects/PosDepartmentStepMobileFlow.vue";
|
||||
const emits = defineEmits<{
|
||||
(e: 'location-updated', coords: { latitude: number | null; longitude: number | null }): void;
|
||||
}>();
|
||||
const { coords, locatedAt, error, resume, pause } = useGeolocation({
|
||||
enableHighAccuracy: true,
|
||||
maximumAge: 30000,
|
||||
@@ -18,6 +21,7 @@ const onUpdate = (newCoords: { latitude: number | null; longitude: number | null
|
||||
timestamp: new Date(),
|
||||
errorMessage: error.value ? error.value.message : null,
|
||||
})
|
||||
emits('location-updated', newCoords);
|
||||
}
|
||||
};
|
||||
watch(coords, (newCoords) => {
|
||||
|
||||
@@ -12,16 +12,18 @@ const props = defineProps({
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue', 'update:hasFocus']);
|
||||
const emit = defineEmits(['update:modelValue', 'update:hasFocus', 'onInput']);
|
||||
const modelValue = ref(props.modelValue);
|
||||
const hasFocus = ref(false);
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
modelValue.value = newValue;
|
||||
modelValue.value = newValue.toUpperCase();
|
||||
});
|
||||
watch(modelValue, (newValue) => {
|
||||
modelValue.value = newValue.toUpperCase();
|
||||
emit('update:modelValue', newValue);
|
||||
});
|
||||
watch(hasFocus, (newValue) => {
|
||||
modelValue.value = modelValue.value.toUpperCase();
|
||||
emit('update:hasFocus', newValue);
|
||||
});
|
||||
|
||||
@@ -41,6 +43,10 @@ watch(hasFocus, (newValue) => {
|
||||
}
|
||||
});
|
||||
|
||||
const onInput = (value: string) => {
|
||||
emit('onInput', value);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -55,6 +61,8 @@ watch(hasFocus, (newValue) => {
|
||||
:class="{ 'is-searched': hasFocus }"
|
||||
type="text"
|
||||
v-model="modelValue"
|
||||
@input="onInput($event.target.value.toUpperCase())"
|
||||
autocomplete="off"
|
||||
v-on:focusin="hasFocus = true"
|
||||
v-on:focusout="hasFocus = false"
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,10 @@ const props = defineProps({
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showPrices: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(["update:addons"]);
|
||||
@@ -42,6 +46,63 @@ const getLabel = (addon: Addon) => {
|
||||
//return `${addon.name} / ${SessionUser.functions.currency.toLocal(addon.price)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* When double-clicking on the product, it will add 1 to the quantity
|
||||
*/
|
||||
const doubleClickTimeout = 500; // milliseconds
|
||||
const doubleClickTimers = ref<{ [key: string]: number | null }>({});
|
||||
|
||||
/**
|
||||
* Function to add a product to the add-ons list
|
||||
* @param addon
|
||||
*/
|
||||
const onAddProduct = (addon: Addon) => {
|
||||
// Check if the product was just added
|
||||
if (doubleClickTimers.value[addon.id]) {
|
||||
// Double-click detected, clear the timeout
|
||||
clearTimeout(doubleClickTimers.value[addon.id]);
|
||||
doubleClickTimers.value[addon.id] = null;
|
||||
// Check if the product can have more quantity
|
||||
if (addon.quantity >= addon.max && addon.max !== -1) {
|
||||
// If the product max is reached, it will not be added.
|
||||
return;
|
||||
}
|
||||
addon.quantity = addon.quantity + 1;
|
||||
emit('update:addons', [...props.addons])
|
||||
// Set the timeout again to allow for the next double-click
|
||||
doubleClickTimers.value[addon.id] = window.setTimeout(() => {
|
||||
doubleClickTimers.value[addon.id] = null;
|
||||
}, doubleClickTimeout);
|
||||
return;
|
||||
}
|
||||
// Single-click detected, add the product to the list
|
||||
// If the product is already has a quantity, set it to 0
|
||||
if (addon.quantity > 0) {
|
||||
addon.quantity = 0;
|
||||
emit('update:addons', [...props.addons])
|
||||
return;
|
||||
}
|
||||
addon.quantity = (addon.min > 0 ? addon.min : 1);
|
||||
// Set a timeout to handle double-clicks.
|
||||
// If the product max is reached, it will not be added.
|
||||
if (addon.quantity >= addon.max && addon.max !== -1) {
|
||||
// If the product max is reached, it will not be added.
|
||||
emit('update:addons', [...props.addons])
|
||||
return;
|
||||
}
|
||||
doubleClickTimers.value[addon.id] = window.setTimeout(() => {
|
||||
doubleClickTimers.value[addon.id] = null;
|
||||
}, doubleClickTimeout);
|
||||
emit('update:addons', [...props.addons])
|
||||
}
|
||||
|
||||
const sortAddonsByProductOrderPriority = (addons: Addon[]) => {
|
||||
return addons.sort((a, b) => {
|
||||
const priorityA = a.product.order_priority || 0;
|
||||
const priorityB = b.product.order_priority || 0;
|
||||
return priorityA - priorityB;
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -49,13 +110,14 @@ const getLabel = (addon: Addon) => {
|
||||
<template v-if="addons.length > 0"> <!-- If there are add-ons available -->
|
||||
<p class="custom-label">{{ props.label }}</p>
|
||||
<div>
|
||||
<template v-for="addon in props.addons" :key="addon.id">
|
||||
<template v-for="addon in sortAddonsByProductOrderPriority(props.addons)" :key="addon.id">
|
||||
<PosDepartmentStepMobile2CategoryProduct
|
||||
:compact="props.compact"
|
||||
:price="addon.price"
|
||||
:label="getLabel(addon)"
|
||||
:piktogram="addon.product.piktogram"
|
||||
@addProduct="addon.quantity = (addon.min > 0 ? addon.min : 1); emit('update:addons', [...props.addons])"
|
||||
:showPrices="props.showPrices"
|
||||
@addProduct="onAddProduct(addon)"
|
||||
:customButton="addon.quantity > 0">
|
||||
<span class="custom-select-quantity-container">
|
||||
<ControlSelectAmount
|
||||
@@ -180,7 +242,7 @@ const getLabel = (addon: Addon) => {
|
||||
}
|
||||
.custom-label {
|
||||
/* Add-ons */
|
||||
|
||||
padding-bottom: 24px;
|
||||
width: 60px;
|
||||
height: 16px;
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import GenericTag from "@/components/viewport/page/templates/generic/graphics/GenericTag.vue";
|
||||
import { pos } from "../objects/PosDepartmentStepMobileFlow.vue";
|
||||
import {onMounted} from "vue";
|
||||
import {onMounted, defineProps} from "vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
// Function to fetch categories based on the department ID
|
||||
const props = defineProps({
|
||||
// Optional list of category IDs to restrict the displayed categories to
|
||||
restrictCategories: {
|
||||
type: Array as () => number[],
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
// This function will be called when the component is mounted
|
||||
const fetchCategories = async (departmentId: number) => {
|
||||
await SessionUser.objects.department_categories.get.single(departmentId)
|
||||
@@ -16,7 +24,11 @@ const fetchCategories = async (departmentId: number) => {
|
||||
// name: "Suggested",
|
||||
// });
|
||||
// Add the categories to the pos.categories object
|
||||
response.forEach((result: {category: {id: number, name: string}}) => {
|
||||
SessionUser.objects.department_categories.functions.orderCategories(response).forEach((result: {category: {id: number, name: string}}) => {
|
||||
// If restrictCategories prop is set, only add categories that are in the list
|
||||
if (props.restrictCategories.length > 0 && !props.restrictCategories.includes(result.category.id)) {
|
||||
return;
|
||||
}
|
||||
pos.categories.add({
|
||||
id: result.category.id,
|
||||
name: result.category.name,
|
||||
|
||||
@@ -29,6 +29,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
},
|
||||
showPrices: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -61,25 +65,28 @@ const imageStyleCompact = {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="border-bottom: 1px solid #E5E5E5; padding: 10px 0;">
|
||||
<div class="columns is-vcentered is-mobile">
|
||||
<div class="column is-narrow">
|
||||
<div style="border-bottom: 1px solid #E5E5E5; padding: 10px 0;" class="mb-1">
|
||||
<div class="columns is-vcentered is-mobile" :class="props.customButton ? 'has-quantity' : ''">
|
||||
<div class="column is-narrow" @click="onClick" style="cursor: pointer;">
|
||||
<img :src="getPicture(props.piktogram)" :alt="props.label" class="product-image"
|
||||
:style="props.compact ? imageStyleCompact : imageSize"
|
||||
style="border-radius: 8px; object-fit: contain;" />
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="column" @click="onClick" style="cursor: pointer;">
|
||||
<template v-if="!props.compact">
|
||||
<h1 class="title is-6">{{ props.label }}</h1>
|
||||
<h2 class="subtitle is-6">{{ SessionUser.functions.currency.toLocal(props.price) }}</h2>
|
||||
<h2 class="subtitle is-6" v-if="props.showPrices">{{ SessionUser.functions.currency.toLocal(props.price) }}</h2>
|
||||
<h2 class="subtitle is-6" v-else> </h2>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1 class="title is-6" style="font-size: small">{{ props.label }}</h1>
|
||||
<h2 class="subtitle is-6" style="font-size: smaller">{{ SessionUser.functions.currency.toLocal(props.price) }}</h2>
|
||||
<h2 class="subtitle is-6" style="font-size: smaller" v-if="props.showPrices">{{ SessionUser.functions.currency.toLocal(props.price) }}</h2>
|
||||
<h2 class="subtitle is-6" style="font-size: smaller" v-else> </h2>
|
||||
</template>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<button class="custom-button-product" @click="onClick" v-if="!props.customButton" :style="props.compact ? imageStyleCompact : imageSize">
|
||||
<slot name="right"/>
|
||||
<button class="custom-button-product" @click="onClick" v-if="!props.customButton" :style="props.compact ? imageStyleCompact : imageSize" v-show="false">
|
||||
<span class="custom-icon">
|
||||
<Plus width="100%" height="100%" />
|
||||
</span>
|
||||
@@ -135,4 +142,14 @@ const imageStyleCompact = {
|
||||
padding: 0px;
|
||||
gap: 0px;
|
||||
}
|
||||
.has-quantity {
|
||||
border-radius: 10px;
|
||||
background-color: #5173d4;
|
||||
}
|
||||
.has-quantity .column .title {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.has-quantity .column .subtitle {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
</style>
|
||||
@@ -15,23 +15,23 @@ const props = defineProps({
|
||||
|
||||
<template>
|
||||
<!-- Box -->
|
||||
<div class="box">
|
||||
<div class="box" style="border: 3px solid rgb(0,45,255);">
|
||||
<!-- Product image -->
|
||||
<div class="has-text-centered">
|
||||
<img :src="getPicture(props?.product?.piktogram || props?.product?.id || '')" :alt="props?.product?.name || 'N/A'" class="product-image">
|
||||
</div>
|
||||
<!-- Product id -->
|
||||
<div class="tag mb-2">
|
||||
<div class="tag mb-2" v-show="false">
|
||||
<span>No. {{ props?.product?.piktogram || props?.product?.id }}</span>
|
||||
</div>
|
||||
<!-- Product name and price -->
|
||||
<div class="columns is-mobile is-vcentered">
|
||||
<!-- Product name -->
|
||||
<div class="column is-6">
|
||||
<div class="column is-narrow">
|
||||
<p class="title is-5">{{ props?.product?.name || '' }}</p>
|
||||
</div>
|
||||
<!-- Product price -->
|
||||
<div class="column is-6 has-text-right">
|
||||
<div class="column has-text-right" v-show="false">
|
||||
<p class="title is-5 is-grey">{{ SessionUser.functions.currency.toLocal(props?.product?.price || 0) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {watch, defineEmits, defineProps, ref} from "vue";
|
||||
import {watch, defineEmits, defineProps, ref, defineSlots} from "vue";
|
||||
import { pos } from "../objects/PosDepartmentStepMobileFlow.vue";
|
||||
import PosDepartmentStepMobile2CategoryProduct
|
||||
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2CategoryProduct.vue";
|
||||
@@ -10,6 +10,7 @@ import { department_id, customer_id } from "@/components/shop/POSDepartmentProce
|
||||
import PosDepartmentStepMobile2Addons
|
||||
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Addons.vue";
|
||||
import {Addon} from "@/components/displays/department/pos/steps/mobile/objects/PosAddon.vue";
|
||||
import { orderProducts } from "@/components/shop/Products.vue";
|
||||
|
||||
const emits = defineEmits(["addProduct"]);
|
||||
const props = defineProps({
|
||||
@@ -26,6 +27,7 @@ const props = defineProps({
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const slots = defineSlots();
|
||||
|
||||
// Function to fetch categories based on the department ID
|
||||
// This function will be called when the component is mounted
|
||||
@@ -34,7 +36,7 @@ const fetchProducts = async (category: PosCategory, departmentId: number) => {
|
||||
.then((response) => {
|
||||
console.log("Products fetched for category:", category.name, response);
|
||||
pos.productList.clear();
|
||||
response.forEach((result: PosProduct) => {
|
||||
orderProducts(response).forEach((result: PosProduct) => {
|
||||
pos.productList.add(result);
|
||||
});
|
||||
// TODO: Update prices of products based on customer pricing rules
|
||||
@@ -110,7 +112,11 @@ watch(() => props.addons, (newAddons) => {
|
||||
:price="product.price"
|
||||
:label="product.name"
|
||||
:piktogram="product.piktogram"
|
||||
/>
|
||||
>
|
||||
<template #right>
|
||||
<slot name="right" :product="product"/>
|
||||
</template>
|
||||
</PosDepartmentStepMobile2CategoryProduct>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
@@ -53,7 +53,7 @@ const parseVehicleStatus = (status: any) => {
|
||||
<div :class="layout.columnClasses">
|
||||
<PosDepartmentStepMobile1RegistrationNumber2 :class="layout.classes" :hideLabel="true" @click="onClick('reg_2', reg_2)" />
|
||||
</div>
|
||||
<div :class="layout.columnClasses">
|
||||
<div :class="layout.columnClasses" v-show="!!pos.vehicles.vehicle_3.value">
|
||||
<PosDepartmentStepMobile1RegistrationNumber3 :class="layout.classes" :hideLabel="true" @click="onClick('reg_3', reg_3)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import ContinueArrow from "@/components/viewport/elements/icons/ContinueArrow.vu
|
||||
import { vehicles, metadata, getCustomerId, popups, resetPos, attachments, transactionHistory } from "../objects/PosDepartmentStepMobileFlow.vue";
|
||||
import Swal from "sweetalert2";
|
||||
import LongPressListener from "@/components/viewport/elements/wrappers/LongPressListener.vue";
|
||||
import {views} from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
||||
const selectCustomerString = `${SessionUser.objects.global.language.select} ${SessionUser.objects.global.language.customer.toLowerCase()}`;
|
||||
|
||||
const props = defineProps({
|
||||
@@ -40,13 +41,20 @@ const props = defineProps({
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
// Button classes
|
||||
buttonClasses: {
|
||||
type: Array as () => string[],
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const updateReference = () => {
|
||||
if (!!order_id.value && order_id.value > 0)
|
||||
SessionUser.objects.orders.set.reference(order_id.value, reference.value);
|
||||
}
|
||||
|
||||
const updateNotes = () => {
|
||||
if (!!order_id.value && order_id.value > 0)
|
||||
SessionUser.objects.orders.set.notes(order_id.value, order_notes.value);
|
||||
}
|
||||
|
||||
@@ -136,6 +144,8 @@ const step1 = () => {
|
||||
reg_1.value = vehicles.vehicle_1.value?.reg || "";
|
||||
reg_2.value = vehicles.vehicle_2.value?.reg || "";
|
||||
reg_3.value = vehicles.vehicle_3.value?.reg || "";
|
||||
// Reset the vehicle selection
|
||||
views.vehicleSelection.value = false;
|
||||
// Merge references and notes
|
||||
mergeReferences();
|
||||
mergeNotes();
|
||||
@@ -226,9 +236,15 @@ const step2 = () => {
|
||||
// If there is a booking ID, complete the booking
|
||||
SessionUser.objects.orders.set.booking_id(order_id.value, metadata.getBookingId());
|
||||
// Check if the booking has been completed
|
||||
SessionUser.objects.bookings.functions.isBookingCompleted(metadata.getBookingId()).then((isBookingCompleted: boolean) => {
|
||||
SessionUser.objects.order_bookings.get.single(metadata.getBookingId()).then((booking: any) => {
|
||||
const isBookingCompleted = booking?.order_id && booking.order_id > 0;
|
||||
if (!isBookingCompleted) {
|
||||
popups.select('complete_booking', {});
|
||||
// Set the order bookings id to the metadata
|
||||
if (metadata.getBookingId() && metadata.getBookingId() > 0) {
|
||||
SessionUser.objects.order_bookings.set.order_id(metadata.getBookingId(), order_id.value).then(() => {
|
||||
popups.select('complete_booking', {});
|
||||
});
|
||||
}
|
||||
// Listen for the booking to be completed
|
||||
awaitBookingCompletion(() => {
|
||||
completeOrder();
|
||||
@@ -425,11 +441,20 @@ const defaultLongPressBehavior = () => {
|
||||
popups.select('select_customer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Colors:
|
||||
* Primary:
|
||||
* :buttonClasses="['has-background-primary', 'has-text-black']"
|
||||
* Secondary:
|
||||
* :buttonClasses="['has-background-primary-dark', 'has-text-black']"
|
||||
*/
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LongPressListener @long-press="onLongPress">
|
||||
<GenericButton @click="onClick" :class="{'white': props.isWhite, 'has-background-black': props.isDark, 'is-loading': metadata.getLoadingState()}"
|
||||
<GenericButton @click="onClick" :class="{'white': props.isWhite, 'has-background-black': props.isDark, 'is-loading': metadata.getLoadingState(), ...(props.buttonClasses.reduce((acc, curr) => ({ ...acc, [curr]: true }), {}))}"
|
||||
class="is-size-6-mobile is-size-5-tablet is-size-4-desktop"
|
||||
:disabled="!isRequirementsForClickMet()" v-show="isVisible">
|
||||
<!-- Default content if no slot is provided -->
|
||||
<template v-if="!$slots.default">
|
||||
@@ -457,5 +482,42 @@ const defaultLongPressBehavior = () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.has-text-black >:not(.icon) {
|
||||
color: black !important;
|
||||
}
|
||||
/* Loading state */
|
||||
.is-loading {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.is-loading > *:not(.icon) {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s ease-in-out;
|
||||
}
|
||||
/* Loading animation for the button */
|
||||
.is-loading::after {
|
||||
transition: opacity 0.6s ease-in-out;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: auto;
|
||||
left: 50%;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-top: -0.5em;
|
||||
margin-left: -0.5em;
|
||||
/* Set the color to the original text color */
|
||||
border: 0.2em solid white;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,231 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineEmits, watch, ref } from "vue";
|
||||
import { PosSearchResult } from "../objects/PosSearchResult.vue";
|
||||
import CustomerSearchField from "@/components/search/economic/customerSearchField.vue";
|
||||
import { searchCustomerResults, isSearching } from "@/components/search/economic/customerSearch.vue";
|
||||
import ControlFieldInputSearchResults
|
||||
from "@/components/viewport/elements/controls/fields/search/ControlFieldInputSearchResults.vue";
|
||||
import { searchAndSelectCustomer } from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import { metadata, vehicles } from "../objects/PosDepartmentStepMobileFlow.vue";
|
||||
import VehicleCustomerSuggestionsPos from "@/components/forms/department/pos/input/vehicleCustomerSuggestionsPos.vue";
|
||||
import {
|
||||
actionButtons,
|
||||
popups
|
||||
} from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*/
|
||||
const searchQuery = ref("");
|
||||
const searchResult = ref(/**{
|
||||
"vat": 41004355,
|
||||
"status": "Normal",
|
||||
"name": "Truckwash ApS",
|
||||
"address": "Letland Alle 2",
|
||||
"zipcode": 2630,
|
||||
"city": "Taastrup",
|
||||
"protected": true,
|
||||
"phone": "21754690",
|
||||
"website": null,
|
||||
"email": "mikkel@truckwash.dk",
|
||||
"fax": null,
|
||||
"startdate": "2019-12-11",
|
||||
"enddate": null,
|
||||
"employees": 14,
|
||||
"industrycode": 953190,
|
||||
"industrydesc": "Reparation og vedligeholdelse af motorkøretøjer i.a.n.",
|
||||
"companytype": "APS",
|
||||
"companydesc": "Anpartsselskab",
|
||||
"owners": [
|
||||
"DELOITTE STATSAUTORISERET REVISIONSPARTNERSELSKAB",
|
||||
"MBL Revision I/S",
|
||||
"WASH GROUP A/S"
|
||||
]
|
||||
}*/null);
|
||||
const lastSearchResult = ref(null); // This is used to change the autofilled fields, when the search result changes
|
||||
const searching = ref(false);
|
||||
const error = ref(popups.get().props.error); // Used to display an error message if the customer creation fails
|
||||
const search = (query: string) => {
|
||||
return SessionUser.request("/cvr/search", "GET", {
|
||||
query: query,
|
||||
}).then((response) => {
|
||||
console.warn(response, 'response');
|
||||
searchResult.value = response.data.data;
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
searchResult.value = null;
|
||||
}).finally(() => {
|
||||
searching.value = false;
|
||||
changeAutofilledFields();
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Form fields
|
||||
*/
|
||||
const cvr = ref("");
|
||||
const companyPhone = ref("");
|
||||
const invoiceEmail = ref("");
|
||||
const contactEmail = ref("");
|
||||
const contactPhone = ref("");
|
||||
/**
|
||||
* Autofill fields, when search result changes
|
||||
* If the operator has changed the values in the fields, we want to keep them, so we can autofill them again when the search result changes
|
||||
*/
|
||||
const changeAutofilledFields = () => {
|
||||
if (lastSearchResult.value !== null) {
|
||||
cvr.value = "";
|
||||
if (lastSearchResult.value.phone === companyPhone.value) {
|
||||
companyPhone.value = "";
|
||||
}
|
||||
if (lastSearchResult.value.phone === contactPhone.value) {
|
||||
contactPhone.value = "";
|
||||
}
|
||||
if (lastSearchResult.value.email === contactEmail.value) {
|
||||
contactEmail.value = "";
|
||||
}
|
||||
if (lastSearchResult.value.email === invoiceEmail.value) {
|
||||
invoiceEmail.value = "";
|
||||
}
|
||||
}
|
||||
// Fill the fields with the new search result
|
||||
if (searchResult.value !== null) {
|
||||
if (cvr.value === "") {
|
||||
cvr.value = searchResult.value.vat.toString();
|
||||
}
|
||||
if (companyPhone.value === "") {
|
||||
companyPhone.value = searchResult.value.phone;
|
||||
}
|
||||
if (contactPhone.value === "") {
|
||||
contactPhone.value = searchResult.value.phone;
|
||||
}
|
||||
if (contactEmail.value === "") {
|
||||
contactEmail.value = searchResult.value.email;
|
||||
}
|
||||
if (invoiceEmail.value === "") {
|
||||
invoiceEmail.value = searchResult.value.email;
|
||||
}
|
||||
}
|
||||
lastSearchResult.value = searchResult.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch the validity of the fields and emit an event to the parent component
|
||||
* If the fields are valid, we can enable the "Add Customer" button
|
||||
*/
|
||||
watch([cvr, companyPhone, invoiceEmail, contactEmail, contactPhone], (newValues) => {
|
||||
// Remove the "Add Customer" button if it exists
|
||||
const index = popups.get().actionButtons.findIndex((button) => button.label === "Tilføj kunde");
|
||||
if (index !== -1) {
|
||||
popups.get().actionButtons.splice(index, 1);
|
||||
}
|
||||
// Add the "Add Customer" button if all fields are filled
|
||||
const requiredFields = [cvr, companyPhone, contactPhone];
|
||||
if (searchResult.value !== null &&
|
||||
searchResult.value.name !== "Ikke fundet" &&
|
||||
requiredFields.every((field) => field.value !== "")) {
|
||||
popups.get().actionButtons.push({
|
||||
label: "Tilføj kunde",
|
||||
onClick: () => {
|
||||
popups.get().actionButtons = []; // Remove all action buttons to prevent multiple clicks
|
||||
try {
|
||||
SessionUser.request("/auth/register/cvr", "POST", {
|
||||
cvr: cvr.value,
|
||||
companyPhone: parseInt(companyPhone.value),
|
||||
invoiceEmail: invoiceEmail.value,
|
||||
contactEmail: contactEmail.value,
|
||||
contactPhone: parseInt(contactPhone.value),
|
||||
searchResult: searchResult.value,
|
||||
}).then((response) => {
|
||||
console.warn(response, 'response register cvr');
|
||||
searchAndSelectCustomer(companyPhone.value);
|
||||
metadata.setCustomerId(parseInt(companyPhone.value));
|
||||
emit('close');
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
const errorResponse = SessionUser.functions.parseErrorMessage(error);
|
||||
if (errorResponse !== null) {
|
||||
popups.get().props.error = `Der opstod en fejl under oprettelsen af kunden: ${errorResponse}`;
|
||||
// Reset the buttons
|
||||
popups.get().actionButtons = popups.get().actionButtons = [{...actionButtons.default.value.cancel}];
|
||||
return;
|
||||
}
|
||||
popups.get().props.error = "Der opstod en fejl under oprettelsen af kunden. Prøv igen.";
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
popups.get().props.error = "Der opstod en fejl under oprettelsen af kunden. Prøv igen.";
|
||||
}
|
||||
},
|
||||
color: "primary",
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="overflow-y: auto; overflow-x: hidden; height: 100%; padding-bottom: 10px;">
|
||||
<div class="mx-3">
|
||||
<div class="field">
|
||||
<label class="label"><small>CVR</small></label>
|
||||
<div class="control has-icons-right" :class="{'is-loading': searching}">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@input="searching = true; search(searchQuery);"
|
||||
class="input is-searched"
|
||||
:class="{'is-danger': searchResult === null, 'is-success': searchResult !== null && searchResult.name !== 'Ikke fundet'}"
|
||||
type="text"
|
||||
placeholder="Søg CVR nummer..."
|
||||
/>
|
||||
<span class="icon is-right" :class="{'has-text-danger': (searchResult === null || searchResult.name === 'Ikke fundet') && !searching, 'has-text-success': (searchResult !== null && searchResult.name !== 'Ikke fundet') && !searching}">
|
||||
<i class="fas fa-check" v-if="(searchResult !== null && searchResult.name !== 'Ikke fundet') && !searching"></i>
|
||||
<i class="fas fa-exclamation-triangle" v-else-if="(searchResult === null || searchResult.name === 'Ikke fundet') && !searching"></i>
|
||||
<i class="fas fa-search" v-else-if="!searching"></i>
|
||||
</span>
|
||||
</div>
|
||||
<p class="help" v-if="searchResult !== null && searchResult.name !== 'Ikke fundet'">{{ searchResult.name }}</p>
|
||||
</div>
|
||||
<!-- Company phone -->
|
||||
<div class="field">
|
||||
<label class="label"><small>Firma telefon</small></label>
|
||||
<div class="control">
|
||||
<input v-model="companyPhone" class="input" type="text" placeholder="Firma telefon" :disabled="searchResult === null" :class="{'is-danger': searchResult !== null && companyPhone === ''}"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Invoice email -->
|
||||
<div class="field">
|
||||
<label class="label"><small>Faktura email</small></label>
|
||||
<div class="control">
|
||||
<input v-model="invoiceEmail" class="input" type="text" placeholder="Faktura email" :disabled="searchResult === null"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Contact email -->
|
||||
<div class="field">
|
||||
<label class="label"><small>Kontakt email</small></label>
|
||||
<div class="control">
|
||||
<input v-model="contactEmail" class="input" type="text" placeholder="Kontakt email" :disabled="searchResult === null"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Contact phone -->
|
||||
<div class="field">
|
||||
<label class="label"><small>Kontakt telefon</small></label>
|
||||
<div class="control">
|
||||
<input v-model="contactPhone" class="input" type="text" placeholder="Kontakt telefon" :disabled="searchResult === null" :class="{'is-danger': searchResult !== null && contactPhone === ''}"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Error message -->
|
||||
<div class="notification is-danger" v-if="popups.get().props.error !== null">
|
||||
{{ popups.get().props.error}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Search results -->
|
||||
<!--{{ searchResult }}-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, defineEmits, watch, ref } from "vue";
|
||||
import { metadata } from "../objects/PosDepartmentStepMobileFlow.vue";
|
||||
import FormDisplay from "@/components/displays/FormDisplay.vue";
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
@@ -11,18 +12,43 @@ const onSuccessfulCertificateSubmission = (response: any) => {
|
||||
console.log("Certificate generated successfully:", response);
|
||||
emit('close');
|
||||
}
|
||||
|
||||
const safetySealNumber = ref("");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Booking completion form -->
|
||||
<FormDisplay
|
||||
<!-- Safety seal certificate field -->
|
||||
<div class="field">
|
||||
<label class="label">Safety Seal</label>
|
||||
<div class="control">
|
||||
<input
|
||||
type="text"
|
||||
class="input is-searched"
|
||||
placeholder="Indtast safety seal nummer"
|
||||
v-model="safetySealNumber"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Generate certificate button -->
|
||||
<div class="buttons is-centered mb-4">
|
||||
<button class="button is-light" @click="emit('close')">
|
||||
Intet vaskecertifikat
|
||||
</button>
|
||||
<button
|
||||
class="button is-primary"
|
||||
@click="SessionUser.objects.order_bookings.functions.complete(metadata.getBookingId(), safetySealNumber, onSuccessfulCertificateSubmission)">
|
||||
Generer vaskecertifikat
|
||||
</button>
|
||||
</div>
|
||||
<!--<FormDisplay
|
||||
:form_identifier="'GENERATE_BOOKING_WASH_CERTIFICATE'"
|
||||
v-bind:validator_options="{validateBookingId: {default: metadata.getBookingId(), locked: true, visible: false}}"
|
||||
:onSuccessfulSubmission="onSuccessfulCertificateSubmission"
|
||||
:showSuccessSubmitAlert="false"
|
||||
:showResetButton="false"
|
||||
/>
|
||||
/>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ export type Addon = {
|
||||
product?: PosProduct; // The product details for the add-on
|
||||
};
|
||||
|
||||
export type PosAddon = Addon;
|
||||
|
||||
export default defineComponent({
|
||||
name: "PosAddon",
|
||||
});
|
||||
|
||||
@@ -157,6 +157,16 @@ const defaultActionButtons = ref<{ [key: string]: PosActionButton }>({
|
||||
},
|
||||
color: 'primary'
|
||||
},
|
||||
addCustomer: {
|
||||
label: 'Tilføj kunde',
|
||||
description: 'Tilføj en ny kunde til systemet',
|
||||
onClick: () => {
|
||||
console.warn('Add Customer button clicked');
|
||||
// Open the add customer popup
|
||||
popups.select('add_customer', { props: { error: null, canCreate: true } });
|
||||
},
|
||||
color: 'primary',
|
||||
},
|
||||
editReference: {
|
||||
label: 'Redigér reference',
|
||||
description: 'Redigér reference for denne transaktion',
|
||||
@@ -229,7 +239,7 @@ const addDefaultPopups = () => {
|
||||
message: 'Bekræft venligst valget af kunde til denne transaktion',
|
||||
component: 'select_customer',
|
||||
style: {height: '50vh'},
|
||||
actionButtons: [{...defaultActionButtons.value.cancel}],
|
||||
actionButtons: [{...defaultActionButtons.value.cancel}, {...defaultActionButtons.value.addCustomer, label: 'Ny kunde'}],
|
||||
});
|
||||
// Completed
|
||||
addPopup({
|
||||
@@ -315,6 +325,18 @@ const addDefaultPopups = () => {
|
||||
],
|
||||
props: {base64String: null, filename: null}
|
||||
});
|
||||
// Add customer
|
||||
addPopup({
|
||||
id: 'add_customer',
|
||||
title: 'Tilføj kunde',
|
||||
message: 'Indtast venligst oplysninger for den nye kunde',
|
||||
component: 'add_customer',
|
||||
style: {maxHeight: '60vh'},
|
||||
actionButtons: [
|
||||
{...defaultActionButtons.value.cancel}
|
||||
],
|
||||
props: {cvr: null, contactEmail: null, contactPhone: null, companyPhone: null, invoiceEmail: null, searchResult: null, canCreate: false, error: null}
|
||||
});
|
||||
}
|
||||
// Function to check if a popup is defined in the list
|
||||
const isPopupDefined = (id: string): boolean => {
|
||||
|
||||
@@ -20,8 +20,10 @@ import PosDepartmentStepMobilePopupCustomerNotes
|
||||
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupCustomerNotes.vue";
|
||||
import PosDepartmentStepMobilePopupImage
|
||||
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupImage.vue";
|
||||
import PosDepartmentStepMobilePopupAddCustomer
|
||||
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupAddCustomer.vue";
|
||||
|
||||
export type PopupComponentKey = 'select_customer' | 'completed_transaction' | 'complete_booking' | 'error' | 'add_product_note' | 'select_vehicle' | 'change_reference' | 'customer_notes' | 'image_viewer';
|
||||
export type PopupComponentKey = 'select_customer' | 'completed_transaction' | 'complete_booking' | 'error' | 'add_product_note' | 'select_vehicle' | 'change_reference' | 'customer_notes' | 'image_viewer' | 'add_customer';
|
||||
|
||||
export const PopupComponents = {
|
||||
select_customer: PosDepartmentStepMobilePopupSelectCustomer,
|
||||
@@ -33,6 +35,7 @@ export const PopupComponents = {
|
||||
change_reference: PosDepartmentStepMobilePopupSetReference,
|
||||
customer_notes: PosDepartmentStepMobilePopupCustomerNotes,
|
||||
image_viewer: PosDepartmentStepMobilePopupImage,
|
||||
add_customer: PosDepartmentStepMobilePopupAddCustomer
|
||||
} as const;
|
||||
|
||||
export const popupComponentKeyToComponent = (key: PopupComponentKey) => PopupComponents[key];
|
||||
@@ -64,6 +67,7 @@ export default defineComponent({
|
||||
PosDepartmentStepMobilePopupSetReference,
|
||||
PosDepartmentStepMobilePopupCustomerNotes,
|
||||
PosDepartmentStepMobilePopupImage,
|
||||
PosDepartmentStepMobilePopupAddCustomer
|
||||
},
|
||||
props: {
|
||||
popup: {
|
||||
|
||||
@@ -10,6 +10,10 @@ import VerifiedCustomer from "@/components/viewport/elements/icons/VerifiedCusto
|
||||
import { pos } from "../objects/PosDepartmentStepMobileFlow.vue";
|
||||
import { PosVehicle } from "@/components/displays/department/pos/steps/mobile/objects/PosVehicle.vue";
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
import PosDepartmentStepMobileFixedBottomControl
|
||||
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileFixedBottomControl.vue";
|
||||
import PosDepartmentStepMobileButtonNextStep
|
||||
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileButtonNextStep.vue";
|
||||
// Define the close event to emit when the component is closed
|
||||
const emit = defineEmits(["close"]);
|
||||
// Define the props
|
||||
@@ -49,7 +53,7 @@ const registrationNumber3 = ref("");
|
||||
const label3 = ref("Reg 3");
|
||||
|
||||
// Function to set a value if it is not null
|
||||
function setIfNotNull(variable: string, value: { registrationNumber: string, customerStatus: any, customerId?: number } | null) {
|
||||
function setIfNotNull(variable: string, value: { registrationNumber: string, customerStatus: any, customerId?: number, lastOrderId?: number } | null) {
|
||||
//console.warn("setIfNotNull", variable, value);
|
||||
if (value !== null) {
|
||||
switch (variable) {
|
||||
@@ -61,6 +65,7 @@ function setIfNotNull(variable: string, value: { registrationNumber: string, cus
|
||||
reg: value.registrationNumber,
|
||||
status: value.customerStatus,
|
||||
customer_id: value.customerId,
|
||||
last_order_id: value.lastOrderId,
|
||||
}
|
||||
)
|
||||
registrationNumber1.value = value.registrationNumber;
|
||||
@@ -131,6 +136,7 @@ onMounted(() => {
|
||||
// If the reg_3 is not empty, set the expanded state to true
|
||||
expanded.value = reg_3.value !== "" || props.forceShowApplicable;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -141,6 +147,7 @@ onMounted(() => {
|
||||
v-model:modelValue="reg_1"
|
||||
v-model:label="label1"
|
||||
@focusout="setValueUserInput(1, reg_1)"
|
||||
@onInput="reg_1 = $event"
|
||||
>
|
||||
<template #searchResults>
|
||||
<RegistrationNumberSearchResult @select="setIfNotNull('reg_1', $event)" :searchQuery="reg_1" :automaticallySelect="true"/>
|
||||
@@ -183,11 +190,17 @@ onMounted(() => {
|
||||
<span class="m-2"><BarcodeScanner/></span>
|
||||
<span class="custom-label-button">{{ SessionUser.objects.global.language.or_scan_plates}}</span>
|
||||
</button>
|
||||
<!-- Add vehicle button -->
|
||||
<GenericButton class="p-5" @click="$emit('close')">
|
||||
{{ SessionUser.objects.global.language.close}}
|
||||
</GenericButton>
|
||||
</template>
|
||||
<!-- Buttons -->
|
||||
<PosDepartmentStepMobileFixedBottomControl v-if="props.showButtons">
|
||||
<!-- Close button -->
|
||||
<PosDepartmentStepMobileButtonNextStep :isWhite="false" :customAction="() => emit('close')" :customDisabled="false" :buttonClasses="['has-background-primary', 'has-text-black']">
|
||||
<span class="is-float-left">{{ SessionUser.objects.global.language.close }}</span>
|
||||
<span class="is-float-right">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</PosDepartmentStepMobileButtonNextStep>
|
||||
</PosDepartmentStepMobileFixedBottomControl>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -102,9 +102,12 @@ const getTransactionError = (transaction: PosOrder) => {
|
||||
}
|
||||
|
||||
const liveTransactions = ref(null)
|
||||
const isLoading = ref(true);
|
||||
|
||||
const syncListTransactionHistory = async () => {
|
||||
let dateToday = new Date().toISOString().split('T')[0]; // Get today's date in YYYY-MM-DD format
|
||||
isLoading.value = true;
|
||||
// Fetch the list of orders created today for the current department
|
||||
SessionUser.objects.orders.get.list({filters: {
|
||||
'created_at-date_from': dateToday,
|
||||
'created_at-date_to': dateToday,
|
||||
@@ -118,6 +121,10 @@ const syncListTransactionHistory = async () => {
|
||||
for (let i = 0; i < liveTransactions.value.length; i++) {
|
||||
pos.transactionHistory.add(liveTransactions.value[i]);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Error syncing list of transactions:', error);
|
||||
}).finally(() => {;
|
||||
isLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -207,24 +214,17 @@ const isVisible = (transaction: PosOrder, showPending: boolean, showCompleted: b
|
||||
</div>
|
||||
<!-- Buttons -->
|
||||
<PosDepartmentStepMobileFixedBottomControl>
|
||||
<!-- Clear button -->
|
||||
<PosDepartmentStepMobileButtonNextStep class="mb-2" :isDark="true" :customAction="() => pos.transactionHistory.clear()" :customDisabled="false" v-if="pos.transactionHistory.get().length > 0" v-show="false">
|
||||
<span class="is-float-left has-text-white">{{ SessionUser.objects.global.language.clear }}</span>
|
||||
<span class="is-float-right has-text-white">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</span>
|
||||
</PosDepartmentStepMobileButtonNextStep>
|
||||
<!-- Refresh button -->
|
||||
<PosDepartmentStepMobileButtonNextStep class="mb-2" :isDark="true" :customAction="() => syncListTransactionHistory()" :customDisabled="false">
|
||||
<PosDepartmentStepMobileButtonNextStep class="mb-2" :isWhite="false" :customAction="() => syncListTransactionHistory()" :customDisabled="false" :buttonClasses="['has-background-primary-dark', 'has-text-black', ...isLoading ? ['is-loading'] : []]">
|
||||
<span class="is-float-left has-text-white">{{ SessionUser.objects.global.language.reload }}</span>
|
||||
<span class="is-float-right has-text-white">
|
||||
<span class="is-float-right has-text-white is-loading">
|
||||
<i class="fa-solid fa-arrows-rotate"></i>
|
||||
</span>
|
||||
</PosDepartmentStepMobileButtonNextStep>
|
||||
<!-- Close button -->
|
||||
<PosDepartmentStepMobileButtonNextStep :isDark="true" :customAction="() => emit('close')" :customDisabled="false">
|
||||
<span class="is-float-left has-text-white">{{ SessionUser.objects.global.language.close }}</span>
|
||||
<span class="is-float-right has-text-white">
|
||||
<PosDepartmentStepMobileButtonNextStep :isWhite="false" :customAction="() => emit('close')" :customDisabled="false" :buttonClasses="['has-background-primary', 'has-text-black']">
|
||||
<span class="is-float-left">{{ SessionUser.objects.global.language.close }}</span>
|
||||
<span class="is-float-right">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</PosDepartmentStepMobileButtonNextStep>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {defineEmits, defineProps} from "vue";
|
||||
import {computed, defineEmits, defineProps, ref} from "vue";
|
||||
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
|
||||
import { pos } from "../objects/PosDepartmentStepMobileFlow.vue";
|
||||
import GenericLabelSearch from "@/components/viewport/page/templates/generic/graphics/GenericLabelSearch.vue";
|
||||
@@ -37,10 +37,16 @@ const props = defineProps({
|
||||
type: Object as () => Addon[],
|
||||
required: false,
|
||||
},
|
||||
minimumCloseDelay: { // Minimum delay before allowing the component to be closed, in milliseconds
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 750
|
||||
}
|
||||
});
|
||||
|
||||
// Function to handle adding a product
|
||||
const onAddProduct = (product: any) => {
|
||||
if (!canClose()) return;
|
||||
// If a custom onAddProduct function is provided, use it
|
||||
if (props.onAddProduct) {
|
||||
product.quantity = product.quantity || 1; // Ensure quantity is set
|
||||
@@ -60,6 +66,9 @@ const onSearchClick = () => {
|
||||
}
|
||||
console.log("Search clicked");
|
||||
};
|
||||
const openedAt = ref(Date.now());
|
||||
const earliestCloseTime = computed(() => openedAt.value + props.minimumCloseDelay);
|
||||
const canClose = () => Date.now() >= earliestCloseTime.value;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -73,7 +82,14 @@ const onSearchClick = () => {
|
||||
<!-- Product Recommendations -->
|
||||
<!--<PosDepartmentStepMobile2ProductRecommendations/>-->
|
||||
<!-- Products -->
|
||||
<PosDepartmentStepMobile2Products @addProduct="onAddProduct" :asAddons="props.asAddons" :addons="props.addons"/>
|
||||
<PosDepartmentStepMobile2Products @addProduct="onAddProduct" :asAddons="props.asAddons" :addons="props.addons">
|
||||
<template v-slot:right>
|
||||
<!-- Right arrow -->
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</span>
|
||||
</template>
|
||||
</PosDepartmentStepMobile2Products>
|
||||
<!-- Next button
|
||||
<GenericButton class="p-5" @click="emit('close')">
|
||||
Next
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
defineProps(['metaItemsPerPage', 'loadFunction', 'isLoading', 'setMetaItemsPerPage'])
|
||||
defineProps(['metaItemsPerPage', 'loadFunction', 'isLoading', 'setMetaItemsPerPage', 'allowSmallScreen'])
|
||||
import { ref } from 'vue'
|
||||
import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
||||
|
||||
@@ -21,10 +21,10 @@ window.addEventListener('resize', () => {
|
||||
<div class="py-2">
|
||||
<div class="columns is-vcentered is-multiline is-mobile">
|
||||
<!-- Change the number of items per page -->
|
||||
<div class="column is-narrow my-3" :class="{ 'is-hidden': isSmall }">
|
||||
<label class="label">Antal per side</label>
|
||||
<div class="column is-narrow" :class="{ 'is-hidden': isSmall && !allowSmallScreen, 'my-3': !isSmall }">
|
||||
<label class="label is-small">Per side</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<div class="select" :class="{ 'is-small': isSmall }">
|
||||
<select v-model="selectedPerPage" @change="setMetaItemsPerPage(selectedPerPage); selectedPerPage.valueOf = selectedPerPage">
|
||||
<!-- If the selected number of items per page is not in the list, add it to the list -->
|
||||
<option v-if="!perPageOptions.includes(selectedPerPage)" :value="selectedPerPage" :class="{ 'is-skeleton': isLoading }" selected>{{ selectedPerPage }}</option>
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
<script setup lang="ts">
|
||||
import {setFilter, getFilter} from "@/components/pagination/paginatedList.vue";
|
||||
import {computed, defineProps, onMounted, ref} from 'vue';
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
import {BAutocomplete} from "buefy";
|
||||
|
||||
/**
|
||||
* Props:
|
||||
* - departmentColumn: string (default: "department_id")
|
||||
*/
|
||||
const props = defineProps({
|
||||
departmentColumn: {
|
||||
type: String||undefined,
|
||||
default: undefined,
|
||||
required: false
|
||||
},
|
||||
productColumn: {
|
||||
type: String||undefined,
|
||||
default: undefined,
|
||||
required: false
|
||||
},
|
||||
columnLabels: {
|
||||
type: Object||undefined,
|
||||
default: undefined,
|
||||
required: false
|
||||
// Example: { department: 'Department', product: 'Product Type' }
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* General functions (Filters)
|
||||
*/
|
||||
const getOptionsForSelect = (options: {label: string, value: string | number | undefined | null}[], searchQuery: string | undefined): {label: string, value: string | number | undefined | null}[] => {
|
||||
if (!searchQuery) return options;
|
||||
const query = searchQuery.toLowerCase();
|
||||
let filteredOptions = options;
|
||||
// Sort the options so that those starting with the query appear first
|
||||
filteredOptions = filteredOptions.sort((a, b) => {
|
||||
const aStartsWith = a.label.toLowerCase().startsWith(query) ? -1 : 1;
|
||||
const bStartsWith = b.label.toLowerCase().startsWith(query) ? -1 : 1;
|
||||
return aStartsWith - bStartsWith;
|
||||
});
|
||||
// Then filter the options based on the query
|
||||
filteredOptions = filteredOptions.filter(option => option.label.toLowerCase().includes(query));
|
||||
return filteredOptions;
|
||||
}
|
||||
|
||||
type FilterType = {
|
||||
label: string;
|
||||
value: string | number | string[] | number[] | undefined | null;
|
||||
columnName?: string;
|
||||
type?: 'select' | 'input' | 'date' | 'checkbox';
|
||||
options?: {label: string, value: string | number | undefined | null}[];
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
icon?: string;
|
||||
searchable?: boolean;
|
||||
searchQuery?: string;
|
||||
multiple?: boolean;
|
||||
loading?: boolean;
|
||||
loadFunction?: () => Promise<void>;
|
||||
onInput?: (value: string | number | string[] | number[]) => void;
|
||||
onChange?: (value: string | number | string[] | number[]) => void;
|
||||
};
|
||||
|
||||
const setFilterValue = (filterName: string, value: string | number | string[] | number[]) => {
|
||||
setFilter(filterName, value);
|
||||
}
|
||||
|
||||
const getFilterValue = (filterName: string): string | number | string[] | number[] | undefined | null => {
|
||||
return getFilter(filterName);
|
||||
}
|
||||
|
||||
const departmentFilter = ref<FilterType>({
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.departments.meta.labels.single),
|
||||
columnName: props.departmentColumn || 'department_id',
|
||||
type: 'select',
|
||||
multiple: false,
|
||||
loading: false,
|
||||
searchable: true,
|
||||
placeholder: SessionUser.objects.global.language.search,
|
||||
value: props.departmentColumn ? getFilterValue(props.departmentColumn) : undefined,
|
||||
loadFunction: async () => {
|
||||
// Load options dynamically if needed
|
||||
if (departmentFilter.value.loading) return;
|
||||
departmentFilter.value.loading = true;
|
||||
try {
|
||||
// Simulate async loading of options
|
||||
const departments = await SessionUser.objects.departments.get.all();
|
||||
departmentFilter.value.options = [
|
||||
{ label: SessionUser.objects.global.language.all, value: '*' },
|
||||
...departments.map((dept: any) => ({
|
||||
label: dept.name,
|
||||
value: dept.id
|
||||
}))
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('Error loading departments:', error);
|
||||
} finally {
|
||||
departmentFilter.value.loading = false;
|
||||
}
|
||||
},
|
||||
options: [
|
||||
{ label: SessionUser.objects.global.language.all, value: '*' },
|
||||
// Additional options can be loaded dynamically
|
||||
],
|
||||
onChange: (value: string | number | string[] | number[]) => {
|
||||
setFilterValue(props.departmentColumn || 'department_id', value);
|
||||
departmentFilter.value.searchQuery = value.toString();
|
||||
departmentFilter.value.value = value;
|
||||
},
|
||||
onInput: (value: string | number | string[] | number[]) => {
|
||||
departmentFilter.value.searchQuery = value.toString();
|
||||
},
|
||||
});
|
||||
|
||||
const productFilter = ref<FilterType>({
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.products.meta.labels.single),
|
||||
columnName: props.productColumn || 'product',
|
||||
type: 'select',
|
||||
multiple: false,
|
||||
loading: false,
|
||||
searchable: true,
|
||||
placeholder: SessionUser.objects.global.language.search,
|
||||
value: props.productColumn ? getFilterValue(props.productColumn) : undefined,
|
||||
loadFunction: async () => {
|
||||
// Load options dynamically if needed
|
||||
if (productFilter.value.loading) return;
|
||||
productFilter.value.loading = true;
|
||||
try {
|
||||
// Simulate async loading of options
|
||||
const productTypes = await SessionUser.objects.vehicles.columns.type.options();
|
||||
productFilter.value.options = [
|
||||
{ label: SessionUser.objects.global.language.all, value: '*' },
|
||||
...productTypes.map((type: any) => ({
|
||||
label: type.name,
|
||||
value: type.id
|
||||
}))
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('Error loading product types:', error);
|
||||
} finally {
|
||||
productFilter.value.loading = false;
|
||||
}
|
||||
},
|
||||
options: [
|
||||
{ label: SessionUser.objects.global.language.all, value: '*' },
|
||||
// Additional options can be loaded dynamically
|
||||
],
|
||||
onChange: (value: string | number | string[] | number[]) => {
|
||||
setFilterValue(props.productColumn || 'product', value);
|
||||
productFilter.value.searchQuery = value.toString();
|
||||
productFilter.value.value = value;
|
||||
},
|
||||
onInput: (value: string | number | string[] | number[]) => {
|
||||
productFilter.value.searchQuery = value.toString();
|
||||
},
|
||||
});
|
||||
|
||||
const filters = computed<FilterType[]>(() => {
|
||||
const filterList: FilterType[] = [];
|
||||
if (props.departmentColumn) {
|
||||
filterList.push(departmentFilter.value);
|
||||
}
|
||||
if (props.productColumn) {
|
||||
filterList.push(productFilter.value);
|
||||
}
|
||||
// Additional filters can be added here
|
||||
return filterList;
|
||||
});
|
||||
|
||||
/**
|
||||
* Auto load options for filters
|
||||
*/
|
||||
const loadFilters = async () => {
|
||||
for (const filter of filters.value) {
|
||||
// Load options dynamically if needed
|
||||
if (filter.loadFunction) {
|
||||
await filter.loadFunction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isFilterSet = (filter: FilterType): boolean => {
|
||||
if (filter.multiple) {
|
||||
return Array.isArray(filter.value) && filter.value.length > 0;
|
||||
} else {
|
||||
return filter.value !== undefined && filter.value !== null && filter.value !== '*';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadFilters();
|
||||
});
|
||||
|
||||
const renderFilterName = (filter: FilterType) => {
|
||||
return props?.columnLabels?.[filter.columnName] || filter.label
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template
|
||||
v-for="filter in filters"
|
||||
:key="filter.label"
|
||||
>
|
||||
<!-- Select -->
|
||||
<template v-if="filter.type === 'select'">
|
||||
<div class="column is-narrow">
|
||||
<div class="field mb-0">
|
||||
<!-- Label -->
|
||||
<label class="label is-small">
|
||||
{{ renderFilterName(filter) }}
|
||||
<span class="is-float-right">
|
||||
<!-- Reset "button" -->
|
||||
<template v-if="isFilterSet(filter)">
|
||||
<span @click="filter.onChange ? filter.onChange(filter.multiple ? [] : '*') : null" class="is-clickable">
|
||||
<span class="mr-1">
|
||||
{{ SessionUser.objects.global.language.reset }}
|
||||
</span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-times-circle"></i>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</span>
|
||||
</label>
|
||||
<div class="control">
|
||||
<!-- Single select -->
|
||||
<div class="select" v-if="!filter.multiple">
|
||||
<!-- Searchable (autocomplete) -->
|
||||
<BAutocomplete
|
||||
v-if="filter.searchable"
|
||||
:loading="filter.loading"
|
||||
:placeholder="filter.placeholder || ''"
|
||||
:open-on-focus="true"
|
||||
:icon="filter.icon || ''"
|
||||
:data="getOptionsForSelect(filter.options || [], filter.searchQuery).map(option => option.label) || []"
|
||||
:model-value="filter.options?.find(option => option.value === filter.value)?.label || ''"
|
||||
@input="(value) => {
|
||||
const selectedOption = filter.options?.find(option => option.label === value);
|
||||
if (selectedOption) {
|
||||
filter.onChange ? filter.onChange(selectedOption.value) : null;
|
||||
}
|
||||
console.warn('Search input: ', value.target.value);
|
||||
filter.onInput ? filter.onInput(value.target.value) : null;
|
||||
}"
|
||||
@select="(value) => {
|
||||
const selectedOption = filter.options?.find(option => option.label === value);
|
||||
if (selectedOption) {
|
||||
filter.onChange ? filter.onChange(selectedOption.value) : null;
|
||||
}
|
||||
}"
|
||||
/>
|
||||
<!-- Regular select -->
|
||||
<select
|
||||
v-else
|
||||
:value="filter.value"
|
||||
@change="(event) => {
|
||||
const selectedValue = event.target.value;
|
||||
filter.onChange ? filter.onChange(selectedValue) : null;
|
||||
}"
|
||||
>
|
||||
<option
|
||||
v-for="option in filter.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>{{ option.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Multiple select goes here when relevant... -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Date Picker -->
|
||||
<!-- ... Other filter types can be added here ... -->
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import {search, metaSearch, isLoading, loadList} from "@/components/pagination/paginatedList.vue";
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Top row: Reload, Search -->
|
||||
<div class="columns is-vcentered is-multiline">
|
||||
<div class="column">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Søg..."
|
||||
v-model="metaSearch"
|
||||
@input="search($event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">{{SessionUser.objects.global.language.reload}}</LoadButtonWhileAwait>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
import { defineProps, ref } from 'vue';
|
||||
import {
|
||||
isLoading,
|
||||
loadList,
|
||||
metaCurrentPage,
|
||||
metaItemsPerPage,
|
||||
metaTotalItems,
|
||||
setMetaItemsPerPage,
|
||||
setPage,
|
||||
} from "@/components/pagination/paginatedList.vue";
|
||||
import PaginationDisplayGeneralSearchReload
|
||||
from "@/components/displays/pagination/PaginationDisplayGeneralSearchReload.vue";
|
||||
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const isSmall = ref(window.innerWidth < 1024);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Label -->
|
||||
<h2 class="title is-4 mb-4">{{ label }}</h2>
|
||||
<!-- Search & reload -->
|
||||
<PaginationDisplayGeneralSearchReload/>
|
||||
<!-- Filters -->
|
||||
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||
<template #paginationColumns>
|
||||
<slot name="paginationDisplayFiltersElement"></slot>
|
||||
<slot name="leftPaginationColumns"></slot>
|
||||
<div class="column is-auto-fill my-3" v-if="!isSmall"/>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12 p-1 m-0"></div>
|
||||
<slot name="rightPaginationColumns"></slot>
|
||||
</div>
|
||||
</template>
|
||||
</PaginationDisplay>
|
||||
<!-- Table -->
|
||||
<section>
|
||||
<slot name="default"></slot>
|
||||
</section>
|
||||
<!-- Pagination -->
|
||||
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -44,6 +44,9 @@ import PaginationNavigation from "@/components/displays/pagination/PaginationNav
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||
import PaginationDisplayGeneralSearchReload
|
||||
from "@/components/displays/pagination/PaginationDisplayGeneralSearchReload.vue";
|
||||
import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue";
|
||||
const router = useRouter();
|
||||
setEndpoint("/orders", false);
|
||||
// If the customer filter is set, filter the orders by the customer number
|
||||
@@ -65,7 +68,7 @@ if (router.currentRoute.value.params.departmentId) {
|
||||
setOrder("created_at", "desc");
|
||||
setFilter("department_id", router.currentRoute.value.params.departmentId, false);
|
||||
} else {
|
||||
setOrder("created_at", "desc");
|
||||
setOrder("created_at", "desc", false);
|
||||
}
|
||||
|
||||
// If the onlyFromInvoiceCollection prop is set, filter the orders by the invoice collection id
|
||||
@@ -81,12 +84,12 @@ if (props.autoLoad) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input @input="search($event.target.value)" class="input" type="text" :placeholder="'Søg i ' + SessionUser.objects.orders.meta.labels.multiple.toLowerCase()" v-if="!hideSearchField"/>
|
||||
<PaginationDisplayGeneralSearchReload/>
|
||||
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||
<template #paginationColumns>
|
||||
<!-- Sort by created_at -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label">Order direction</label>
|
||||
<label class="label is-small">Order direction</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select @change="setOrder('created_at', $event.target.value); loadList();">
|
||||
@@ -96,11 +99,11 @@ if (props.autoLoad) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationDisplayFilters />
|
||||
</template>
|
||||
</PaginationDisplay>
|
||||
<OrdersTable :orders="list" />
|
||||
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">{{SessionUser.objects.global.language.reload}}</LoadButtonWhileAwait>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
|
||||
import TableLabeledPagination from "@/components/displays/pagination/TableLabeledPagination.vue";
|
||||
|
||||
const props = defineProps({
|
||||
onlyFromInvoiceCollection: {
|
||||
type: Number,
|
||||
@@ -60,6 +62,10 @@ const props = defineProps({
|
||||
queryParameters : {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
excludedOrderIds: {
|
||||
type: Array, // Used to exclude specific order ids from the list, this will also ignore warnings about hidden orders
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
import {defineProps, ref} from "vue";
|
||||
@@ -91,6 +97,9 @@ import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue"
|
||||
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||
import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue";
|
||||
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
|
||||
import {now} from "@vueuse/core";
|
||||
|
||||
const router = useRouter();
|
||||
setHideSearchField(props.hideSearch);
|
||||
@@ -260,15 +269,19 @@ const doesEndpointMatch = (matcher) => {
|
||||
|
||||
<template>
|
||||
<template v-if="doesEndpointMatch('/orders')">
|
||||
<input @input="search($event.target.value)" class="input" type="text" placeholder="Søg i transaktioner" v-if="!hideSearchField"/>
|
||||
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage" v-show="props.hideFilter === false">
|
||||
<template #paginationColumns>
|
||||
<TableLabeledPagination :label="SessionUser.objects.orders.meta.labels.multiple">
|
||||
<template #paginationDisplayFiltersElement>
|
||||
<PaginationDisplayFilters
|
||||
:departmentColumn="'department_id'"
|
||||
/>
|
||||
</template>
|
||||
<template #leftPaginationColumns>
|
||||
<!-- Sort by created_at -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label">Order direction</label>
|
||||
<div class="column is-narrow">
|
||||
<label class="label is-small">Order direction</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select @change="setOrder('created_at', $event.target.value, true);">
|
||||
<select @change="setOrder('created_at', $event.target.value); loadList();">
|
||||
<option value="asc">Ascending (Oldest first)</option>
|
||||
<option value="desc" selected>Descending (Newest first)</option>
|
||||
</select>
|
||||
@@ -276,8 +289,8 @@ const doesEndpointMatch = (matcher) => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Invoice status -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label">Bogføringsstatus</label>
|
||||
<div class="column is-narrow">
|
||||
<label class="label is-small">Bogføringsstatus</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select @change="setFilter('booked_invoice_id', $event.target.value, true);">
|
||||
@@ -289,8 +302,8 @@ const doesEndpointMatch = (matcher) => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Error status -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label">Fejlstatus</label>
|
||||
<div class="column is-narrow">
|
||||
<label class="label is-small">Fejlstatus</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select @change="setFilter('error_message', $event.target.value, true);">
|
||||
@@ -302,8 +315,8 @@ const doesEndpointMatch = (matcher) => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Payment processor -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label">Betalingsprocessor</label>
|
||||
<div class="column is-narrow">
|
||||
<label class="label is-small">Betalingsprocessor</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select @change="setFilter('processor', $event.target.value, true);">
|
||||
@@ -317,8 +330,8 @@ const doesEndpointMatch = (matcher) => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Marked as complete ("Gennemført") -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label">Gennemført</label>
|
||||
<div class="column is-narrow">
|
||||
<label class="label is-small">Gennemført</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select @change="setFilter('completed_at', $event.target.value, true);">
|
||||
@@ -330,8 +343,8 @@ const doesEndpointMatch = (matcher) => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Multiple selection, customer has attribute -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label">Faktura valg</label>
|
||||
<div class="column is-narrow">
|
||||
<label class="label is-small">Faktura valg</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select @change="setFilter('customer_id-has_attribute', $event.target.value, true);">
|
||||
@@ -343,8 +356,8 @@ const doesEndpointMatch = (matcher) => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Multiple selection, customer has special arrangement -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label">Specialaftale</label>
|
||||
<div class="column is-narrow">
|
||||
<label class="label is-small">Specialaftale</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select @change="setFilter('customer_id-has_key-OtherSpecialArrangement', $event.target.value, true);">
|
||||
@@ -356,8 +369,8 @@ const doesEndpointMatch = (matcher) => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Multiple selection, customer has washing subscription -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label">Vaskeabonnement</label>
|
||||
<div class="column is-narrow">
|
||||
<label class="label is-small">Vaskeabonnement</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select @change="setFilter('customer_id-has_key-OtherVaskeabonnement', $event.target.value, true);">
|
||||
@@ -369,7 +382,7 @@ const doesEndpointMatch = (matcher) => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Multiple selection, customer has washing subscription -FIX
|
||||
<div class="column is-narrow my-3">
|
||||
<div class="column is-narrow">
|
||||
<label class="label">Only tank cleaning</label>s
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
@@ -382,115 +395,47 @@ const doesEndpointMatch = (matcher) => {
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- Date from -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label">Fra dato</label>
|
||||
<div class="column is-narrow">
|
||||
<label class="label is-small">Fra dato</label>
|
||||
<div class="control">
|
||||
<input type="date" class="input" @change="setFilter('created_at-date_from', $event.target.value, true)" v-model="date_from" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Date to -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label">Til dato</label>
|
||||
<div class="column is-narrow">
|
||||
<label class="label is-small">Til dato</label>
|
||||
<div class="control">
|
||||
<input type="date" class="input" @change="setFilter('created_at-date_to', $event.target.value, true)" v-model="date_to" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Time shortcuts -->
|
||||
<!-- Last month -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label"> </label>
|
||||
<div class="control">
|
||||
<button
|
||||
class="button"
|
||||
v-bind:class="getTimeShortcutClass(setFilterTime.is_last_month(date_from, date_to))"
|
||||
@click="setFilterTime.last_month()"
|
||||
>
|
||||
<span class="icon">
|
||||
<i v-bind:class="getTimeShortcutIcon(setFilterTime.is_last_month(date_from, date_to))"></i>
|
||||
</span>
|
||||
<span>{{SessionUser.objects.global.language.time.last_month}}</span>
|
||||
<span class="icon">
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- This month -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label"> </label>
|
||||
<div class="control">
|
||||
<button
|
||||
class="button"
|
||||
v-bind:class="getTimeShortcutClass(setFilterTime.is_this_month(date_from, date_to))"
|
||||
@click="setFilterTime.this_month()"
|
||||
>
|
||||
<span class="icon">
|
||||
<i v-bind:class="getTimeShortcutIcon(setFilterTime.is_this_month(date_from, date_to))"></i>
|
||||
</span>
|
||||
<span>{{SessionUser.objects.global.language.time.this_month}}</span>
|
||||
<span class="icon">
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Today -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label"> </label>
|
||||
<div class="control">
|
||||
<button
|
||||
class="button"
|
||||
v-bind:class="getTimeShortcutClass(setFilterTime.is_today(date_from, date_to))"
|
||||
@click="setFilterTime.today()"
|
||||
>
|
||||
<span class="icon">
|
||||
<i v-bind:class="getTimeShortcutIcon(setFilterTime.is_today(date_from, date_to))"></i>
|
||||
</span>
|
||||
<span>{{SessionUser.objects.global.language.time.today}}</span>
|
||||
<span class="icon">
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Yesterday -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label"> </label>
|
||||
<div class="control">
|
||||
<button
|
||||
class="button"
|
||||
v-bind:class="getTimeShortcutClass(setFilterTime.is_yesterday(date_from, date_to))"
|
||||
@click="setFilterTime.yesterday()"
|
||||
>
|
||||
<span class="icon">
|
||||
<i v-bind:class="getTimeShortcutIcon(setFilterTime.is_yesterday(date_from, date_to))"></i>
|
||||
</span>
|
||||
<span>{{SessionUser.objects.global.language.time.yesterday}}</span>
|
||||
<span class="icon">
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- All -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label"> </label>
|
||||
<div class="control">
|
||||
<button
|
||||
class="button"
|
||||
v-bind:class="getTimeShortcutClass(setFilterTime.is_all(date_from, date_to))"
|
||||
@click="setFilterTime.all()"
|
||||
>
|
||||
<span class="icon">
|
||||
<i v-bind:class="getTimeShortcutIcon(setFilterTime.is_all(date_from, date_to))"></i>
|
||||
</span>
|
||||
<span>{{SessionUser.objects.global.language.time.all}}</span>
|
||||
<span class="icon">
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #rightPaginationColumns>
|
||||
<div class="column is-12">
|
||||
<!-- Shortcuts for date filters -->
|
||||
<DatePeriodSelector :on-selection-change="(newSelectionStartDate, newSelectionToDate) => {
|
||||
const formattedStartDate = newSelectionStartDate.toISOString().split('T')[0];
|
||||
const formattedEndDate = newSelectionToDate.toISOString().split('T')[0];
|
||||
date_from = formattedStartDate;
|
||||
date_to = formattedEndDate;
|
||||
setFilter('created_at-date_from', formattedStartDate, true);
|
||||
setFilter('created_at-date_to', formattedEndDate, true);
|
||||
loadList();
|
||||
}"
|
||||
:visibility="{ showDailySelector: false, showWeeklySelector: false, showMultipleMonthWarning: false, showUpdateButton: false, showMonthSelector: false, showStartDate: false, showEndDate: false, showSelectionValidity: false, showYearSelector: false, showToLabel: false }"
|
||||
v-bind:selection="{ startDate: date_from ? new Date(date_from) : new Date(now()), endDate: date_to ? new Date(date_to) : new Date(now()) }"/>
|
||||
</div>
|
||||
</template>
|
||||
</PaginationDisplay>
|
||||
<OrdersTable :orders="list" :invoiceView="props.invoiceView" :allowSelectMultiple="true" v-bind:groupInvoiceCollection="props.groupInvoiceCollection" v-bind:showOnlyWithIds="props.showOnlyWithIds" v-bind:autoExpandAll="props.autoExpandAll"/>
|
||||
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" v-show="!props.hidePagination" />
|
||||
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||
<template #default>
|
||||
<OrdersTable :orders="list"
|
||||
:invoiceView="props.invoiceView"
|
||||
:allowSelectMultiple="true"
|
||||
v-bind:groupInvoiceCollection="props.groupInvoiceCollection"
|
||||
v-bind:showOnlyWithIds="props.showOnlyWithIds"
|
||||
v-bind:autoExpandAll="props.autoExpandAll"
|
||||
v-bind:excludedOrderIds="props.excludedOrderIds"
|
||||
/>
|
||||
</template>
|
||||
</TableLabeledPagination>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ if (props.autoLoad) {
|
||||
<template #paginationColumns>
|
||||
<!-- Sort by created_at -->
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label">Rækkefølge</label>
|
||||
<label class="label is-small">Rækkefølge</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select @change="setOrder('created_at', $event.target.value); loadList();">
|
||||
|
||||
@@ -90,7 +90,14 @@ window.addEventListener('resize', () => {
|
||||
isSmall.value = window.innerWidth < 1024;
|
||||
});
|
||||
|
||||
|
||||
const showNewOrderBookingsPortal = () => {
|
||||
const departmentId = SessionUser.functions.getDepartmentIdFromUrl();
|
||||
if (!departmentId) {
|
||||
SessionUser.functions.redirectTo.user('/bookings', true);
|
||||
return;
|
||||
}
|
||||
SessionUser.functions.redirectTo.department(SessionUser.functions.getDepartmentIdFromUrl(), 'modules/bookings', true);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -169,6 +176,9 @@ window.addEventListener('resize', () => {
|
||||
</div>
|
||||
</template>
|
||||
</PaginationDisplay>
|
||||
<div class="notification is-warning is-light mb-3 is-clickable" @click="showNewOrderBookingsPortal()">
|
||||
<strong>Bemærk:</strong> Du befinder dig på den gamle bookingportal, vi er i gang med at flytte historisk data til den nye. Du finder den nye booking portal <u>her</u>.
|
||||
</div>
|
||||
<BookingsTable :objects="list" :meta="meta" class="mb-4" />
|
||||
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" class="mt-3" />
|
||||
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Opdater</LoadButtonWhileAwait>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import {
|
||||
isLoaded,
|
||||
isLoading,
|
||||
list,
|
||||
loadList,
|
||||
@@ -9,27 +8,17 @@ import {
|
||||
metaTotalItems,
|
||||
setEndpoint,
|
||||
setMetaItemsPerPage,
|
||||
setFilter,
|
||||
getFilter,
|
||||
setPage,
|
||||
search,
|
||||
metaSearch,
|
||||
setOrder,
|
||||
meta
|
||||
} from "@/components/pagination/paginatedList.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||
import BookingsTable from "@/components/displays/user/bookings/bookingsTable.vue";
|
||||
import {departments} from "@/components/pagination/departmentTabs.vue";
|
||||
import {ref, watch} from "vue";
|
||||
import { Colors } from "@/ThemeConfig.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import InvoiceCollectionsTable from "@/components/displays/user/invoicecollections/InvoiceCollectionsTable.vue";
|
||||
import PaginationDisplayGeneralSearchReload
|
||||
from "@/components/displays/pagination/PaginationDisplayGeneralSearchReload.vue";
|
||||
import TableLabeledPagination from "@/components/displays/pagination/TableLabeledPagination.vue";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
setEndpoint("/user/invoices", true);
|
||||
setOrder("created_at", "desc");
|
||||
@@ -38,13 +27,11 @@ loadList();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input @input="search($event.target.value)" class="input mt-2" type="text" placeholder="Søg efter faktura" v-model="metaSearch" />
|
||||
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||
<template #paginationColumns></template>
|
||||
</PaginationDisplay>
|
||||
<InvoiceCollectionsTable :objects="list" :meta="meta" class="mb-4" />
|
||||
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" class="mt-3" />
|
||||
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Opdater</LoadButtonWhileAwait>
|
||||
<TableLabeledPagination label="Oversigt over fakturaer">
|
||||
<template #default>
|
||||
<InvoiceCollectionsTable :objects="list" :meta="meta" />
|
||||
</template>
|
||||
</TableLabeledPagination>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<script setup>
|
||||
import {
|
||||
list,
|
||||
loadList,
|
||||
setEndpoint,
|
||||
setFilter,
|
||||
} from "@/components/pagination/paginatedList.vue";
|
||||
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { defineProps, onMounted, ref} from "vue";
|
||||
import TableLabeledPagination from "@/components/displays/pagination/TableLabeledPagination.vue";
|
||||
import OrderBookingsTable from "@/views/dashboards/userDashboard/bookings/displays/tables/OrderBookingsTable.vue";
|
||||
import { departments, getDepartments } from "@/components/pagination/departmentTabs.vue";
|
||||
import { Colors } from "@/ThemeConfig.vue";
|
||||
|
||||
/**
|
||||
* Router
|
||||
*/
|
||||
const router = useRouter();
|
||||
/**
|
||||
* Props
|
||||
*/
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
required: false
|
||||
},
|
||||
})
|
||||
const orderIdFilter = ref('*');
|
||||
const onOrderIdFilterChange = (event) => {
|
||||
const val = event.target.value;
|
||||
orderIdFilter.value = val;
|
||||
// Apply filter: '*' clears, 'is null' shows without order, 'not null' shows with order
|
||||
setFilter('order_id', val);
|
||||
};
|
||||
|
||||
// Department filter
|
||||
const departmentFilter = ref('*');
|
||||
const onDepartmentFilterChange = (event) => {
|
||||
const val = event.target.value;
|
||||
departmentFilter.value = val;
|
||||
setFilter('department', val);
|
||||
};
|
||||
|
||||
// Only today filter
|
||||
const onlyTodayFilter = ref('*');
|
||||
const onOnlyTodayFilterChange = (event, autoLoadList = true) => {
|
||||
const val = event.target.value;
|
||||
onlyTodayFilter.value = val;
|
||||
if (val === '*') {
|
||||
setFilter('datetime', val, false); // Clear filter
|
||||
setFilter('datetime-date_from', null, false);
|
||||
setFilter('datetime-date_to', null, false);
|
||||
} else {
|
||||
const startOfDay = new Date().setHours(0, 0, 0, 0);
|
||||
const endOfDay = new Date().setHours(23, 59, 59, 999);
|
||||
setFilter('datetime', null, false);
|
||||
setFilter('datetime-date_from', new Date(startOfDay).toISOString(), false);
|
||||
setFilter('datetime-date_to', new Date(endOfDay).toISOString(), false);
|
||||
}
|
||||
if (autoLoadList) {
|
||||
loadList();
|
||||
}
|
||||
};
|
||||
|
||||
// Version selector + helpers
|
||||
const versionSelector = ref('new');
|
||||
const resetVersionToNew = () => {
|
||||
versionSelector.value = 'new';
|
||||
};
|
||||
const showLegacyOrderBookingsPortal = () => {
|
||||
const departmentId = SessionUser.functions.getDepartmentIdFromUrl();
|
||||
if (!departmentId) {
|
||||
SessionUser.functions.redirectTo.user('/bookings-legacy', true);
|
||||
return;
|
||||
}
|
||||
SessionUser.functions.redirectTo.department(SessionUser.functions.getDepartmentIdFromUrl(), 'modules/bookings-legacy', true);
|
||||
};
|
||||
|
||||
// Filters
|
||||
onMounted(() => {
|
||||
setEndpoint(SessionUser.objects.order_bookings.meta.endpoint, false);
|
||||
for (const [key, value] of Object.entries(props.filters)) {
|
||||
let setFilterKey = true;
|
||||
// Also set the filter controls if applicable
|
||||
if (key === 'order_id') {
|
||||
orderIdFilter.value = value;
|
||||
} else if (key === 'department') {
|
||||
departmentFilter.value = value;
|
||||
} else if (key === 'only_today' && value === true) {
|
||||
setFilterKey = false; // Since the only_today filter is handled separately
|
||||
onOnlyTodayFilterChange({ target: { value: new Date().toISOString().split('T')[0] } }, false);
|
||||
}
|
||||
if (setFilterKey) {
|
||||
setFilter(key, value, false);
|
||||
}
|
||||
}
|
||||
loadList();
|
||||
// Load departments for filter options
|
||||
getDepartments().catch(() => {});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<TableLabeledPagination label="Oversigt over bookinger">
|
||||
<template #paginationDisplayFiltersElement>
|
||||
<!-- Status filter -->
|
||||
<div class="column is-narrow">
|
||||
<div class="field mb-0">
|
||||
<label class="label is-small">Status</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select :value="orderIdFilter" @change="onOrderIdFilterChange">
|
||||
<option value="*">Alle</option>
|
||||
<option value="is null">Ikke udført</option>
|
||||
<option value="not null">Udført</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Department filter -->
|
||||
<div class="column is-narrow">
|
||||
<div class="field mb-0">
|
||||
<label class="label is-small">Afdeling</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select :value="departmentFilter" @change="onDepartmentFilterChange">
|
||||
<option value="*">Alle</option>
|
||||
<option v-for="department in departments" :key="department.id" :value="department.id">{{ department.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Version selector -->
|
||||
<div class="column is-narrow">
|
||||
<div class="field mb-0">
|
||||
<label class="label is-small">Version</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select @change="showLegacyOrderBookingsPortal(); resetVersionToNew()" v-model="versionSelector">
|
||||
<option value="new">Ny</option>
|
||||
<option value="legacy">Gammel</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Only today filter -->
|
||||
<div class="column is-narrow">
|
||||
<div class="field mb-0">
|
||||
<label class="label is-small">Kun i dag</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select :value="onlyTodayFilter" @change="onOnlyTodayFilterChange">
|
||||
<option value="*">Alle</option>
|
||||
<option :value="new Date().toISOString().split('T')[0]">Ja</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #leftPaginationColumns>
|
||||
</template>
|
||||
<template #rightPaginationColumns>
|
||||
<!-- Toggles and actions depending on route -->
|
||||
<!-- User: Only today switch + New booking button -->
|
||||
<div class="column is-narrow" v-if="router.currentRoute.value.path.startsWith('/user')">
|
||||
<label class="label is-small">Vis kun dagens bookinger</label>
|
||||
<div class="field">
|
||||
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { onOnlyTodayFilterChange({ target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' } }) }" :class="{ 'is-link': onlyTodayFilter !== '*' }" :checked="onlyTodayFilter !== '*'" />
|
||||
<label for="today"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-narrow is-float-right" v-if="router.currentRoute.value.path.startsWith('/user')">
|
||||
<label class="label is-small"> </label>
|
||||
<button
|
||||
class="button is-link"
|
||||
@click="SessionUser.functions.redirectTo.external('/user/bookings/new', true)"
|
||||
>
|
||||
Ny booking
|
||||
</button>
|
||||
</div>
|
||||
<!-- Admin: pending + only today combined switch -->
|
||||
<div class="column is-narrow" v-if="router.currentRoute.value.path.startsWith('/admin')">
|
||||
<label class="label is-small">Vis kun dagens afventende</label>
|
||||
<div class="field">
|
||||
<input id="today-pending" type="checkbox" class="switch is-rounded" @change="(event) => { onOnlyTodayFilterChange({ target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' } }, false); onOrderIdFilterChange({ target: { value: event.target.checked ? 'is null' : '*' } }); }" :class="{ 'is-link': onlyTodayFilter !== '*' && orderIdFilter === 'is null' }" :checked="onlyTodayFilter !== '*' && orderIdFilter === 'is null'" />
|
||||
<label for="today-pending"></label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<OrderBookingsTable :objects="list" />
|
||||
</template>
|
||||
</TableLabeledPagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
metaItemsPerPage,
|
||||
metaTotalItems,
|
||||
setEndpoint,
|
||||
metaSearch,
|
||||
setMetaItemsPerPage,
|
||||
setOrder,
|
||||
setPage,
|
||||
search,
|
||||
setFilter
|
||||
@@ -21,6 +23,10 @@ import VehiclesTable from "@/components/displays/user/vehicles/vehiclesTable.vue
|
||||
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||
import InvoicesTable from "@/components/displays/user/invoices/invoicesTable.vue";
|
||||
import {defineProps} from "vue";
|
||||
import PaginationDisplayGeneralSearchReload
|
||||
from "@/components/displays/pagination/PaginationDisplayGeneralSearchReload.vue";
|
||||
import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue";
|
||||
import TableLabeledPagination from "@/components/displays/pagination/TableLabeledPagination.vue";
|
||||
const props = defineProps({
|
||||
onlyFromInvoiceCollection: {
|
||||
type: Number,
|
||||
@@ -40,6 +46,8 @@ if (props.onlyFromInvoiceCollection > 0) {
|
||||
setFilter("invoice_collection_id", props.onlyFromInvoiceCollection, false);
|
||||
}
|
||||
|
||||
setOrder("id", "desc", false);
|
||||
|
||||
if (props.autoLoad) {
|
||||
loadList();
|
||||
}
|
||||
@@ -47,16 +55,20 @@ if (props.autoLoad) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input @input="search($event.target.value)" class="input mt-2" type="text" placeholder="Søg efter vask" v-model="metaSearch" />
|
||||
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage" />
|
||||
<!-- <invoicesTable :objects="list" /> -->
|
||||
<orders-table
|
||||
v-bind:orders="list"
|
||||
v-bind:is-customer-view="true"
|
||||
v-bind:invoiceView="props.allowDropdownToShowOrderContent"
|
||||
/>
|
||||
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||
<TableLabeledPagination label="Historiske vaske">
|
||||
<template #paginationDisplayFiltersElement>
|
||||
<PaginationDisplayFilters
|
||||
:departmentColumn="'department_id'"
|
||||
/>
|
||||
</template>
|
||||
<template #default>
|
||||
<orders-table
|
||||
v-bind:orders="list"
|
||||
v-bind:is-customer-view="true"
|
||||
v-bind:invoiceView="props.allowDropdownToShowOrderContent"
|
||||
/>
|
||||
</template>
|
||||
</TableLabeledPagination>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
<script setup>
|
||||
import {
|
||||
isLoaded,
|
||||
isLoading,
|
||||
list,
|
||||
loadList,
|
||||
metaCurrentPage,
|
||||
metaItemsPerPage,
|
||||
metaTotalItems,
|
||||
setEndpoint,
|
||||
setMetaItemsPerPage,
|
||||
setPage,
|
||||
setFilter,
|
||||
getFilter,
|
||||
} from "@/components/pagination/paginatedList.vue";
|
||||
// Create a new pagination component for the vehicles, to allow multiple pagination components on the same page
|
||||
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import VehiclesTable from "@/components/displays/user/vehicles/vehiclesTable.vue";
|
||||
import {defineProps, ref} from "vue";
|
||||
import {defineProps} from "vue";
|
||||
import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue";
|
||||
import {Colors} from "@/ThemeConfig.vue";
|
||||
import TableLabeledPagination from "@/components/displays/pagination/TableLabeledPagination.vue";
|
||||
|
||||
/**
|
||||
* Router
|
||||
*/
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps({
|
||||
customer_id: {
|
||||
@@ -42,11 +38,31 @@ loadList();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||
</PaginationDisplay>
|
||||
<VehiclesTable :vehicles="list" :show_add_vehicle="props.customer_id" :add_other_customer_id="props.add_other_customer_id"/>
|
||||
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||
<LoadButtonWhileAwait class="is-dark" :isLoading="!isLoaded()" :loadFunction="loadList" icon="fas fa-sync-alt">Opdater</LoadButtonWhileAwait>
|
||||
<div>
|
||||
<TableLabeledPagination label="Oversigt over registrerede køretøjer">
|
||||
<template #paginationDisplayFiltersElement>
|
||||
<PaginationDisplayFilters
|
||||
:productColumn="'type'"
|
||||
:columnLabels="{ type: SessionUser.objects.vehicles.columns.type.label }"
|
||||
/>
|
||||
</template>
|
||||
<template #rightPaginationColumns>
|
||||
<!-- Create a new vehicle, if the route is /user -->
|
||||
<div class="column is-narrow is-float-right"
|
||||
v-if="router.currentRoute.value.path.startsWith('/user')">
|
||||
<label class="label is-small"> </label>
|
||||
<button
|
||||
class="button is-link"
|
||||
@click="SessionUser.objects.vehicles.functions.showCreateObjectForCustomerForm(props.customer_id, () => loadList())" style="text-decoration-line: none;" :style="{ 'color': Colors.buttons.textColor, 'background-color': Colors.buttons.backgroundColor }">
|
||||
Tilføj køretøj
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<VehiclesTable :vehicles="list" :show_add_vehicle="props.customer_id" :add_other_customer_id="props.add_other_customer_id"/>
|
||||
</template>
|
||||
</TableLabeledPagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, defineProps } from "vue";
|
||||
/**
|
||||
* Props
|
||||
*/
|
||||
const props = defineProps({
|
||||
// The maximum height of the steps container
|
||||
maxHeight: {
|
||||
type: String,
|
||||
default: '300px',
|
||||
},
|
||||
});
|
||||
/**
|
||||
* Variables
|
||||
*/
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<slot name="header">
|
||||
<!-- Default header content can go here if needed -->
|
||||
</slot>
|
||||
</div>
|
||||
<!-- Steps container with scrollable content -->
|
||||
<div :style="{ maxHeight: maxHeight, overflowY: 'auto' }">
|
||||
<slot name="steps">
|
||||
<!-- Default steps content can go here if needed -->
|
||||
</slot>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<div>
|
||||
<slot name="footer">
|
||||
<!-- Default footer content can go here if needed -->
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -27,6 +27,10 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
summaryMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const order_item_default = {
|
||||
@@ -100,7 +104,7 @@ watch(() => props.orderId, (newValue, oldValue) => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<table class="table is-fullwidth">
|
||||
<table class="table is-fullwidth" v-if="!props.summaryMode">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ucFirst(SessionUser.objects.products.meta.labels.single)}}</th>
|
||||
@@ -153,6 +157,34 @@ watch(() => props.orderId, (newValue, oldValue) => {
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Summary Mode -->
|
||||
<div v-else>
|
||||
<!-- Loader -->
|
||||
<template v-if="orderItems.length === 0">
|
||||
<span class="is-flex is-justify-content-center is-align-items-center" style="height: 100px;">
|
||||
<span class="loader is-loading is-align-self-center"></span>
|
||||
</span>
|
||||
</template>
|
||||
<!-- Order Items -->
|
||||
<template v-else>
|
||||
<div v-for="item in orderItems" :key="item.id" class="mb-2">
|
||||
<!-- If the item is a primary item, show it -->
|
||||
<template v-if="!isOrderItemAddon(item)">
|
||||
<div>
|
||||
<strong>{{ item.product.name }}</strong> x {{ item.quantity }}
|
||||
<span v-if="props.displayPrice"> - {{ SessionUser.functions.currency.toLocal(item.price) }}</span>
|
||||
</div>
|
||||
<!-- Show the add-on items -->
|
||||
<div v-for="addon_item in getItemAddons(item)" :key="addon_item.id" class="ml-4">
|
||||
<small>
|
||||
+ {{ addon_item.product.name }} x {{ addon_item.quantity }}
|
||||
<span v-if="props.displayPrice"> - {{ SessionUser.functions.currency.toLocal(addon_item.price) }}</span>
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
|
||||
defineProps(['objects']);
|
||||
import { ref } from 'vue';
|
||||
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||
import { showEditDepartmentForm } from "@/components/forms/superUser/editDepartmentForm.vue";
|
||||
|
||||
import { showEditUserForm } from "@/components/forms/superUser/editUserForm.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue";
|
||||
|
||||
@@ -18,7 +15,7 @@ const redirect = (path) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="table is-fullwidth">
|
||||
<table class="table is-fullwidth is-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ SessionUser.objects.department_lanes.columns.id.label }}</th>
|
||||
@@ -26,6 +23,7 @@ const redirect = (path) => {
|
||||
<th>{{ SessionUser.objects.department_lanes.columns.name.label }}</th>
|
||||
<th>{{ SessionUser.objects.department_lanes.columns.relay_in_id.label }}</th>
|
||||
<th>{{ SessionUser.objects.department_lanes.columns.relay_out_id.label }}</th>
|
||||
<th>{{ SessionUser.objects.department_lanes.columns.relay_machine_id.label }}</th>
|
||||
<th class="has-text-right">Handlinger</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -57,6 +55,12 @@ const redirect = (path) => {
|
||||
column="relay_out_id"
|
||||
:edit-function="SessionUser.objects.department_lanes.showEditObjectFieldForm"
|
||||
/>
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="loadList"
|
||||
column="relay_machine_id"
|
||||
:edit-function="SessionUser.objects.department_lanes.showEditObjectFieldForm"
|
||||
/>
|
||||
<!-- Actions -->
|
||||
<td class="has-text-right">
|
||||
<ActionSettingsWheelButton>
|
||||
|
||||
@@ -95,7 +95,7 @@ const getDisplayProductAddons = (product) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="table is-fullwidth is-hoverable is-striped">
|
||||
<table class="table is-fullwidth is-hoverable is-striped" style="table-layout: fixed;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ SessionUser.objects.products.columns.order_priority.label }}</th>
|
||||
|
||||
@@ -20,33 +20,7 @@ const redirect = (path) => {
|
||||
window.location = path;
|
||||
}
|
||||
|
||||
const onShowImpersonationQRCode = (src, directLink) => {
|
||||
Swal.fire({
|
||||
title: 'Scan QR-koden for at logge ind som bruger',
|
||||
html: `<img src="${src}" alt="QR-kode" />`,
|
||||
showCloseButton: true,
|
||||
showConfirmButton: !!directLink,
|
||||
showCancelButton: true,
|
||||
focusConfirm: false,
|
||||
confirmButtonText: 'Kopiér link',
|
||||
cancelButtonText: 'Luk',
|
||||
preConfirm: () => {
|
||||
navigator.clipboard.writeText(directLink);
|
||||
Swal.showValidationMessage('Link kopieret til udklipsholderen');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const onClickShowImpersonationQRCode = async (userId) => {
|
||||
// Get the session token
|
||||
const newToken = await SessionUser.superUser.intimidate.getImpersonationToken(userId);
|
||||
// Create the impersonation link
|
||||
const link = `${window.location.protocol}//${window.location.host}/login/qr?token=${newToken}`;
|
||||
// Generate a QR code for the impersonation link
|
||||
const response = await SessionUser.superUser.intimidate.showImpersonationQRCode(link);
|
||||
// Show the QR code in a modal
|
||||
onShowImpersonationQRCode(response.value, link);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -69,6 +43,8 @@ const onClickShowImpersonationQRCode = async (userId) => {
|
||||
<div class="buttons is-float-right">
|
||||
<!-- Settings wheel -->
|
||||
<ActionSettingsWheelButton
|
||||
:customer_number="user.customer_number"
|
||||
:user_id="user.id"
|
||||
>
|
||||
<template #actions>
|
||||
<ActionSettingsWheelItem
|
||||
@@ -76,18 +52,6 @@ const onClickShowImpersonationQRCode = async (userId) => {
|
||||
icon="fas fa-user-edit"
|
||||
label="Rediger"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
@click="SessionUser.superUser.intimidate.intimidateUser(user.id)"
|
||||
icon="fas fa-user-shield"
|
||||
label="Log ind som bruger"
|
||||
:disabled="user.group_id === 1"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
@click="onClickShowImpersonationQRCode(user.id)"
|
||||
icon="fas fa-user-shield"
|
||||
label="Log ind som bruger (QR-kode)"
|
||||
:disabled="user.group_id === 1 && !SessionUser.superUser.intimidate.canImpersonateAdmin"
|
||||
/>
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import NewBookingStep1 from "@/views/dashboards/userDashboard/bookings/displays/
|
||||
import NewBookingStep2 from "@/views/dashboards/userDashboard/bookings/displays/steps/NewBookingStep2.vue";
|
||||
import NewBookingStep3 from "@/views/dashboards/userDashboard/bookings/displays/steps/NewBookingStep3.vue";
|
||||
import NewBookingStep4 from "@/views/dashboards/userDashboard/bookings/displays/steps/NewBookingStep4.vue";
|
||||
import NewBookingStep5 from "@/views/dashboards/userDashboard/bookings/displays/steps/NewBookingStep5.vue";
|
||||
/**
|
||||
* This is the new time booking flow.
|
||||
* It contains the functions used to create a new time booking.
|
||||
@@ -31,7 +32,7 @@ const stepComponents = {
|
||||
2: NewBookingStep2,
|
||||
3: NewBookingStep3,
|
||||
4: NewBookingStep4,
|
||||
5: null,
|
||||
5: NewBookingStep5,
|
||||
}
|
||||
const steps = ref<stepType[]>([
|
||||
{
|
||||
|
||||
@@ -328,7 +328,7 @@ const canUserEditObject = (object) => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<table class="table is-hoverable is-narrow" :class="isSmall ? 'is-hidden' : ''">
|
||||
<table class="table is-hoverable is-narrow is-fullwidth" :class="isSmall ? 'is-hidden' : ''" style="table-layout: fixed;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
@@ -349,7 +349,7 @@ const canUserEditObject = (object) => {
|
||||
<tbody>
|
||||
<tr v-for="object in parseAndSortBookings(objects)" :key="object.id">
|
||||
<template v-if="object.isLabel">
|
||||
<td colspan="14" class="has-text-left">
|
||||
<td colspan="13" class="has-text-left">
|
||||
<span class="has-text-grey">
|
||||
<i class="fas fa-calendar-day"></i>
|
||||
</span>
|
||||
@@ -369,7 +369,6 @@ const canUserEditObject = (object) => {
|
||||
<td>{{ object.date }}</td>
|
||||
<td>{{ object.regNrTraekker }}</td>
|
||||
<td>{{ object.regNrTrailer }}</td>
|
||||
<td>
|
||||
<!-- Reference number -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
@@ -379,7 +378,6 @@ const canUserEditObject = (object) => {
|
||||
:edit-function="SessionUser.objects.bookings.showEditObjectFieldForm"
|
||||
:permission-check-function="() => canUserEditObject(object)"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ object.notes }}</td>
|
||||
<td>{{ object.pickup_bool == 1 ? "Ja" : "Nej" }}</td>
|
||||
<td>{{ object.created_at }}</td>
|
||||
@@ -440,7 +438,7 @@ const canUserEditObject = (object) => {
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="14">Viser {{ objects.length }} bookinger {{ isSmall ? '' : 'i alt' }}</td>
|
||||
<td colspan="13">Viser {{ objects.length }} bookinger {{ isSmall ? '' : 'i alt' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ref } from 'vue';
|
||||
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import ColorIndicator from "@/components/displays/buttons/ColorIndicator.vue";
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
|
||||
const su_object = SessionUser.objects.collectedOrderInvoices;
|
||||
|
||||
@@ -114,7 +115,8 @@ const dropdown_content = (object) => {
|
||||
<th>{{su_object.columns.id.label}}</th>
|
||||
<th>{{su_object.columns.created_at.label}}</th>
|
||||
<th>{{su_object.columns.po_number.label}}</th>
|
||||
<th>{{SessionUser.objects.global.language.total}}</th>
|
||||
<th>{{SessionUser.objects.global.language.total_price_amount}}</th>
|
||||
<th><!-- Actions --></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -136,6 +138,13 @@ const dropdown_content = (object) => {
|
||||
:permission-check-function="() => canUserEditObject(object)"
|
||||
/>
|
||||
<td>{{ SessionUser.functions.currency.toLocal(object.total_net_amount) }}</td>
|
||||
<td>
|
||||
<ActionSettingsWheelButton
|
||||
:invoice_collection_id="object.id"
|
||||
>
|
||||
<template #actions></template>
|
||||
</ActionSettingsWheelButton>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<script setup>
|
||||
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||
|
||||
import {ref} from 'vue';
|
||||
import { departments, getDepartments} from "@/components/pagination/departmentTabs.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||
const props = defineProps({
|
||||
vehicles: {
|
||||
type: Array,
|
||||
@@ -27,12 +32,6 @@ const props = defineProps({
|
||||
default: null
|
||||
}
|
||||
});
|
||||
import { ref, defineEmits } from 'vue';
|
||||
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||
|
||||
|
||||
const reload = () => {
|
||||
@@ -140,7 +139,7 @@ const isVehicleAddonApplied = (addon) => {
|
||||
<th>{{ SessionUser.objects.vehicles.columns.wash_subscription.label }}</th>
|
||||
<th v-if="!props.compact">{{ SessionUser.objects.product_options.meta.title }}</th>
|
||||
<th v-if="!props.compact">{{ SessionUser.objects.vehicles.columns.reference.label }}</th>
|
||||
<th v-if="!props.compact">{{ SessionUser.objects.global.language.actions }}</th>
|
||||
<th v-if="!props.compact"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -254,31 +253,10 @@ const isVehicleAddonApplied = (addon) => {
|
||||
</ActionSettingsWheelButton>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Add vehicle button -->
|
||||
<tr>
|
||||
<td colspan="10">
|
||||
<div class="buttons">
|
||||
<!-- Add own vehicle button (If the add_other_customer_id is not set) -->
|
||||
<button
|
||||
v-if="props.show_add_vehicle && !props.add_other_customer_id"
|
||||
class="button is-small is-text"
|
||||
@click="SessionUser.objects.vehicles.functions.showCreateObjectForCustomerForm(props.show_add_vehicle, () => reload())" style="text-decoration-line: none">
|
||||
Tilføj køretøj
|
||||
</button>
|
||||
<!-- Add other vehicle button -->
|
||||
<button
|
||||
v-if="props.add_other_customer_id"
|
||||
class="button is-small is-text"
|
||||
@click="SessionUser.objects.vehicles.functions.showCreateObjectForCustomerForm(props.add_other_customer_id, () => reload())" style="text-decoration-line: none">
|
||||
Tilføj køretøj til kunde
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="10">Showing {{ vehicles.length }} vehicles</td>
|
||||
<td colspan="10">{{SessionUser.objects.global.language.showing}} {{ props.vehicles.length }} {{SessionUser.objects.vehicles.meta.labels.multiple}}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
@@ -67,13 +67,13 @@ SessionUser.auth.reCAPTCHA.preCheck.get().then((response) => {
|
||||
<template>
|
||||
<form @submit.prevent="doNothing">
|
||||
<div class="field">
|
||||
<label class="label">Customer Number</label>
|
||||
<label class="label">{{ SessionUser.functions.ucFirst(SessionUser.objects.global.language.customer_number) }}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" v-model="customer_number" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Password</label>
|
||||
<label class="label">{{ SessionUser.functions.ucFirst(SessionUser.objects.global.language.password) }}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="password" v-model="password" />
|
||||
</div>
|
||||
@@ -93,13 +93,13 @@ SessionUser.auth.reCAPTCHA.preCheck.get().then((response) => {
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<div class="buttons">
|
||||
<LoadButtonWhileAwait :loadFunction="login" class="button is-dark is-fullwidth" id="login-button">Login</LoadButtonWhileAwait>
|
||||
<LoadButtonWhileAwait :loadFunction="login" class="button is-dark is-fullwidth" id="login-button">{{ SessionUser.functions.ucFirst(SessionUser.objects.global.language.login) }}</LoadButtonWhileAwait>
|
||||
<LoadButtonWhileAwait :loadFunction="() => router.push('/login/qr')" class="button is-dark is-outlined is-fullwidth is-hidden-desktop" id="qr-login-button">
|
||||
<!-- QR code icon -->
|
||||
<span class="icon">
|
||||
<i class="fas fa-qrcode"></i>
|
||||
</span>
|
||||
<span>Login with QR code</span>
|
||||
<span>{{ SessionUser.functions.ucFirst(SessionUser.objects.global.language.login_with_qr_code) }}</span>
|
||||
</LoadButtonWhileAwait>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {onMounted, ref, watch, defineProps, defineEmits} from 'vue';
|
||||
import {onMounted, ref, watch, defineProps, defineEmits, nextTick, computed} from 'vue';
|
||||
import {
|
||||
getOrderId,
|
||||
getProductsCategory,
|
||||
@@ -24,6 +24,7 @@ import ConfigurationCategory from "@/components/displays/superuser/configuration
|
||||
import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
|
||||
import ProductBox from "@/components/displays/boxes/ProductBox.vue";
|
||||
import { getPicture } from "@/components/displays/department/pos/displays/Piktogrammer.vue";
|
||||
import { products_category } from "@/components/shop/POSDepartmentProcess.vue";
|
||||
const emits = defineEmits(['onAddToCartProduct', 'onAddProduct', 'onSelectProduct']);
|
||||
const props = defineProps({
|
||||
isCustomerBooking: {
|
||||
@@ -50,27 +51,103 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
},
|
||||
attemptAutoSelectId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
// New props for grid-before-select and auto-scroll behavior (desktop)
|
||||
gridBeforeSelect: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
},
|
||||
selectedId: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
autoScrollOnSelect: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
},
|
||||
filterProductsVisibleOnCustomerBooking: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: false
|
||||
},
|
||||
customer_number: {
|
||||
type: Number,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
showPrices: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
// Define the products
|
||||
const products = ref([]);
|
||||
onMounted(() => {
|
||||
console.warn('Mounted SelectProductsFormPOS');
|
||||
// If the category is not defined, get all products
|
||||
if (getProductsCategory() === null) {
|
||||
getProducts(department_id.value).then((response) => {
|
||||
getProducts(department_id.value, !!props.isCustomerBooking, props.customer_number).then((response) => {
|
||||
products.value = response.data.data;
|
||||
console.log(products.value);
|
||||
});
|
||||
} else {
|
||||
// Get the products by category
|
||||
getProductCategory(getProductsCategory(), department_id.value).then((response) => {
|
||||
getProductCategory(getProductsCategory(), department_id.value, !!props.isCustomerBooking, props.customer_number).then((response) => {
|
||||
products.value = response.data.data;
|
||||
// Wait a bit to ensure the DOM has settled
|
||||
setTimeout(() => {
|
||||
|
||||
// Check if the attempt auto select id is set
|
||||
if (props.attemptAutoSelectId !== null) {
|
||||
// Check if the product is in the products
|
||||
if (products.value.find((product) => product.id === props.attemptAutoSelectId) !== undefined) {
|
||||
// Set the selected id
|
||||
selectedProductId.value = props.attemptAutoSelectId;
|
||||
emits('onSelectProduct', products.value.find((product) => product.id === props.attemptAutoSelectId));
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
console.log(products.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// React to department or category changes
|
||||
watch(() => department_id.value, (newDeptId) => {
|
||||
// Reload categories for the new department
|
||||
loadCategories();
|
||||
// Refetch products for the current category scope
|
||||
if (getProductsCategory() === null) {
|
||||
getProducts(newDeptId, !!props.isCustomerBooking, props.customer_number).then((response) => {
|
||||
products.value = response.data.data;
|
||||
});
|
||||
} else {
|
||||
getProductCategory(getProductsCategory(), newDeptId, !!props.isCustomerBooking, props.customer_number).then((response) => {
|
||||
products.value = response.data.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => getProductsCategory(), (newCategory) => {
|
||||
if (newCategory === null) {
|
||||
getProducts(department_id.value, !!props.isCustomerBooking, props.customer_number).then((response) => {
|
||||
products.value = response.data.data;
|
||||
});
|
||||
} else {
|
||||
getProductCategory(newCategory, department_id.value, !!props.isCustomerBooking, props.customer_number).then((response) => {
|
||||
products.value = response.data.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -129,9 +206,10 @@ const loadCategories = async () => {
|
||||
categories.value.push(...categories_static.value);
|
||||
// Get the department id from the route
|
||||
const route = useRoute();
|
||||
const departmentId = route.params.departmentId;
|
||||
// Prefer route param, otherwise fallback to global POS department_id
|
||||
const departmentId = route.params.departmentId ?? department_id.value;
|
||||
// If the department id is not set, return
|
||||
if (departmentId === undefined) {
|
||||
if (departmentId === undefined || departmentId === null) {
|
||||
return;
|
||||
}
|
||||
// Get the categories
|
||||
@@ -139,10 +217,16 @@ const loadCategories = async () => {
|
||||
departmentId
|
||||
).then((response) => {
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
categories.value.push({
|
||||
name: response[i].category.name,
|
||||
identifier: response[i].category.id
|
||||
});
|
||||
// Check if the id is already in the categories by id,
|
||||
// This is needed to prevent duplicate categories for higher privilege users
|
||||
if (categories.value.find((category) => category.identifier === response[i].category.id) !== undefined) {
|
||||
// The id is already included in the list. Do nothing.
|
||||
} else {
|
||||
categories.value.push({
|
||||
name: response[i].category.name,
|
||||
identifier: response[i].category.id
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -358,19 +442,113 @@ const hideRecommendedProducts = () => {
|
||||
isShowingRecommended.value = false;
|
||||
};
|
||||
|
||||
const selectedProduct = ref(null);
|
||||
const selectedProductId = ref(null);
|
||||
|
||||
const isProductSelected = (productId) => {
|
||||
return selectedProduct.valueOf() === parseInt(productId);
|
||||
// Effective selected id prefers external prop when provided
|
||||
const effectiveSelectedId = computed(() => (props.selectedId !== null && props.selectedId !== undefined) ? props.selectedId : selectedProductId.value);
|
||||
|
||||
// Determine layout mode: if gridBeforeSelect is true and nothing is selected, use grid (not compact);
|
||||
// once a product is selected, switch to compact (horizontal scroll) automatically
|
||||
const isCompactMode = computed(() => {
|
||||
if (props.gridBeforeSelect) {
|
||||
return !!effectiveSelectedId.value; // compact (scroll) after selection
|
||||
}
|
||||
return !!props.compact;
|
||||
});
|
||||
|
||||
const isGridLayout = computed(() => props.gridBeforeSelect && !effectiveSelectedId.value);
|
||||
|
||||
// Card visual compactness: in gridBeforeSelect we still want the compact look
|
||||
const useCompactCard = computed(() => {
|
||||
return props.gridBeforeSelect ? true : isCompactMode.value;
|
||||
});
|
||||
|
||||
// Refs for auto-scroll to selected item
|
||||
const scrollWrapRef = ref(null);
|
||||
const itemRefs = new Map();
|
||||
const setItemRef = (el, id) => {
|
||||
// Normalize possible component refs to their root DOM elements
|
||||
const node = el && (el.$el || el);
|
||||
if (node) {
|
||||
itemRefs.set(id, node);
|
||||
} else {
|
||||
itemRefs.delete(id);
|
||||
}
|
||||
};
|
||||
|
||||
const selectProduct = (product) => {
|
||||
if (selectedProduct.valueOf() === parseInt(product.id)) {
|
||||
selectedProduct.value = null;
|
||||
} else {
|
||||
emits('onSelectProduct', product);
|
||||
selectedProduct.value = parseInt(product.id);
|
||||
const scrollSelectedIntoCenter = async (isRetry = false) => {
|
||||
if (!props.autoScrollOnSelect) return;
|
||||
const sel = effectiveSelectedId.value;
|
||||
if (!sel) return;
|
||||
// Ensure DOM/classes/layout have settled after mode switch
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
// Wait one frame to ensure CSS class toggles (horizontal vs vertical) have applied
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
const container = scrollWrapRef.value;
|
||||
const el = itemRefs.get(sel);
|
||||
if (container && el) {
|
||||
try {
|
||||
// If container has no width yet, retry shortly
|
||||
if (!container.clientWidth) {
|
||||
setTimeout(() => scrollSelectedIntoCenter(isRetry), 50);
|
||||
return;
|
||||
}
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const elRect = el.getBoundingClientRect();
|
||||
const currentLeft = container.scrollLeft || 0;
|
||||
const itemCenterX = currentLeft + (elRect.left - containerRect.left) + (elRect.width / 2);
|
||||
let targetLeft = Math.round(itemCenterX - (container.clientWidth / 2));
|
||||
const maxScrollLeft = Math.max(0, container.scrollWidth - container.clientWidth);
|
||||
if (targetLeft < 0) targetLeft = 0;
|
||||
if (targetLeft > maxScrollLeft) targetLeft = maxScrollLeft;
|
||||
container.scrollTo({ left: targetLeft, behavior: 'smooth' });
|
||||
// One-off delayed re-center to account for late image/font reflows
|
||||
if (!isRetry) {
|
||||
setTimeout(() => scrollSelectedIntoCenter(true), 300);
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
// fall through to fallback below
|
||||
}
|
||||
}
|
||||
// Fallback to native element-based scroll if container metrics aren't ready
|
||||
if (el && typeof el.scrollIntoView === 'function') {
|
||||
try {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
||||
} catch (e2) {
|
||||
el.scrollIntoView(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for changes to the customer number
|
||||
watch(() => props.customer_number, (newCustomerNumber) => {
|
||||
console.warn('Customer number changed in SelectProductsFormPOS:', newCustomerNumber);
|
||||
// Refetch products for the current category scope
|
||||
if (getProductsCategory() === null) {
|
||||
getProducts(department_id.value, !!props.isCustomerBooking, newCustomerNumber).then((response) => {
|
||||
products.value = response.data.data;
|
||||
});
|
||||
} else {
|
||||
getProductCategory(getProductsCategory(), department_id.value, !!props.isCustomerBooking, newCustomerNumber).then((response) => {
|
||||
products.value = response.data.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// When switching from grid to scroll, or when selection changes in scroll mode, ensure visibility
|
||||
watch([isGridLayout, effectiveSelectedId], (newVals) => {
|
||||
const [gridNow] = newVals;
|
||||
// If we just entered scroll mode or changed selection while in scroll mode
|
||||
if (!gridNow) {
|
||||
scrollSelectedIntoCenter();
|
||||
}
|
||||
});
|
||||
|
||||
const selectProduct = (product) => {
|
||||
emits('onSelectProduct', product);
|
||||
selectedProductId.value = product.id;
|
||||
};
|
||||
|
||||
const getAddonPrice = (productId, addonId) => {
|
||||
@@ -418,7 +596,7 @@ const subtractProductAddon = (productId, addonId) => {
|
||||
const unselectProduct = () => {
|
||||
// Wait for the selection from the click to be done
|
||||
setTimeout(() => {
|
||||
selectedProduct.value = null;
|
||||
selectedProductId.value = null;
|
||||
}, 100);
|
||||
};
|
||||
|
||||
@@ -444,8 +622,10 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
const orderByOrderPriority = (productsList) => {
|
||||
// If the subkey is set, sort the products by the subkey
|
||||
return productsList.sort((a, b) => a.order_priority - b.order_priority);
|
||||
// Normalize to an array to avoid runtime errors when source is null/undefined
|
||||
const list = Array.isArray(productsList) ? productsList : [];
|
||||
// If the subkey is set, sort the products by the subkey without mutating the original array
|
||||
return [...list].sort((a, b) => (a?.order_priority ?? 0) - (b?.order_priority ?? 0));
|
||||
};
|
||||
|
||||
const filterProductsVisibleOnCustomerBooking = (productsList) => {
|
||||
@@ -467,13 +647,16 @@ const filterProductsIsWash = (productsList) => {
|
||||
}
|
||||
|
||||
const filterProductAddonsVisibleOnCustomerBooking = (productsList) => {
|
||||
if (!props.isCustomerBooking) {
|
||||
return productsList;
|
||||
}
|
||||
// Return a new array of products with filtered addons to avoid mutating reactive sources during render
|
||||
return productsList.map((product) => {
|
||||
if (props.isCustomerBooking) {
|
||||
product.addons = product.addons.filter((addon) => {
|
||||
return addon.product.display_in_booking_form;
|
||||
});
|
||||
}
|
||||
return product;
|
||||
const filteredAddons = (product.addons || []).filter((addon) => addon.product && addon.product.display_in_booking_form);
|
||||
return {
|
||||
...product,
|
||||
addons: filteredAddons
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -493,18 +676,37 @@ const orderByOrderPriorityCategories = (productsList) => {
|
||||
}
|
||||
|
||||
|
||||
const hasAutoSelectedCategory = ref(false);
|
||||
watch(() => categories.value, (newCategories) => {
|
||||
// If the automaticallySelectCategory prop is set, select the category
|
||||
if (props.automaticallySelectCategory !== null) {
|
||||
if (newCategories.find((category) => parseInt(category.identifier) === props.automaticallySelectCategory) !== undefined) {
|
||||
setProductsCategory(props.automaticallySelectCategory);
|
||||
getProductCategory(props.automaticallySelectCategory, department_id.valueOf())
|
||||
.then((response) => {
|
||||
products.value = response.data.data;
|
||||
});
|
||||
// If the automaticallySelectCategory prop is set, select the category once per mount
|
||||
if (props.automaticallySelectCategory !== null && !hasAutoSelectedCategory.value) {
|
||||
const targetId = Number(props.automaticallySelectCategory);
|
||||
if (newCategories.find((category) => Number(category.identifier) === targetId) !== undefined) {
|
||||
if (getProductsCategory() !== targetId) {
|
||||
setProductsCategory(targetId);
|
||||
}
|
||||
hasAutoSelectedCategory.value = true;
|
||||
getProductCategory(targetId, department_id.value, true, props.customer_number)
|
||||
.then((response) => {
|
||||
products.value = response.data.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
}, { immediate: true });
|
||||
});
|
||||
|
||||
// Guarded category selection to avoid redundant reactive churn
|
||||
const onSelectCategory = (rawId) => {
|
||||
hideRecommendedProducts();
|
||||
// Handle 'null' from mobile <select>
|
||||
const targetId = rawId === 'null' || rawId === null ? null : Number(rawId);
|
||||
if (getProductsCategory() !== targetId) {
|
||||
setProductsCategory(targetId);
|
||||
}
|
||||
getProductCategory(targetId, department_id.value, !!props.isCustomerBooking, props.customer_number)
|
||||
.then((response) => {
|
||||
products.value = response.data.data;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -524,12 +726,7 @@ watch(() => categories.value, (newCategories) => {
|
||||
'is-active': category.identifier === getProductsCategory() && !isShowingRecommended
|
||||
}">
|
||||
<a
|
||||
@click="hideRecommendedProducts();
|
||||
setProductsCategory(category.identifier);
|
||||
getProductCategory(category.identifier, department_id.valueOf())
|
||||
.then((response) => {
|
||||
products = response.data.data;
|
||||
});"
|
||||
@click="onSelectCategory(category.identifier)"
|
||||
:style="{ 'border-bottom-color': category.identifier === getProductsCategory() ? '#3273dc' : '#fff' }"
|
||||
style="
|
||||
border-bottom-width: 0.6rem;
|
||||
@@ -548,12 +745,8 @@ watch(() => categories.value, (newCategories) => {
|
||||
<template #default>
|
||||
<div class="select is-fullwidth mb-2">
|
||||
<select
|
||||
@change="hideRecommendedProducts();
|
||||
setProductsCategory($event.target.value);
|
||||
getProductCategory($event.target.value, department_id.valueOf())
|
||||
.then((response) => {
|
||||
products = response.data.data;
|
||||
});"
|
||||
@change="onSelectCategory($event.target.value)"
|
||||
:value="getProductsCategory() ?? 'null' "
|
||||
>
|
||||
<option v-for="category in categories" :key="category.identifier" :value="category.identifier ?? 'null'">
|
||||
{{ category.name }}
|
||||
@@ -564,8 +757,8 @@ watch(() => categories.value, (newCategories) => {
|
||||
</WhiteBox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-scroll-minimal" :class="{ 'tw-scroll-vertical': !props.compact, 'tw-scroll-horizontal': props.compact }">
|
||||
<div class="columns is-mobile" :class="{ 'is-multiline': !props.compact }">
|
||||
<div class="tw-scroll-minimal" :class="{ 'tw-scroll-vertical': !isCompactMode, 'tw-scroll-horizontal': isCompactMode }" ref="scrollWrapRef">
|
||||
<div class="columns is-mobile" :class="{ 'is-multiline': isGridLayout || !isCustomerBooking }">
|
||||
<!-- Recommended products -->
|
||||
<div class="column is-12" v-if="isShowingRecommended">
|
||||
<!-- Same as last time -->
|
||||
@@ -624,7 +817,7 @@ watch(() => categories.value, (newCategories) => {
|
||||
<div class="card-content">
|
||||
<p class="title is-6">
|
||||
<span>{{ products.find(product => product.id === product_id).name }}</span>
|
||||
<span class="is-float-right">{{ products.find(product => product.id === product_id).price - (products.find(product => product.id === product_id).price * (getUserProductDiscount(products.find(product => product.id === product_id)) / 100)).toFixed(0) }} Kr.</span>
|
||||
<span class="is-float-right" v-if="props.showPrices">{{ products.find(product => product.id === product_id).price - (products.find(product => product.id === product_id).price * (getUserProductDiscount(products.find(product => product.id === product_id)) / 100)).toFixed(0) }} Kr.</span>
|
||||
</p>
|
||||
<p class="subtitle is-6 has-text-grey">{{ products.find(product => product.id === product_id).description }}</p>
|
||||
<div class="content">
|
||||
@@ -668,13 +861,15 @@ watch(() => categories.value, (newCategories) => {
|
||||
</div>
|
||||
<!-- New product display -->
|
||||
<template v-for="product in filterProductAddonsVisibleOnCustomerBooking(filterProductsIsWash(filterProductsVisibleOnCustomerBooking(orderByOrderPriority(products))))" :key="product.id" v-if="!isShowingRecommended">
|
||||
<div class="column" :class="{ 'is-3': props.compact, 'is-6': !props.compact }">
|
||||
<div class="column" :class="{ 'is-3-desktop': useCompactCard, 'is-6-desktop': !useCompactCard, 'is-12-mobile': !useCompactCard, 'is-10-mobile': useCompactCard }">
|
||||
<WhiteBox
|
||||
class="p-0 pos-product-box"
|
||||
:class="{ 'is-selected': selectedProduct === product.id, 'is-compact': props.compact }"
|
||||
:class="{ 'is-selected': effectiveSelectedId === product.id, 'is-compact': useCompactCard }"
|
||||
:ref="el => setItemRef(el, product.id)"
|
||||
>
|
||||
<ProductBox
|
||||
:compact="!!props.compact"
|
||||
:showPrices="props.showPrices"
|
||||
:compact="useCompactCard"
|
||||
:product="product"
|
||||
:id="product.id"
|
||||
:name="product.name"
|
||||
@@ -682,13 +877,13 @@ watch(() => categories.value, (newCategories) => {
|
||||
:price="product.price"
|
||||
:image="getPicture(product.piktogram)"
|
||||
:addons="props.isAddonsVisible ? product.addons : []"
|
||||
:isSelected="selectedProduct === product.id"
|
||||
:isSelected="effectiveSelectedId === product.id"
|
||||
:selectedAddons="productAddons"
|
||||
:toggleProductAddon="toggleProductAddon"
|
||||
:getProductAddonQuantity="getProductAddonQuantity"
|
||||
:addProductAddon="addProductAddon"
|
||||
:subtractProductAddon="subtractProductAddon"
|
||||
@click="!isProductSelected(product.id) ? selectProduct(product) : void 0"
|
||||
@click="selectProduct(product);"
|
||||
@addToCart="addProductWithAddonsToOrder(product.id);"
|
||||
:unselect="unselectProduct"
|
||||
:setProductAddonNote="setProductAddonNote"
|
||||
@@ -799,4 +994,19 @@ watch(() => categories.value, (newCategories) => {
|
||||
.tw-scroll-vertical::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Enforce single-row horizontal layout and ensure horizontal overflow exists */
|
||||
.tw-scroll-horizontal > .columns {
|
||||
flex-wrap: nowrap; /* prevent wrapping so we get a single horizontal row */
|
||||
}
|
||||
.tw-scroll-horizontal .column {
|
||||
flex: 0 0 auto; /* items keep intrinsic/column width and overflow horizontally */
|
||||
}
|
||||
/* Optional: nicer feel when manually scrolling */
|
||||
.tw-scroll-horizontal {
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
.tw-scroll-horizontal .pos-product-box {
|
||||
scroll-snap-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -12,14 +12,18 @@ const props = defineProps({
|
||||
type: [String, Function, Object],
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
default: 'is-warning',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="message">
|
||||
<div class="notification is-warning">
|
||||
<button class="delete"
|
||||
<div class="notification" :class="template">
|
||||
<button class="delete" v-show="false"
|
||||
@click="clearErrors()"
|
||||
></button>
|
||||
{{ typeof message === 'function' ? message() : message }}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup>
|
||||
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||
import logo from "@/assets/branding/truckwash-banner-white-compressed.png";
|
||||
import { showAuthSignOutForm } from "@/components/forms/auth/authSignOutForm.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed } from "vue";
|
||||
import Swal from "sweetalert2";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
const VITE_COMMIT_HASH = import.meta.env.VITE_COMMIT_HASH || 'No commit hash';
|
||||
const VITE_IS_DEV = import.meta.env.DEV || false;
|
||||
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*/
|
||||
const localVersion = VITE_COMMIT_HASH;
|
||||
const localStorageLastCheckKey = "lastVersionCheck";
|
||||
const versionCheckUrl = "/worker/version";
|
||||
const versionCheckDelay = 1000 * 60 * 5; // 5 minutes
|
||||
const latestVersion = ref<string | null>(null);
|
||||
const lastVersionCheck = ref(localStorage.getItem(localStorageLastCheckKey) ? parseInt(localStorage.getItem(localStorageLastCheckKey) || "0") : 0);
|
||||
/**
|
||||
* Computed
|
||||
*/
|
||||
const currentVersion = computed(() => localVersion || "unknown");
|
||||
const isUpdateAvailable = computed(() => {
|
||||
return latestVersion.value !== null && latestVersion.value !== currentVersion.value;
|
||||
});
|
||||
const shouldCheckForUpdate = computed(() => {
|
||||
// In dev mode, do not check for updates
|
||||
if (VITE_IS_DEV) {
|
||||
return false;
|
||||
}
|
||||
// If never checked before, should check
|
||||
if (lastVersionCheck.value === 0) {
|
||||
return true;
|
||||
}
|
||||
// If enough time has passed since last check, should check
|
||||
const now = Date.now();
|
||||
return (now - lastVersionCheck.value) > versionCheckDelay;
|
||||
});
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
const showUpdateAvailableNotification = () => {
|
||||
Swal.fire({
|
||||
title: 'Opdatering tilgængelig',
|
||||
text: `En ny version (${latestVersion.value}) er tilgængelig! Du bruger i øjeblikket version ${currentVersion.value}. Opdater venligst siden for at få den nyeste version.`,
|
||||
icon: 'info',
|
||||
confirmButtonText: 'Opdater nu',
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
const checkForUpdate = async () => {
|
||||
const response = await SessionUser.request(
|
||||
versionCheckUrl,
|
||||
'GET',
|
||||
);
|
||||
// Verify response
|
||||
if (!response || !response.data || !response.data.data || !response.data.data.version) {
|
||||
console.error("Invalid version check response:", response);
|
||||
return;
|
||||
}
|
||||
// Update last check time
|
||||
lastVersionCheck.value = Date.now();
|
||||
localStorage.setItem(localStorageLastCheckKey, lastVersionCheck.value.toString());
|
||||
// Update latest version
|
||||
latestVersion.value = response.data.data.version;
|
||||
// Notify user if update is available
|
||||
if (isUpdateAvailable.value) {
|
||||
showUpdateAvailableNotification();
|
||||
}
|
||||
return response;
|
||||
}
|
||||
/**
|
||||
* Lifecycle Hooks
|
||||
*/
|
||||
onMounted(() => {
|
||||
if (shouldCheckForUpdate.value) {
|
||||
checkForUpdate();
|
||||
}
|
||||
window.addEventListener('focus', () => {
|
||||
if (shouldCheckForUpdate.value) {
|
||||
checkForUpdate();
|
||||
}
|
||||
});
|
||||
window.setInterval(() => {
|
||||
if (shouldCheckForUpdate.value) {
|
||||
checkForUpdate();
|
||||
}
|
||||
}, versionCheckDelay);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -21,6 +21,8 @@ export type NavigationItemProps = {
|
||||
permissions?: string[];
|
||||
// Children can be used for nested navigation items
|
||||
children?: NavigationItemProps[] | [];
|
||||
classes?: string[]; // Optional CSS classes for the navigation item
|
||||
hidden?: boolean; // Optional property to hide the navigation item
|
||||
};
|
||||
|
||||
type NavigationItemType = typeof props;
|
||||
@@ -61,7 +63,13 @@ const props = {
|
||||
type: Array as PropType<NavigationItemProps[]>,
|
||||
required: false,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
// Optional CSS classes for the navigation item
|
||||
classes: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -89,13 +97,13 @@ export default defineComponent({
|
||||
|
||||
<template>
|
||||
<router-link v-if="to" v-bind:to="to">
|
||||
<div class="navbar-item">
|
||||
<div class="navbar-item" :class="classes">
|
||||
<i v-if="icon" :class="icon"></i>
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
<!-- If 'to' is not provided, we can assume it's a category or a non-link item -->
|
||||
<div v-else class="navbar-item">
|
||||
<div v-else class="navbar-item" :class="classes">
|
||||
<i v-if="icon" :class="icon"></i>
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
|
||||
@@ -43,18 +43,18 @@ export function useNavigationItems() {
|
||||
return itemsGuest;
|
||||
});
|
||||
|
||||
const parsedItems = computed(() => {
|
||||
const parsedItems = () => {
|
||||
if (SessionUser) {
|
||||
if (SessionUser.canAccessUser() && isUser.value) {
|
||||
return parsedItemsUser;
|
||||
return parsedItemsUser();
|
||||
} else if (SessionUser.canAccessAdmin() && isAdmin.value) {
|
||||
return parsedItemsAdmin;
|
||||
return parsedItemsAdmin();
|
||||
} else if (SessionUser.canAccessSuperUser()) {
|
||||
return parsedItemsSuperUser;
|
||||
return parsedItemsSuperUser();
|
||||
}
|
||||
}
|
||||
return parsedItemsGuest;
|
||||
});
|
||||
return parsedItemsGuest();
|
||||
};
|
||||
|
||||
// Keep exposing global parsed items as-is
|
||||
return {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { NavigationItemProps } from "@/components/models/navigation/NavigationItem.vue";
|
||||
import { defineComponent, ref, computed} from 'vue';
|
||||
import {defineComponent, ref, computed, watch} from 'vue';
|
||||
import { SessionUser } from '@/components/session/token/SessionUser.vue';
|
||||
const department_id = ref(window.location.pathname.split('/')[2] || 'default');
|
||||
import { getDepartmentName } from "@/components/session/token/SessionUser/Objects/economicDepartments.vue";
|
||||
const department_id = computed(() => {return SessionUser.functions.getDepartmentIdFromUrl()});
|
||||
const department_booking_count = ref(0); // Placeholder for actual booking count logic
|
||||
const fetchDepartmentBookingCount = () => {
|
||||
// Check if the department_id is a valid number
|
||||
@@ -22,6 +23,10 @@ const fetchDepartmentBookingCount = () => {
|
||||
});
|
||||
}
|
||||
|
||||
const isDepartmentSet = computed(() => {
|
||||
return department_id.value !== 'default' && parseInt(department_id.value) > 0;
|
||||
});
|
||||
|
||||
// Fetch the booking count every 5 seconds
|
||||
setInterval(fetchDepartmentBookingCount, 5000);
|
||||
|
||||
@@ -31,10 +36,25 @@ const items = computed<NavigationItemProps[]>(() => [
|
||||
to: `/admin`,
|
||||
permissions: ['admin'],
|
||||
},
|
||||
// Select available department options
|
||||
{
|
||||
label: 'Vælg afdeling',
|
||||
to: `/admin/departments`,
|
||||
type: 'category',
|
||||
hidden: isDepartmentSet.value,
|
||||
permissions: ['admin', 'list_departments'],
|
||||
children: SessionUser.functions.getAccessibleDepartments().map((department: any) => ({
|
||||
label: getDepartmentName(department),
|
||||
to: `/admin/${department}`,
|
||||
type: 'department',
|
||||
permissions: ['admin', 'view_department'],
|
||||
})),
|
||||
},
|
||||
// Kassesystem
|
||||
{
|
||||
label: 'Kassesystem',
|
||||
type: 'category',
|
||||
hidden: !isDepartmentSet.value,
|
||||
permissions: ['admin', 'add_order'],
|
||||
children: [
|
||||
{
|
||||
@@ -52,6 +72,7 @@ const items = computed<NavigationItemProps[]>(() => [
|
||||
],
|
||||
},
|
||||
// Tidsbookinger
|
||||
/**
|
||||
{
|
||||
label: 'Tidsbookinger',
|
||||
type: 'category',
|
||||
@@ -82,13 +103,14 @@ const items = computed<NavigationItemProps[]>(() => [
|
||||
permissions: ['admin', 'department_timebookings_types_get'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}, */
|
||||
// Bookinger
|
||||
{
|
||||
label: 'Bookinger',
|
||||
to: `/admin/${department_id.value}/modules/bookings`,
|
||||
type: 'department',
|
||||
permissions: ['admin', 'list_bookings'],
|
||||
hidden: !isDepartmentSet.value,
|
||||
badge: {
|
||||
// Example badge, replace with actual logic if needed
|
||||
type: 'info',
|
||||
@@ -102,6 +124,7 @@ const items = computed<NavigationItemProps[]>(() => [
|
||||
to: `/admin/${department_id.value}/modules/daily-report`,
|
||||
type: 'department',
|
||||
permissions: ['admin', 'list_department_daily_reports'],
|
||||
hidden: !isDepartmentSet.value,
|
||||
},
|
||||
// Notifikationer
|
||||
{
|
||||
@@ -109,14 +132,16 @@ const items = computed<NavigationItemProps[]>(() => [
|
||||
to: `/admin/${department_id.value}/modules/notifications`,
|
||||
type: 'department',
|
||||
permissions: ['admin', 'department_notification_sms_get'],
|
||||
hidden: !isDepartmentSet.value,
|
||||
},
|
||||
// Intranet
|
||||
/**
|
||||
{
|
||||
label: 'Intranet',
|
||||
to: `/admin/${department_id.value}/modules/intranet`,
|
||||
type: 'department',
|
||||
permissions: ['admin', 'list_intranet_posts'],
|
||||
}
|
||||
}*/
|
||||
]);
|
||||
const isDepartment = computed(() => department_id.value !== 'default' && parseInt(department_id.value) > 0);
|
||||
|
||||
|
||||
@@ -19,45 +19,73 @@ const addChildrenIfApplicable = (item: NavigationItemProps) => {
|
||||
if (item.type === 'link' && item.permissions) {
|
||||
// Check if the user has at least one of the required permissions
|
||||
for (const perm of item.permissions) {
|
||||
if (hasPermission(perm)) {
|
||||
return [item];
|
||||
if (!hasPermission(perm)) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return []; // User lacks required permissions
|
||||
return [item]; // User lacks required permissions
|
||||
}
|
||||
}
|
||||
|
||||
const items = computed<NavigationItemProps[]>(() => [
|
||||
/**
|
||||
* {
|
||||
* label: getNameIfSignedIn('Hjem'),
|
||||
* type: 'category',
|
||||
* children: [
|
||||
* ...addChildrenIfApplicable({
|
||||
* label: 'Kunde side',
|
||||
* to: `/user`,
|
||||
* type: 'link',
|
||||
* permissions: ['user'],
|
||||
* }) || [],
|
||||
* ...addChildrenIfApplicable({
|
||||
* label: 'Afdelings side',
|
||||
* to: `/admin`,
|
||||
* type: 'link',
|
||||
* permissions: ['admin'],
|
||||
* }) || [],
|
||||
* ...addChildrenIfApplicable({
|
||||
* label: 'Backoffice',
|
||||
* to: `/superuser`,
|
||||
* type: 'link',
|
||||
* permissions: ['superuser'],
|
||||
* }) || [],
|
||||
* ...addChildrenIfApplicable({
|
||||
* label: 'Indstillinger',
|
||||
* to: `/user/profile`,
|
||||
* type: 'link',
|
||||
* permissions: ['user'],
|
||||
* }) || [],
|
||||
* ...addChildrenIfApplicable({
|
||||
* label: 'Tilbage til egen konto →',
|
||||
* to: `/auth/return`,
|
||||
* type: 'link',
|
||||
* permissions: ['has_superuser_token'],
|
||||
* }) || [],
|
||||
* ...addChildrenIfApplicable({
|
||||
* label: 'Log ud →',
|
||||
* to: `/logout`,
|
||||
* type: 'link',
|
||||
* permissions: ['user'],
|
||||
* }) || []
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
{
|
||||
label: getNameIfSignedIn('Hjem'),
|
||||
type: 'category',
|
||||
children: [
|
||||
...addChildrenIfApplicable({
|
||||
label: 'Kunde side',
|
||||
to: `/user`,
|
||||
type: 'link',
|
||||
permissions: ['user'],
|
||||
}) || [],
|
||||
...addChildrenIfApplicable({
|
||||
label: 'Afdelings side',
|
||||
to: `/admin`,
|
||||
type: 'link',
|
||||
permissions: ['admin'],
|
||||
}) || [],
|
||||
...addChildrenIfApplicable({
|
||||
label: 'Backoffice',
|
||||
to: `/superuser`,
|
||||
type: 'link',
|
||||
permissions: ['superuser'],
|
||||
}) || [],
|
||||
...addChildrenIfApplicable({
|
||||
label: 'Sign out →',
|
||||
to: `/logout`,
|
||||
type: 'link',
|
||||
permissions: ['user'],
|
||||
}) || []
|
||||
]
|
||||
}
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.global.language.settings),
|
||||
to: `/user/profile`,
|
||||
type: 'link',
|
||||
permissions: ['user'],
|
||||
hidden: !window.location.pathname.startsWith('/user')
|
||||
},
|
||||
{
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.global.language.logout),
|
||||
to: `/logout`,
|
||||
type: 'link',
|
||||
permissions: ['user'],
|
||||
hidden: !SessionUser.authenticated.value
|
||||
},
|
||||
]);
|
||||
|
||||
const parsedItems = () => {
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { NavigationItemProps } from "@/components/models/navigation/NavigationItem.vue";
|
||||
import {computed, defineComponent, ref} from 'vue';
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
|
||||
|
||||
|
||||
const items = computed<NavigationItemProps[]>(() => [
|
||||
{
|
||||
label: 'Log ind',
|
||||
to: '/login',
|
||||
},
|
||||
{
|
||||
label: 'Log ind (med QR-kode)',
|
||||
to: '/login/qr',
|
||||
},
|
||||
{
|
||||
label: 'Opret bruger',
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.global.language.register),
|
||||
to: '/register',
|
||||
},
|
||||
{
|
||||
label: 'Book Vask',
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.global.language.login),
|
||||
to: '/login',
|
||||
hidden: SessionUser.authenticated.value,
|
||||
children: [
|
||||
{
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.global.language.login_with_qr_code),
|
||||
to: '/login/qr',
|
||||
hidden: !SessionUser.functions.device.isMobile(),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Book vask',
|
||||
to: '/guest/book/wash',
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { NavigationItemProps } from "@/components/models/navigation/NavigationItem.vue";
|
||||
import {computed, defineComponent, ref} from 'vue';
|
||||
import {computed, defineComponent} from 'vue';
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
|
||||
const firstToUpperCase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
||||
@@ -48,10 +48,11 @@ const items = computed<NavigationItemProps[]>(() => [
|
||||
{ label: firstToUpperCase(SessionUser.superUser.modules.licenseplaterecognizer.meta.labels.multiple), to: '/superuser/configuration/licenseplaterecognizer'},
|
||||
{ label: firstToUpperCase(SessionUser.superUser.modules.virkdata.meta.labels.multiple), to: '/superuser/configuration/virkdata'},
|
||||
{ label: firstToUpperCase(SessionUser.superUser.modules.shelly.meta.labels.multiple), to: '/superuser/configuration/shelly'},
|
||||
{ label: firstToUpperCase(SessionUser.superUser.modules.selfserve.meta.labels.multiple), to: '/superuser/configuration/selfserve'},
|
||||
]},
|
||||
// Other
|
||||
{ label: 'Other', type: 'category', children: [
|
||||
{ label: 'Statistics', to: '/superuser/statistics' },
|
||||
//{ label: 'Statistics', to: '/superuser/statistics' },
|
||||
{ label: firstToUpperCase(SessionUser.objects.vehicles.meta.labels.multiple), to: '/superuser/vehicles' },
|
||||
]},
|
||||
]);
|
||||
|
||||
@@ -1,57 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { NavigationItemProps } from "@/components/models/navigation/NavigationItem.vue";
|
||||
import {computed, defineComponent, ref} from 'vue';
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
|
||||
const items = computed<NavigationItemProps[]>(() => [
|
||||
/**
|
||||
* {
|
||||
* label: 'Kunde side',
|
||||
* type: 'category',
|
||||
* permissions: ['user'],
|
||||
* children: [
|
||||
* {
|
||||
* label: 'Mine vaske',
|
||||
* to: `/user/orders`,
|
||||
* type: 'link',
|
||||
* permissions: ['user'],
|
||||
* },
|
||||
* {
|
||||
* label: 'Mine køretøjer',
|
||||
* to: `/user/vehicles`,
|
||||
* type: 'link',
|
||||
* permissions: ['user'],
|
||||
* },
|
||||
* {
|
||||
* label: 'Mine bookinger',
|
||||
* to: `/user/bookings`,
|
||||
* type: 'link',
|
||||
* permissions: ['user'],
|
||||
* },
|
||||
* {
|
||||
* label: 'Mine fakturaer',
|
||||
* to: `/user/invoices`,
|
||||
* type: 'link',
|
||||
* permissions: ['user', 'user_invoices'],
|
||||
* },
|
||||
* {
|
||||
* label: 'Mine materialer',
|
||||
* to: `/user/materials`,
|
||||
* type: 'link',
|
||||
* permissions: ['user', 'user_materials'],
|
||||
* },
|
||||
* ],
|
||||
* },
|
||||
*/
|
||||
{
|
||||
label: 'Hjem',
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.global.language.homepage),
|
||||
to: `/user`,
|
||||
type: 'link',
|
||||
permissions: ['user'],
|
||||
},
|
||||
{
|
||||
label: 'Kunde side',
|
||||
type: 'category',
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.vehicles.meta.labels.multiple),
|
||||
to: `/user/vehicles`,
|
||||
type: 'link',
|
||||
permissions: ['user'],
|
||||
children: [
|
||||
{
|
||||
label: 'Mine vaske',
|
||||
to: `/user/orders`,
|
||||
type: 'link',
|
||||
permissions: ['user'],
|
||||
},
|
||||
{
|
||||
label: 'Mine køretøjer',
|
||||
to: `/user/vehicles`,
|
||||
type: 'link',
|
||||
permissions: ['user'],
|
||||
},
|
||||
{
|
||||
label: 'Mine bookinger',
|
||||
to: `/user/bookings`,
|
||||
type: 'link',
|
||||
permissions: ['user'],
|
||||
},
|
||||
{
|
||||
label: 'Mine fakturaer',
|
||||
to: `/user/invoices`,
|
||||
type: 'link',
|
||||
permissions: ['user', 'user_invoices'],
|
||||
},
|
||||
{
|
||||
label: 'Mine materialer',
|
||||
to: `/user/materials`,
|
||||
type: 'link',
|
||||
permissions: ['user', 'user_materials'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Book Vask',
|
||||
to: '/user/bookings/new',
|
||||
type: 'button',
|
||||
permissions: ['user', 'user_bookings'],
|
||||
color: 'primary',
|
||||
}
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.bookings.meta.labels.multiple),
|
||||
to: `/user/bookings`,
|
||||
type: 'link',
|
||||
permissions: ['user'],
|
||||
},
|
||||
{
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.global.language.wash_multiple),
|
||||
to: `/user/orders`,
|
||||
type: 'link',
|
||||
permissions: ['user'],
|
||||
},
|
||||
{
|
||||
label: SessionUser.functions.ucFirst(SessionUser.objects.collectedOrderInvoices.meta.labels.multiple),
|
||||
to: `/user/invoices`,
|
||||
type: 'link',
|
||||
permissions: ['user', 'user_invoices'],
|
||||
},
|
||||
/**
|
||||
* {
|
||||
* label: 'Book Vask',
|
||||
* to: '/user/bookings/new',
|
||||
* type: 'button',
|
||||
* permissions: ['user', 'user_bookings'],
|
||||
* color: 'primary',
|
||||
* }
|
||||
*/
|
||||
]);
|
||||
|
||||
const parsedItems = () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { vehicles_matching, searchVehicle, register_new_search, is_latest_search
|
||||
import { setCustomerId, pos } from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
||||
import {PosSearchResult} from "@/components/displays/department/pos/steps/mobile/objects/PosSearchResult.vue";
|
||||
import Swal from "sweetalert2";
|
||||
import SessionUser from "@/components/session/token/SessionUser.vue";
|
||||
defineComponent({
|
||||
name: "RegistrationNumberSearchResult"
|
||||
});
|
||||
@@ -72,6 +73,9 @@ const applyBookingAutomatically = (booking: any) => {
|
||||
console.warn("Booking already applied, ignoring.");
|
||||
return;
|
||||
}
|
||||
if (!booking?.id) {
|
||||
console.warn("No booking found to apply.");
|
||||
}
|
||||
pos.metadata.setBookingId(booking.id); // Set the booking ID to prevent multiple prompts, this is reset if the user cancels.
|
||||
// If the input registration numbers manually is displayed, hide it.
|
||||
pos.views.manualInput.value = false;
|
||||
@@ -88,25 +92,25 @@ const applyBookingAutomatically = (booking: any) => {
|
||||
//console.warn("User confirmed to apply booking:", booking);
|
||||
// Apply the booking
|
||||
pos.metadata.setBookingId(booking.id);
|
||||
pos.metadata.setNotes(booking.notes || `Booking ID: ${booking.id}`);
|
||||
pos.metadata.setReference(booking.reference_number || null);
|
||||
pos.metadata.setNotes(booking.note || `Booking ID: ${booking.id}`);
|
||||
pos.metadata.setReference(booking.reference || null);
|
||||
// Set the registration numbers
|
||||
reg_1.value = booking.regNrTraekker;
|
||||
reg_2.value = booking.regNrTrailer || null;
|
||||
reg_1.value = booking.reg_1;
|
||||
reg_2.value = booking.reg_2 || null;
|
||||
// Select the vehicles
|
||||
pos.vehicles.select(1, {
|
||||
reg: booking.regNrTraekker,
|
||||
reg: booking.reg_1,
|
||||
type: 0, // TODO: Determine type from booking if possible
|
||||
customer_id: booking.customer_id,
|
||||
customer_id: booking.customer_number,
|
||||
status: 'booked',
|
||||
barred: false,
|
||||
booking_id: booking.id,
|
||||
});
|
||||
if (booking.regNrTrailer) {
|
||||
if (booking.reg_2) {
|
||||
pos.vehicles.select(2, {
|
||||
reg: booking.regNrTrailer,
|
||||
reg: booking.reg_2,
|
||||
type: 0, //TODO: Determine type from booking if possible
|
||||
customer_id: booking.customer_id,
|
||||
customer_id: booking.customer_number,
|
||||
status: 'booked',
|
||||
barred: false,
|
||||
booking_id: booking.id,
|
||||
@@ -129,9 +133,14 @@ const applyBookingAutomatically = (booking: any) => {
|
||||
const onSelect = (result: PosSearchResult) => {
|
||||
//console.warn("onSelect:", result);
|
||||
// Check if the vehicle has a booking
|
||||
if (doesVehiclePlateHaveBooking(result.registrationNumber)) {
|
||||
if (result?.bookingId && result.bookingId > 0) {
|
||||
//console.warn("Vehicle has a booking:", getVehiclePlateBooking(result.registrationNumber));
|
||||
applyBookingAutomatically(getVehiclePlateBooking(result.registrationNumber));
|
||||
SessionUser.objects.order_bookings.get.single(result.bookingId).then((booking) => {
|
||||
// Apply the booking automatically
|
||||
applyBookingAutomatically(booking);
|
||||
}).catch((error) => {
|
||||
console.error("Error fetching booking:", error);
|
||||
});
|
||||
}
|
||||
// Search and select the customer based on the registration number
|
||||
if (props.modifyCustomerOnChange) {
|
||||
@@ -207,6 +216,9 @@ watch(isSearching, (newValue) => {
|
||||
watch(() => props.searchQuery, (newValue) => {
|
||||
if (newValue) {
|
||||
performSearch(newValue);
|
||||
if (props.automaticallySelect) {
|
||||
automaticallySelect();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -232,6 +244,7 @@ watch(() => vehicles_matching.value, (newValue) => {
|
||||
reference: vehicle?.reference || null,
|
||||
lastOrderId: vehicle?.last_order_id || null,
|
||||
washSubscription: vehicle?.wash_subscription || null,
|
||||
bookingId: vehicle?.booking_id || null,
|
||||
} as PosSearchResult;
|
||||
});
|
||||
//console.warn('Transformed PosSearchResult:', result);
|
||||
@@ -244,24 +257,26 @@ watch(() => vehicles_matching.value, (newValue) => {
|
||||
});
|
||||
|
||||
const automaticallySelect = () => {
|
||||
if (props.automaticallySelect && props.searchQuery !== '' && vehicles_matching.value.length > 0) {
|
||||
const withoutDuplicates = preventDuplicates(searchResults.value);
|
||||
if (props.automaticallySelect && props.searchQuery !== '' && withoutDuplicates.length > 0) {
|
||||
//console.warn("Automatically selecting vehicle for reg:", props.searchQuery, "from vehicles_matching:", vehicles_matching.value);
|
||||
// Check if the vehicle is found in the results.
|
||||
// If the vehicle is found.
|
||||
if (vehicles_matching.value.find(v => v.reg === props.searchQuery)) {
|
||||
const match = vehicles_matching.value.find(v => v.reg === props.searchQuery);
|
||||
//console.warn("Automatically selecting vehicle:", match);
|
||||
if (withoutDuplicates.find(v => v.registrationNumber.toUpperCase() === props.searchQuery.toUpperCase())) {
|
||||
const match = withoutDuplicates.find(v => v.registrationNumber.toUpperCase() === props.searchQuery.toUpperCase());
|
||||
console.warn("Automatically selecting vehicle:", match);
|
||||
// Emit the selected vehicle object
|
||||
onSelect({
|
||||
registrationNumber: props.searchQuery,
|
||||
customerName: match?.customer_name || "Unknown Customer",
|
||||
customerId: match?.customer_id || 0,
|
||||
customerStatus: determineCustomerStatus(match),
|
||||
registrationNumber: props.searchQuery.toUpperCase(),
|
||||
customerName: match?.customerName || "Unknown Customer",
|
||||
customerId: match?.customerId || 0,
|
||||
customerStatus: match ? match.customerStatus : determineCustomerStatus({reg: props.searchQuery} as any),
|
||||
type: match?.type || null,
|
||||
barred: match?.barred,
|
||||
reference: match?.reference || null,
|
||||
lastOrderId: match?.last_order_id || null,
|
||||
washSubscription: match?.wash_subscription,
|
||||
lastOrderId: match?.lastOrderId || null,
|
||||
washSubscription: match?.washSubscription,
|
||||
bookingId: match?.bookingId || null,
|
||||
});
|
||||
} else {
|
||||
console.warn("No exact match found for automatic selection.");
|
||||
@@ -272,19 +287,19 @@ const automaticallySelect = () => {
|
||||
const determineCustomerStatus = (vehicle: any): VehicleStatusKey => {
|
||||
//console.warn('Determine customer status for vehicle:', vehicle);
|
||||
// Determine the customer status based on the vehicle object
|
||||
if (doesVehiclePlateHaveBooking(vehicle.reg)) {
|
||||
if (vehicle?.booking_id) {
|
||||
//console.warn('Vehicle has a booking:', getVehiclePlateBooking(vehicle.reg));
|
||||
return 'booked';
|
||||
} else if (vehicle?.status === 'known') {
|
||||
//console.warn('Vehicle is known:', vehicle);
|
||||
return 'known'
|
||||
} else if (!vehicle.customer_id && !vehicle.customer_name) {
|
||||
} else if (vehicle?.status === 'unknown') {
|
||||
//console.warn('Vehicle is unknown:', vehicle);
|
||||
return 'unknown';
|
||||
} else if (vehicle.barred) {
|
||||
} else if (vehicle?.status === 'card' || vehicle?.barred === true) {
|
||||
//console.warn('Vehicle is barred:', vehicle);
|
||||
return 'card';
|
||||
} else if (vehicle.status === 'verified') {
|
||||
} else if (vehicle?.status === 'verified') {
|
||||
//console.warn('Vehicle is verified:', vehicle);
|
||||
return 'verified';
|
||||
} else {
|
||||
@@ -296,7 +311,7 @@ const determineCustomerStatus = (vehicle: any): VehicleStatusKey => {
|
||||
const importPendingBookings = () => {
|
||||
// Import pending bookings and update the search results
|
||||
pendingBookings.value.forEach(booking => {
|
||||
const vehicle = vehicles_matching.value.find(v => v.reg === booking.vehicle_plate);
|
||||
const vehicle = vehicles_matching.value.find(v => v.reg.toUpperCase() === booking.reg_1.toUpperCase() || v.reg.toUpperCase() === booking.reg_2?.toUpperCase());
|
||||
if (vehicle) {
|
||||
vehicle.customer_name = booking.customer_name;
|
||||
//vehicle.customer_id = booking.customer_id;
|
||||
@@ -312,6 +327,25 @@ onMounted(() => {
|
||||
performSearch(props.searchQuery);
|
||||
}
|
||||
});
|
||||
|
||||
const preventDuplicates = (results: PosSearchResult[]) => {
|
||||
// The same registration number should not appear multiple times in the results
|
||||
const seen = new Set();
|
||||
const uniqueResults: PosSearchResult[] = [];
|
||||
for (const result of results) {
|
||||
const strippedReg = stripSpaces(result.registrationNumber).toUpperCase();
|
||||
if (!seen.has(strippedReg)) {
|
||||
//console.warn("Adding unique result:", result);
|
||||
seen.add(strippedReg);
|
||||
uniqueResults.push(result);
|
||||
}
|
||||
}
|
||||
//console.warn("Unique results after preventing duplicates:", uniqueResults);
|
||||
return uniqueResults;
|
||||
}
|
||||
const stripSpaces = (str: string) => {
|
||||
return str.replace(/\s+/g, '');
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<!-- Debugging information
|
||||
@@ -319,7 +353,7 @@ onMounted(() => {
|
||||
{{ vehicles_matching.length }} vehicles matching
|
||||
{{ pendingBookings.length }} pending bookings
|
||||
-->
|
||||
<div v-for="result in searchResults" :key="result.customerId" style="border-bottom: 1px solid #EAEAEA" @click="onSelect(result)" class="is-clickable" v-if="!props.isHidden">
|
||||
<div v-for="result in preventDuplicates(searchResults)" :key="result.registrationNumber" style="border-bottom: 1px solid #EAEAEA" @click="onSelect(result)" class="is-clickable" v-if="!props.isHidden">
|
||||
<div class="columns is-mobile pb-0">
|
||||
<div class="column is-align-content-center pl-4">
|
||||
<p>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-6">
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
import { authenticatedRequest, unauthenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
// Define the departments
|
||||
@@ -40,4 +40,14 @@ export const getDepartmentName = (id) => {
|
||||
export const getDepartmentDescription = (id) => {
|
||||
return departments.value.find(department => department.id === id).description;
|
||||
};
|
||||
|
||||
export const getDepartmentsGuest = async (queryParams = {}) => {
|
||||
isLoading.value = true;
|
||||
const queryString = new URLSearchParams(queryParams).toString();
|
||||
const request = await unauthenticatedRequest('/guest/departments' + (queryString ? `?${queryString}` : ''),'get');
|
||||
departments.value = request.data.data;
|
||||
isLoading.value = false;
|
||||
console.log(departments.value);
|
||||
return departments.value;
|
||||
};
|
||||
</script>
|
||||
@@ -243,7 +243,6 @@ export const isLatestSearch = (search) => {
|
||||
export const loadList = () => {
|
||||
loadSwitch(true);
|
||||
// clear the list
|
||||
list.value = [];
|
||||
paginatedGetRequest();
|
||||
};
|
||||
|
||||
|
||||