1
client/.eslintignore
Normal file
@@ -0,0 +1 @@
|
||||
src/libs/*.js
|
||||
24
client/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
/tests/e2e/videos/
|
||||
/tests/e2e/screenshots/
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
36
client/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# sockeye
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run your tests
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Run your end-to-end tests
|
||||
```
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Run your unit tests
|
||||
```
|
||||
npm run test:unit
|
||||
```
|
||||
1
client/appdev.bat
Normal file
@@ -0,0 +1 @@
|
||||
npm run serve
|
||||
947
client/audit.txt
Normal file
@@ -0,0 +1,947 @@
|
||||
=== npm audit security report ===
|
||||
|
||||
# Run npm install --save-dev babel-jest@24.1.0 to resolve 1 vulnerability
|
||||
SEMVER WARNING: Recommended action is a potentially breaking change
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Dependency of babel-jest [dev]
|
||||
|
||||
Path babel-jest > babel-plugin-istanbul > test-exclude >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
|
||||
|
||||
Manual Review
|
||||
Some vulnerabilities require your attention to resolve
|
||||
|
||||
Visit https://go.npm.me/audit-guide for additional guidance
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > babel-jest >
|
||||
babel-plugin-istanbul > test-exclude > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-config >
|
||||
babel-jest > babel-plugin-istanbul > test-exclude >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-config >
|
||||
jest-environment-jsdom > jest-util > jest-message-util >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-config >
|
||||
jest-environment-node > jest-util > jest-message-util >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-config >
|
||||
jest-jasmine2 > expect > jest-message-util > micromatch >
|
||||
braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-config >
|
||||
jest-jasmine2 > jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-config >
|
||||
jest-jasmine2 > jest-snapshot > jest-message-util >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-config >
|
||||
jest-jasmine2 > jest-util > jest-message-util > micromatch >
|
||||
braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-config >
|
||||
jest-util > jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-config >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli >
|
||||
jest-environment-jsdom > jest-util > jest-message-util >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-haste-map
|
||||
> micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli >
|
||||
jest-resolve-dependencies > jest-snapshot >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-config > babel-jest > babel-plugin-istanbul >
|
||||
test-exclude > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-config > jest-environment-jsdom > jest-util >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-config > jest-environment-node > jest-util >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-config > jest-jasmine2 > expect > jest-message-util >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-config > jest-jasmine2 > jest-message-util > micromatch
|
||||
> braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-config > jest-jasmine2 > jest-snapshot >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-config > jest-jasmine2 > jest-util > jest-message-util
|
||||
> micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-config > jest-util > jest-message-util > micromatch >
|
||||
braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-config > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-haste-map > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-jasmine2 > expect > jest-message-util > micromatch >
|
||||
braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-jasmine2 > jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-jasmine2 > jest-snapshot > jest-message-util >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-jasmine2 > jest-util > jest-message-util > micromatch >
|
||||
braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > babel-plugin-istanbul > test-exclude >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-config > babel-jest >
|
||||
babel-plugin-istanbul > test-exclude > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-config > jest-environment-jsdom >
|
||||
jest-util > jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-config > jest-environment-node >
|
||||
jest-util > jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-config > jest-jasmine2 > expect >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-config > jest-jasmine2 >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-config > jest-jasmine2 > jest-snapshot >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-config > jest-jasmine2 > jest-util >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-config > jest-util > jest-message-util >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-config > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-haste-map > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-snapshot > jest-message-util >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > jest-util > jest-message-util > micromatch >
|
||||
braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-runtime > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runner >
|
||||
jest-util > jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
babel-plugin-istanbul > test-exclude > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-config > babel-jest > babel-plugin-istanbul >
|
||||
test-exclude > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-config > jest-environment-jsdom > jest-util >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-config > jest-environment-node > jest-util >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-config > jest-jasmine2 > expect > jest-message-util >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-config > jest-jasmine2 > jest-message-util > micromatch
|
||||
> braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-config > jest-jasmine2 > jest-snapshot >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-config > jest-jasmine2 > jest-util > jest-message-util
|
||||
> micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-config > jest-util > jest-message-util > micromatch >
|
||||
braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-config > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-haste-map > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-snapshot > jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
jest-util > jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-runtime >
|
||||
micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-snapshot
|
||||
> jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > jest-util >
|
||||
jest-message-util > micromatch > braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
|
||||
Low Regular Expression Denial of Service
|
||||
|
||||
Package braces
|
||||
|
||||
Patched in >=2.3.1
|
||||
|
||||
Dependency of @vue/cli-plugin-unit-jest [dev]
|
||||
|
||||
Path @vue/cli-plugin-unit-jest > jest > jest-cli > micromatch >
|
||||
braces
|
||||
|
||||
More info https://nodesecurity.io/advisories/786
|
||||
|
||||
found 64 low severity vulnerabilities in 40594 scanned packages
|
||||
1 vulnerability requires semver-major dependency updates.
|
||||
63 vulnerabilities require manual review. See the full report for details.
|
||||
10
client/babel.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
"@vue/cli-plugin-babel/preset",
|
||||
{
|
||||
useBuiltIns: "entry"
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
||||
19
client/build/binding.sln
Normal file
@@ -0,0 +1,19 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 2015
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "binding", "binding.vcxproj", "{2A7051B2-EC6A-582D-A375-8C69A961C66B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Release|x64 = Release|x64
|
||||
Debug|x64 = Debug|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{2A7051B2-EC6A-582D-A375-8C69A961C66B}.Release|x64.ActiveCfg = Release|x64
|
||||
{2A7051B2-EC6A-582D-A375-8C69A961C66B}.Release|x64.Build.0 = Release|x64
|
||||
{2A7051B2-EC6A-582D-A375-8C69A961C66B}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{2A7051B2-EC6A-582D-A375-8C69A961C66B}.Debug|x64.Build.0 = Debug|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
148
client/build/binding.vcxproj
Normal file
@@ -0,0 +1,148 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{2A7051B2-EC6A-582D-A375-8C69A961C66B}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>binding</RootNamespace>
|
||||
<IgnoreWarnCompileDuplicatedFilename>true</IgnoreWarnCompileDuplicatedFilename>
|
||||
<PreferredToolArchitecture>x64</PreferredToolArchitecture>
|
||||
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props"/>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Locals">
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props"/>
|
||||
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.props"/>
|
||||
<ImportGroup Label="ExtensionSettings"/>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props"/>
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros"/>
|
||||
<PropertyGroup>
|
||||
<ExecutablePath>$(ExecutablePath);$(MSBuildProjectDirectory)\..\bin\;$(MSBuildProjectDirectory)\..\bin\</ExecutablePath>
|
||||
<IgnoreImportLibrary>true</IgnoreImportLibrary>
|
||||
<IntDir>$(Configuration)\obj\$(ProjectName)\</IntDir>
|
||||
<LinkIncremental Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</LinkIncremental>
|
||||
<LinkIncremental Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</LinkIncremental>
|
||||
<OutDir>$(SolutionDir)$(Configuration)\</OutDir>
|
||||
<TargetExt Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.node</TargetExt>
|
||||
<TargetExt Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.node</TargetExt>
|
||||
<TargetExt Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.node</TargetExt>
|
||||
<TargetExt Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.node</TargetExt>
|
||||
<TargetName>$(ProjectName)</TargetName>
|
||||
<TargetPath>$(OutDir)\$(ProjectName).node</TargetPath>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\include\node;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\src;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\openssl\config;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\openssl\openssl\include;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\uv\include;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\zlib;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\v8\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalOptions>/Zc:__cplusplus %(AdditionalOptions)</AdditionalOptions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<BufferSecurityCheck>true</BufferSecurityCheck>
|
||||
<DebugInformationFormat>OldStyle</DebugInformationFormat>
|
||||
<DisableSpecificWarnings>4351;4355;4800;4251;4275;4244;4267;%(DisableSpecificWarnings)</DisableSpecificWarnings>
|
||||
<ExceptionHandling>false</ExceptionHandling>
|
||||
<MinimalRebuild>false</MinimalRebuild>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<OmitFramePointers>false</OmitFramePointers>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<PreprocessorDefinitions>NODE_GYP_MODULE_NAME=binding;USING_UV_SHARED=1;USING_V8_SHARED=1;V8_DEPRECATION_WARNINGS=1;V8_DEPRECATION_WARNINGS;V8_IMMINENT_DEPRECATION_WARNINGS;_GLIBCXX_USE_CXX11_ABI=1;WIN32;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE;_HAS_EXCEPTIONS=0;OPENSSL_NO_PINSHARED;OPENSSL_THREADS;BUILDING_NODE_EXTENSION;HOST_BINARY="node.exe";DEBUG;_DEBUG;V8_ENABLE_CHECKS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<StringPooling>true</StringPooling>
|
||||
<SuppressStartupBanner>true</SuppressStartupBanner>
|
||||
<TreatWarningAsError>false</TreatWarningAsError>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
</ClCompile>
|
||||
<Lib>
|
||||
<AdditionalOptions>/LTCG:INCREMENTAL %(AdditionalOptions)</AdditionalOptions>
|
||||
</Lib>
|
||||
<Link>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;DelayImp.lib;"C:\\Users\\cardj\\AppData\\Local\\node-gyp\\Cache\\16.13.1\\x64\\node.lib"</AdditionalDependencies>
|
||||
<AdditionalOptions>/LTCG:INCREMENTAL /ignore:4199 %(AdditionalOptions)</AdditionalOptions>
|
||||
<DelayLoadDLLs>node.exe;%(DelayLoadDLLs)</DelayLoadDLLs>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<OutputFile>$(OutDir)$(ProjectName).node</OutputFile>
|
||||
<SuppressStartupBanner>true</SuppressStartupBanner>
|
||||
<TargetExt>.node</TargetExt>
|
||||
<TargetMachine>MachineX64</TargetMachine>
|
||||
</Link>
|
||||
<ResourceCompile>
|
||||
<AdditionalIncludeDirectories>C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\include\node;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\src;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\openssl\config;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\openssl\openssl\include;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\uv\include;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\zlib;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\v8\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>NODE_GYP_MODULE_NAME=binding;USING_UV_SHARED=1;USING_V8_SHARED=1;V8_DEPRECATION_WARNINGS=1;V8_DEPRECATION_WARNINGS;V8_IMMINENT_DEPRECATION_WARNINGS;_GLIBCXX_USE_CXX11_ABI=1;WIN32;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE;_HAS_EXCEPTIONS=0;OPENSSL_NO_PINSHARED;OPENSSL_THREADS;BUILDING_NODE_EXTENSION;HOST_BINARY="node.exe";DEBUG;_DEBUG;V8_ENABLE_CHECKS;%(PreprocessorDefinitions);%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ResourceCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\include\node;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\src;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\openssl\config;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\openssl\openssl\include;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\uv\include;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\zlib;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\v8\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalOptions>/Zc:__cplusplus %(AdditionalOptions)</AdditionalOptions>
|
||||
<BufferSecurityCheck>true</BufferSecurityCheck>
|
||||
<DebugInformationFormat>OldStyle</DebugInformationFormat>
|
||||
<DisableSpecificWarnings>4351;4355;4800;4251;4275;4244;4267;%(DisableSpecificWarnings)</DisableSpecificWarnings>
|
||||
<ExceptionHandling>false</ExceptionHandling>
|
||||
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<InlineFunctionExpansion>AnySuitable</InlineFunctionExpansion>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<OmitFramePointers>true</OmitFramePointers>
|
||||
<Optimization>Full</Optimization>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<PreprocessorDefinitions>NODE_GYP_MODULE_NAME=binding;USING_UV_SHARED=1;USING_V8_SHARED=1;V8_DEPRECATION_WARNINGS=1;V8_DEPRECATION_WARNINGS;V8_IMMINENT_DEPRECATION_WARNINGS;_GLIBCXX_USE_CXX11_ABI=1;WIN32;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE;_HAS_EXCEPTIONS=0;OPENSSL_NO_PINSHARED;OPENSSL_THREADS;BUILDING_NODE_EXTENSION;HOST_BINARY="node.exe";%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<RuntimeTypeInfo>false</RuntimeTypeInfo>
|
||||
<StringPooling>true</StringPooling>
|
||||
<SuppressStartupBanner>true</SuppressStartupBanner>
|
||||
<TreatWarningAsError>false</TreatWarningAsError>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
</ClCompile>
|
||||
<Lib>
|
||||
<AdditionalOptions>/LTCG:INCREMENTAL %(AdditionalOptions)</AdditionalOptions>
|
||||
</Lib>
|
||||
<Link>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;DelayImp.lib;"C:\\Users\\cardj\\AppData\\Local\\node-gyp\\Cache\\16.13.1\\x64\\node.lib"</AdditionalDependencies>
|
||||
<AdditionalOptions>/LTCG:INCREMENTAL /ignore:4199 %(AdditionalOptions)</AdditionalOptions>
|
||||
<DelayLoadDLLs>node.exe;%(DelayLoadDLLs)</DelayLoadDLLs>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<OutputFile>$(OutDir)$(ProjectName).node</OutputFile>
|
||||
<SuppressStartupBanner>true</SuppressStartupBanner>
|
||||
<TargetExt>.node</TargetExt>
|
||||
<TargetMachine>MachineX64</TargetMachine>
|
||||
</Link>
|
||||
<ResourceCompile>
|
||||
<AdditionalIncludeDirectories>C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\include\node;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\src;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\openssl\config;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\openssl\openssl\include;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\uv\include;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\zlib;C:\Users\cardj\AppData\Local\node-gyp\Cache\16.13.1\deps\v8\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>NODE_GYP_MODULE_NAME=binding;USING_UV_SHARED=1;USING_V8_SHARED=1;V8_DEPRECATION_WARNINGS=1;V8_DEPRECATION_WARNINGS;V8_IMMINENT_DEPRECATION_WARNINGS;_GLIBCXX_USE_CXX11_ABI=1;WIN32;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE;_HAS_EXCEPTIONS=0;OPENSSL_NO_PINSHARED;OPENSSL_THREADS;BUILDING_NODE_EXTENSION;HOST_BINARY="node.exe";%(PreprocessorDefinitions);%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ResourceCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\binding.gyp"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\src\binding.cc">
|
||||
<ObjectFileName>$(IntDir)\src\binding.obj</ObjectFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="C:\Users\cardj\AppData\Roaming\npm\node_modules\npm\node_modules\node-gyp\src\win_delay_load_hook.cc"/>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets"/>
|
||||
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets"/>
|
||||
<ImportGroup Label="ExtensionTargets"/>
|
||||
</Project>
|
||||
58
client/build/binding.vcxproj.filters
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="..">
|
||||
<UniqueIdentifier>{739DB09A-CC57-A953-A6CF-F64FA08E4FA7}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="..\src">
|
||||
<UniqueIdentifier>{8CDEE807-BC53-E450-C8B8-4DEBB66742D4}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="C:">
|
||||
<UniqueIdentifier>{7B735499-E5DD-1C2B-6C26-70023832A1CF}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="C:\Users">
|
||||
<UniqueIdentifier>{E9F714C1-DA89-54E2-60CF-39FEB20BF756}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="C:\Users\cardj">
|
||||
<UniqueIdentifier>{2FED1623-A556-91A0-589D-0214C37A7125}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="C:\Users\cardj\AppData">
|
||||
<UniqueIdentifier>{F852EB63-437C-846A-220F-8D9ED6DAEC1D}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="C:\Users\cardj\AppData\Roaming">
|
||||
<UniqueIdentifier>{D51E5808-912B-5C70-4BB7-475D1DBFA067}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="C:\Users\cardj\AppData\Roaming\npm">
|
||||
<UniqueIdentifier>{741E0E76-39B2-B1AB-9FA1-F1A20B16F295}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="C:\Users\cardj\AppData\Roaming\npm\node_modules">
|
||||
<UniqueIdentifier>{56DF7A98-063D-FB9D-485C-089023B4C16A}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="C:\Users\cardj\AppData\Roaming\npm\node_modules\npm">
|
||||
<UniqueIdentifier>{741E0E76-39B2-B1AB-9FA1-F1A20B16F295}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="C:\Users\cardj\AppData\Roaming\npm\node_modules\npm\node_modules">
|
||||
<UniqueIdentifier>{56DF7A98-063D-FB9D-485C-089023B4C16A}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="C:\Users\cardj\AppData\Roaming\npm\node_modules\npm\node_modules\node-gyp">
|
||||
<UniqueIdentifier>{77348C0E-2034-7791-74D5-63C077DF5A3B}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="C:\Users\cardj\AppData\Roaming\npm\node_modules\npm\node_modules\node-gyp\src">
|
||||
<UniqueIdentifier>{8CDEE807-BC53-E450-C8B8-4DEBB66742D4}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="..">
|
||||
<UniqueIdentifier>{739DB09A-CC57-A953-A6CF-F64FA08E4FA7}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\src\binding.cc">
|
||||
<Filter>..\src</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="C:\Users\cardj\AppData\Roaming\npm\node_modules\npm\node_modules\node-gyp\src\win_delay_load_hook.cc">
|
||||
<Filter>C:\Users\cardj\AppData\Roaming\npm\node_modules\npm\node_modules\node-gyp\src</Filter>
|
||||
</ClCompile>
|
||||
<None Include="..\binding.gyp">
|
||||
<Filter>..</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
360
client/build/config.gypi
Normal file
@@ -0,0 +1,360 @@
|
||||
# Do not edit. File was generated by node-gyp's "configure" step
|
||||
{
|
||||
"target_defaults": {
|
||||
"cflags": [],
|
||||
"default_configuration": "Release",
|
||||
"defines": [],
|
||||
"include_dirs": [],
|
||||
"libraries": [],
|
||||
"msbuild_toolset": "v142",
|
||||
"msvs_windows_target_platform_version": "10.0.19041.0"
|
||||
},
|
||||
"variables": {
|
||||
"asan": 0,
|
||||
"coverage": "false",
|
||||
"dcheck_always_on": 0,
|
||||
"debug_nghttp2": "false",
|
||||
"debug_node": "false",
|
||||
"enable_lto": "false",
|
||||
"enable_pgo_generate": "false",
|
||||
"enable_pgo_use": "false",
|
||||
"error_on_warn": "false",
|
||||
"force_dynamic_crt": 0,
|
||||
"host_arch": "x64",
|
||||
"icu_data_in": "..\\..\\deps\\icu-tmp\\icudt69l.dat",
|
||||
"icu_endianness": "l",
|
||||
"icu_gyp_path": "tools/icu/icu-generic.gyp",
|
||||
"icu_path": "deps/icu-small",
|
||||
"icu_small": "false",
|
||||
"icu_ver_major": "69",
|
||||
"is_debug": 0,
|
||||
"llvm_version": "0.0",
|
||||
"napi_build_version": "8",
|
||||
"nasm_version": "2.15",
|
||||
"node_byteorder": "little",
|
||||
"node_debug_lib": "false",
|
||||
"node_enable_d8": "false",
|
||||
"node_install_npm": "true",
|
||||
"node_library_files": [
|
||||
"lib/assert.js",
|
||||
"lib/async_hooks.js",
|
||||
"lib/buffer.js",
|
||||
"lib/child_process.js",
|
||||
"lib/cluster.js",
|
||||
"lib/console.js",
|
||||
"lib/constants.js",
|
||||
"lib/crypto.js",
|
||||
"lib/dgram.js",
|
||||
"lib/diagnostics_channel.js",
|
||||
"lib/dns.js",
|
||||
"lib/domain.js",
|
||||
"lib/events.js",
|
||||
"lib/fs.js",
|
||||
"lib/http.js",
|
||||
"lib/http2.js",
|
||||
"lib/https.js",
|
||||
"lib/inspector.js",
|
||||
"lib/module.js",
|
||||
"lib/net.js",
|
||||
"lib/os.js",
|
||||
"lib/path.js",
|
||||
"lib/perf_hooks.js",
|
||||
"lib/process.js",
|
||||
"lib/punycode.js",
|
||||
"lib/querystring.js",
|
||||
"lib/readline.js",
|
||||
"lib/repl.js",
|
||||
"lib/stream.js",
|
||||
"lib/string_decoder.js",
|
||||
"lib/sys.js",
|
||||
"lib/timers.js",
|
||||
"lib/tls.js",
|
||||
"lib/trace_events.js",
|
||||
"lib/tty.js",
|
||||
"lib/url.js",
|
||||
"lib/util.js",
|
||||
"lib/v8.js",
|
||||
"lib/vm.js",
|
||||
"lib/wasi.js",
|
||||
"lib/worker_threads.js",
|
||||
"lib/zlib.js",
|
||||
"lib/_http_agent.js",
|
||||
"lib/_http_client.js",
|
||||
"lib/_http_common.js",
|
||||
"lib/_http_incoming.js",
|
||||
"lib/_http_outgoing.js",
|
||||
"lib/_http_server.js",
|
||||
"lib/_stream_duplex.js",
|
||||
"lib/_stream_passthrough.js",
|
||||
"lib/_stream_readable.js",
|
||||
"lib/_stream_transform.js",
|
||||
"lib/_stream_wrap.js",
|
||||
"lib/_stream_writable.js",
|
||||
"lib/_tls_common.js",
|
||||
"lib/_tls_wrap.js",
|
||||
"lib/assert/strict.js",
|
||||
"lib/dns/promises.js",
|
||||
"lib/fs/promises.js",
|
||||
"lib/internal/abort_controller.js",
|
||||
"lib/internal/assert.js",
|
||||
"lib/internal/async_hooks.js",
|
||||
"lib/internal/blob.js",
|
||||
"lib/internal/blocklist.js",
|
||||
"lib/internal/buffer.js",
|
||||
"lib/internal/child_process.js",
|
||||
"lib/internal/cli_table.js",
|
||||
"lib/internal/constants.js",
|
||||
"lib/internal/dgram.js",
|
||||
"lib/internal/dtrace.js",
|
||||
"lib/internal/encoding.js",
|
||||
"lib/internal/errors.js",
|
||||
"lib/internal/error_serdes.js",
|
||||
"lib/internal/event_target.js",
|
||||
"lib/internal/fixed_queue.js",
|
||||
"lib/internal/freelist.js",
|
||||
"lib/internal/freeze_intrinsics.js",
|
||||
"lib/internal/heap_utils.js",
|
||||
"lib/internal/histogram.js",
|
||||
"lib/internal/http.js",
|
||||
"lib/internal/idna.js",
|
||||
"lib/internal/inspector_async_hook.js",
|
||||
"lib/internal/js_stream_socket.js",
|
||||
"lib/internal/linkedlist.js",
|
||||
"lib/internal/net.js",
|
||||
"lib/internal/options.js",
|
||||
"lib/internal/priority_queue.js",
|
||||
"lib/internal/querystring.js",
|
||||
"lib/internal/repl.js",
|
||||
"lib/internal/socketaddress.js",
|
||||
"lib/internal/socket_list.js",
|
||||
"lib/internal/stream_base_commons.js",
|
||||
"lib/internal/timers.js",
|
||||
"lib/internal/trace_events_async_hooks.js",
|
||||
"lib/internal/tty.js",
|
||||
"lib/internal/url.js",
|
||||
"lib/internal/util.js",
|
||||
"lib/internal/v8_prof_polyfill.js",
|
||||
"lib/internal/v8_prof_processor.js",
|
||||
"lib/internal/validators.js",
|
||||
"lib/internal/watchdog.js",
|
||||
"lib/internal/worker.js",
|
||||
"lib/internal/assert/assertion_error.js",
|
||||
"lib/internal/assert/calltracker.js",
|
||||
"lib/internal/bootstrap/environment.js",
|
||||
"lib/internal/bootstrap/loaders.js",
|
||||
"lib/internal/bootstrap/node.js",
|
||||
"lib/internal/bootstrap/pre_execution.js",
|
||||
"lib/internal/bootstrap/switches/does_not_own_process_state.js",
|
||||
"lib/internal/bootstrap/switches/does_own_process_state.js",
|
||||
"lib/internal/bootstrap/switches/is_main_thread.js",
|
||||
"lib/internal/bootstrap/switches/is_not_main_thread.js",
|
||||
"lib/internal/child_process/serialization.js",
|
||||
"lib/internal/cluster/child.js",
|
||||
"lib/internal/cluster/primary.js",
|
||||
"lib/internal/cluster/round_robin_handle.js",
|
||||
"lib/internal/cluster/shared_handle.js",
|
||||
"lib/internal/cluster/utils.js",
|
||||
"lib/internal/cluster/worker.js",
|
||||
"lib/internal/console/constructor.js",
|
||||
"lib/internal/console/global.js",
|
||||
"lib/internal/crypto/aes.js",
|
||||
"lib/internal/crypto/certificate.js",
|
||||
"lib/internal/crypto/cipher.js",
|
||||
"lib/internal/crypto/diffiehellman.js",
|
||||
"lib/internal/crypto/dsa.js",
|
||||
"lib/internal/crypto/ec.js",
|
||||
"lib/internal/crypto/hash.js",
|
||||
"lib/internal/crypto/hashnames.js",
|
||||
"lib/internal/crypto/hkdf.js",
|
||||
"lib/internal/crypto/keygen.js",
|
||||
"lib/internal/crypto/keys.js",
|
||||
"lib/internal/crypto/mac.js",
|
||||
"lib/internal/crypto/pbkdf2.js",
|
||||
"lib/internal/crypto/random.js",
|
||||
"lib/internal/crypto/rsa.js",
|
||||
"lib/internal/crypto/scrypt.js",
|
||||
"lib/internal/crypto/sig.js",
|
||||
"lib/internal/crypto/util.js",
|
||||
"lib/internal/crypto/webcrypto.js",
|
||||
"lib/internal/crypto/x509.js",
|
||||
"lib/internal/debugger/inspect.js",
|
||||
"lib/internal/debugger/inspect_client.js",
|
||||
"lib/internal/debugger/inspect_repl.js",
|
||||
"lib/internal/dns/promises.js",
|
||||
"lib/internal/dns/utils.js",
|
||||
"lib/internal/fs/dir.js",
|
||||
"lib/internal/fs/promises.js",
|
||||
"lib/internal/fs/read_file_context.js",
|
||||
"lib/internal/fs/rimraf.js",
|
||||
"lib/internal/fs/streams.js",
|
||||
"lib/internal/fs/sync_write_stream.js",
|
||||
"lib/internal/fs/utils.js",
|
||||
"lib/internal/fs/watchers.js",
|
||||
"lib/internal/fs/cp/cp-sync.js",
|
||||
"lib/internal/fs/cp/cp.js",
|
||||
"lib/internal/http2/compat.js",
|
||||
"lib/internal/http2/core.js",
|
||||
"lib/internal/http2/util.js",
|
||||
"lib/internal/legacy/processbinding.js",
|
||||
"lib/internal/main/check_syntax.js",
|
||||
"lib/internal/main/eval_stdin.js",
|
||||
"lib/internal/main/eval_string.js",
|
||||
"lib/internal/main/inspect.js",
|
||||
"lib/internal/main/print_help.js",
|
||||
"lib/internal/main/prof_process.js",
|
||||
"lib/internal/main/repl.js",
|
||||
"lib/internal/main/run_main_module.js",
|
||||
"lib/internal/main/worker_thread.js",
|
||||
"lib/internal/modules/package_json_reader.js",
|
||||
"lib/internal/modules/run_main.js",
|
||||
"lib/internal/modules/cjs/helpers.js",
|
||||
"lib/internal/modules/cjs/loader.js",
|
||||
"lib/internal/modules/esm/create_dynamic_module.js",
|
||||
"lib/internal/modules/esm/get_format.js",
|
||||
"lib/internal/modules/esm/get_source.js",
|
||||
"lib/internal/modules/esm/load.js",
|
||||
"lib/internal/modules/esm/loader.js",
|
||||
"lib/internal/modules/esm/module_job.js",
|
||||
"lib/internal/modules/esm/module_map.js",
|
||||
"lib/internal/modules/esm/resolve.js",
|
||||
"lib/internal/modules/esm/translators.js",
|
||||
"lib/internal/perf/event_loop_delay.js",
|
||||
"lib/internal/perf/event_loop_utilization.js",
|
||||
"lib/internal/perf/nodetiming.js",
|
||||
"lib/internal/perf/observe.js",
|
||||
"lib/internal/perf/performance.js",
|
||||
"lib/internal/perf/performance_entry.js",
|
||||
"lib/internal/perf/timerify.js",
|
||||
"lib/internal/perf/usertiming.js",
|
||||
"lib/internal/perf/utils.js",
|
||||
"lib/internal/per_context/domexception.js",
|
||||
"lib/internal/per_context/messageport.js",
|
||||
"lib/internal/per_context/primordials.js",
|
||||
"lib/internal/policy/manifest.js",
|
||||
"lib/internal/policy/sri.js",
|
||||
"lib/internal/process/esm_loader.js",
|
||||
"lib/internal/process/execution.js",
|
||||
"lib/internal/process/per_thread.js",
|
||||
"lib/internal/process/policy.js",
|
||||
"lib/internal/process/promises.js",
|
||||
"lib/internal/process/report.js",
|
||||
"lib/internal/process/signal.js",
|
||||
"lib/internal/process/task_queues.js",
|
||||
"lib/internal/process/warning.js",
|
||||
"lib/internal/process/worker_thread_only.js",
|
||||
"lib/internal/readline/callbacks.js",
|
||||
"lib/internal/readline/emitKeypressEvents.js",
|
||||
"lib/internal/readline/utils.js",
|
||||
"lib/internal/repl/await.js",
|
||||
"lib/internal/repl/history.js",
|
||||
"lib/internal/repl/utils.js",
|
||||
"lib/internal/source_map/prepare_stack_trace.js",
|
||||
"lib/internal/source_map/source_map.js",
|
||||
"lib/internal/source_map/source_map_cache.js",
|
||||
"lib/internal/streams/add-abort-signal.js",
|
||||
"lib/internal/streams/buffer_list.js",
|
||||
"lib/internal/streams/compose.js",
|
||||
"lib/internal/streams/destroy.js",
|
||||
"lib/internal/streams/duplex.js",
|
||||
"lib/internal/streams/duplexify.js",
|
||||
"lib/internal/streams/end-of-stream.js",
|
||||
"lib/internal/streams/from.js",
|
||||
"lib/internal/streams/lazy_transform.js",
|
||||
"lib/internal/streams/legacy.js",
|
||||
"lib/internal/streams/passthrough.js",
|
||||
"lib/internal/streams/pipeline.js",
|
||||
"lib/internal/streams/readable.js",
|
||||
"lib/internal/streams/state.js",
|
||||
"lib/internal/streams/transform.js",
|
||||
"lib/internal/streams/utils.js",
|
||||
"lib/internal/streams/writable.js",
|
||||
"lib/internal/test/binding.js",
|
||||
"lib/internal/test/transfer.js",
|
||||
"lib/internal/tls/parse-cert-string.js",
|
||||
"lib/internal/tls/secure-context.js",
|
||||
"lib/internal/tls/secure-pair.js",
|
||||
"lib/internal/util/comparisons.js",
|
||||
"lib/internal/util/debuglog.js",
|
||||
"lib/internal/util/inspect.js",
|
||||
"lib/internal/util/inspector.js",
|
||||
"lib/internal/util/iterable_weak_map.js",
|
||||
"lib/internal/util/types.js",
|
||||
"lib/internal/vm/module.js",
|
||||
"lib/internal/webstreams/encoding.js",
|
||||
"lib/internal/webstreams/queuingstrategies.js",
|
||||
"lib/internal/webstreams/readablestream.js",
|
||||
"lib/internal/webstreams/transfer.js",
|
||||
"lib/internal/webstreams/transformstream.js",
|
||||
"lib/internal/webstreams/util.js",
|
||||
"lib/internal/webstreams/writablestream.js",
|
||||
"lib/internal/worker/io.js",
|
||||
"lib/internal/worker/js_transferable.js",
|
||||
"lib/path/posix.js",
|
||||
"lib/path/win32.js",
|
||||
"lib/stream/consumers.js",
|
||||
"lib/stream/promises.js",
|
||||
"lib/stream/web.js",
|
||||
"lib/timers/promises.js",
|
||||
"lib/util/types.js"
|
||||
],
|
||||
"node_module_version": 93,
|
||||
"node_no_browser_globals": "false",
|
||||
"node_prefix": "/usr/local",
|
||||
"node_release_urlbase": "https://nodejs.org/download/release/",
|
||||
"node_shared": "false",
|
||||
"node_shared_brotli": "false",
|
||||
"node_shared_cares": "false",
|
||||
"node_shared_http_parser": "false",
|
||||
"node_shared_libuv": "false",
|
||||
"node_shared_nghttp2": "false",
|
||||
"node_shared_nghttp3": "false",
|
||||
"node_shared_ngtcp2": "false",
|
||||
"node_shared_openssl": "false",
|
||||
"node_shared_zlib": "false",
|
||||
"node_tag": "",
|
||||
"node_target_type": "executable",
|
||||
"node_use_bundled_v8": "true",
|
||||
"node_use_dtrace": "false",
|
||||
"node_use_etw": "true",
|
||||
"node_use_node_code_cache": "true",
|
||||
"node_use_node_snapshot": "true",
|
||||
"node_use_openssl": "true",
|
||||
"node_use_v8_platform": "true",
|
||||
"node_with_ltcg": "true",
|
||||
"node_without_node_options": "false",
|
||||
"openssl_fips": "",
|
||||
"openssl_is_fips": "false",
|
||||
"openssl_quic": "true",
|
||||
"ossfuzz": "false",
|
||||
"shlib_suffix": "so.93",
|
||||
"target_arch": "x64",
|
||||
"v8_enable_31bit_smis_on_64bit_arch": 0,
|
||||
"v8_enable_gdbjit": 0,
|
||||
"v8_enable_i18n_support": 1,
|
||||
"v8_enable_inspector": 1,
|
||||
"v8_enable_lite_mode": 0,
|
||||
"v8_enable_object_print": 1,
|
||||
"v8_enable_pointer_compression": 0,
|
||||
"v8_enable_webassembly": 1,
|
||||
"v8_no_strict_aliasing": 1,
|
||||
"v8_optimized_debug": 1,
|
||||
"v8_promise_internal_field_count": 1,
|
||||
"v8_random_seed": 0,
|
||||
"v8_trace_maps": 0,
|
||||
"v8_use_siphash": 1,
|
||||
"want_separate_host_toolset": 0,
|
||||
"nodedir": "C:\\Users\\cardj\\AppData\\Local\\node-gyp\\Cache\\16.13.1",
|
||||
"standalone_static_library": 1,
|
||||
"msbuild_path": "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\BuildTools\\MSBuild\\Current\\Bin\\MSBuild.exe",
|
||||
"cache": "C:\\Users\\cardj\\AppData\\Local\\npm-cache",
|
||||
"globalconfig": "C:\\Users\\cardj\\AppData\\Roaming\\npm\\etc\\npmrc",
|
||||
"global_prefix": "C:\\Users\\cardj\\AppData\\Roaming\\npm",
|
||||
"init_module": "C:\\Users\\cardj\\.npm-init.js",
|
||||
"local_prefix": "C:\\data\\code\\sockeye\\client",
|
||||
"metrics_registry": "https://registry.npmjs.org/",
|
||||
"node_gyp": "C:\\Users\\cardj\\AppData\\Roaming\\npm\\node_modules\\npm\\node_modules\\node-gyp\\bin\\node-gyp.js",
|
||||
"prefix": "C:\\Users\\cardj\\AppData\\Roaming\\npm",
|
||||
"userconfig": "C:\\Users\\cardj\\.npmrc",
|
||||
"user_agent": "npm/8.3.0 node/v16.13.1 win32 x64 workspaces/false"
|
||||
}
|
||||
}
|
||||
9
client/cypress.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"pluginsFile": "tests/e2e/plugins/index.js",
|
||||
"baseUrl": "http://localhost:7676",
|
||||
"defaultCommandTimeout":10000,
|
||||
"env": {
|
||||
"adminusername": "john",
|
||||
"adminpassword": "abraxis"
|
||||
}
|
||||
}
|
||||
1
client/devdocs/todo.txt
Normal file
@@ -0,0 +1 @@
|
||||
test new svn
|
||||
30380
client/package-lock.json
generated
Normal file
89
client/package.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"name": "sockeye",
|
||||
"version": "8.0.28",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"myLint": "npm run lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.7",
|
||||
"chart.js": "^2.9.4",
|
||||
"chartjs-adapter-luxon": "^0.2.2",
|
||||
"core-js": "^3.23.1",
|
||||
"dompurify": "^2.3.8",
|
||||
"fontsource-roboto": "^3.1.5",
|
||||
"github-markdown-css": "^4.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"luxon": "^1.28.0",
|
||||
"marked": "^1.2.9",
|
||||
"monaco-editor": "^0.30.1",
|
||||
"monaco-editor-webpack-plugin": "^6.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"papaparse": "^5.3.2",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"vue": "^2.6.14",
|
||||
"vue-chartjs": "^3.5.1",
|
||||
"vue-currency-input": "1.22.3",
|
||||
"vue-router": "^3.5.4",
|
||||
"vue-signature": "^2.5.5",
|
||||
"vuetify": "^2.6.6",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-persistedstate": "^2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.5",
|
||||
"@vue/cli-plugin-babel": "^4.5.15",
|
||||
"@vue/cli-plugin-eslint": "^4.5.15",
|
||||
"@vue/cli-plugin-pwa": "^4.5.15",
|
||||
"@vue/cli-plugin-router": "^4.5.15",
|
||||
"@vue/cli-plugin-vuex": "^4.5.15",
|
||||
"@vue/cli-service": "^4.5.15",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"@vue/test-utils": "^1.3.0",
|
||||
"babel-core": "6.26.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"fibers": "^4.0.3",
|
||||
"prettier": "^1.19.1",
|
||||
"sass": "^1.52.3",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue-cli-plugin-vuetify": "^2.5.0",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vuetify-loader": "^1.7.3",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^3.3.12"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/recommended",
|
||||
"eslint:recommended",
|
||||
"@vue/prettier"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
9
client/public/browserconfig.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png?v=82a"/>
|
||||
<TileColor>#ffc40d</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
BIN
client/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 110 KiB |
18
client/public/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0"
|
||||
/>
|
||||
|
||||
<title>Sockeye</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
Sockeye requires JavaScript
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
8
client/public/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Sockeye",
|
||||
"short_name": "Sockeye",
|
||||
"start_url": "/",
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
2
client/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
419
client/src/App.vue
Normal file
@@ -0,0 +1,419 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<gznotify ref="gznotify"></gznotify>
|
||||
<gzconfirm ref="gzconfirm"></gzconfirm>
|
||||
<!-- Width of nav drawer set to allow widest translated text menu item to show which is spanish client service requests item
|
||||
and also leave a tiny space to click on outside of nav for galaxy 9 phone (narrowest width supported device)
|
||||
|
||||
Also there is a height bug in vuetify with chrome on mobile (and safari) so using workaround with 100% height set on v-navigation-drawer
|
||||
https://github.com/vuetifyjs/vuetify/issues/9607
|
||||
|
||||
-->
|
||||
|
||||
<v-navigation-drawer
|
||||
v-if="isAuthenticated"
|
||||
v-model="drawer"
|
||||
app
|
||||
temporary
|
||||
width="345"
|
||||
height="100%"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<div class="subtitle-2 primary--text pt-2 pl-4">
|
||||
{{ $store.state.userName }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<template v-for="item in navItems">
|
||||
<!-- TOP LEVEL can be holders or actions -->
|
||||
|
||||
<!-- TOP LEVEL HOLDER -->
|
||||
<template v-if="!item.route">
|
||||
<v-list-group
|
||||
:key="item.key"
|
||||
:prepend-icon="item.icon"
|
||||
:value="false"
|
||||
:data-cy="item.testid"
|
||||
>
|
||||
<template v-slot:activator>
|
||||
<!--group activator -->
|
||||
<v-list-item-title>{{ $sock.t(item.title) }}</v-list-item-title>
|
||||
</template>
|
||||
|
||||
<!-- TOP LEVEL HOLDER SUBITEMS -->
|
||||
<template v-for="subitem in item.navItems">
|
||||
<template v-if="!subitem.route">
|
||||
<!-- SECOND LEVEL HOLDER -->
|
||||
<div :key="subitem.key" class="pl-2">
|
||||
<v-list-group
|
||||
:key="subitem.key"
|
||||
no-action
|
||||
sub-group
|
||||
:value="false"
|
||||
>
|
||||
<!-- Second level activator -->
|
||||
<template v-slot:activator>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{
|
||||
$sock.t(subitem.title)
|
||||
}}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
|
||||
<v-list-item
|
||||
v-for="subsub in subitem.navItems"
|
||||
:key="subsub.key"
|
||||
:to="subsub.route"
|
||||
>
|
||||
<v-list-item-action>
|
||||
<v-icon
|
||||
v-if="subsub.icon"
|
||||
:color="item.color ? item.color : ''"
|
||||
>{{ subsub.icon }}</v-icon
|
||||
>
|
||||
</v-list-item-action>
|
||||
<v-list-item-title
|
||||
:v-text="$sock.t(subsub.title)"
|
||||
></v-list-item-title>
|
||||
</v-list-item>
|
||||
<!-- was end of v-list-group here -->
|
||||
</v-list-group>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- SECOND LEVEL ACTION -->
|
||||
<div :key="subitem.key" class="pl-3">
|
||||
<v-list-item
|
||||
:to="subitem.route"
|
||||
:data-cy="'nav' + subitem.testid"
|
||||
>
|
||||
<v-list-item-action>
|
||||
<v-icon
|
||||
v-if="subitem.icon"
|
||||
:color="item.color ? item.color : ''"
|
||||
>{{ subitem.icon }}</v-icon
|
||||
>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{
|
||||
$sock.t(subitem.title)
|
||||
}}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</v-list-group>
|
||||
<!-- END OF TOP LEVEL HOLDER -->
|
||||
</template>
|
||||
|
||||
<!-- TOP LEVEL ACTION -->
|
||||
<template v-else>
|
||||
<div :key="item.key">
|
||||
<v-list-item :to="item.route" :data-cy="item.testid">
|
||||
<v-list-item-action v-if="item.icon">
|
||||
<v-icon :color="item.color ? item.color : ''">{{
|
||||
item.icon
|
||||
}}</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{
|
||||
$sock.t(item.title)
|
||||
}}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- end of entire list -->
|
||||
</template>
|
||||
</v-list>
|
||||
<template v-slot:append>
|
||||
<div>
|
||||
<v-btn x-large block to="/login" data-cy="logout">
|
||||
<v-icon large left>$sockiSignOut</v-icon>
|
||||
<span class="ml-2 text-h6">{{ $sock.t("Logout") }}</span></v-btn
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
<v-app-bar v-if="isAuthenticated" :color="appBar.color" dark fixed app>
|
||||
<v-app-bar-nav-icon
|
||||
data-cy="navicon"
|
||||
@click.stop="drawer = !drawer"
|
||||
></v-app-bar-nav-icon>
|
||||
<v-toolbar-title class="ml-n5 ml-sm-0 pl-sm-4">
|
||||
<v-icon>{{ appBar.icon }}</v-icon>
|
||||
<span class="text-subtitle-2 ml-2 text-sm-h6 ml-sm-4">{{
|
||||
titleDisplay
|
||||
}}</span>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<!-- All users can see this, they may not be able to subscribe to notifications but they may see direct or system notifications for any account so this is always available -->
|
||||
<template>
|
||||
<v-btn text icon to="/home-notifications" data-cy="notification">
|
||||
<v-badge color="deep-purple" :value="newNotificationCount() > 0">
|
||||
<template v-slot:badge>
|
||||
{{ newNotificationCount() }}
|
||||
</template>
|
||||
<v-icon>$sockiBell</v-icon>
|
||||
</v-badge>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-toolbar-items>
|
||||
<template v-for="item in appBar.menuItems">
|
||||
<v-btn
|
||||
v-if="item.surface"
|
||||
:key="item.key"
|
||||
class="hidden-xs-only"
|
||||
icon
|
||||
:disabled="item.disabled"
|
||||
:data-cy="item.key"
|
||||
@click="clickMenuItem(item)"
|
||||
>
|
||||
<v-icon :color="item.color ? item.color : ''">
|
||||
{{ item.icon }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-menu float-left>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn text icon data-cy="contextmenu" v-on="on">
|
||||
<v-icon>$sockiEllipsisV</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<!-- https://stackoverflow.com/questions/54904746/scrolling-list-in-vuetify -->
|
||||
<v-list style="max-height: 100vh" class="overflow-y-auto">
|
||||
<template v-for="(item, index) in appBar.menuItems">
|
||||
<v-subheader v-if="item.header" :key="index">
|
||||
{{ item.header }}
|
||||
</v-subheader>
|
||||
<v-divider
|
||||
v-else-if="item.divider"
|
||||
:key="index"
|
||||
:inset="item.inset"
|
||||
></v-divider>
|
||||
<v-list-item
|
||||
v-else
|
||||
:key="item.key"
|
||||
:disabled="item.disabled"
|
||||
:href="item.href"
|
||||
:target="item.target"
|
||||
:class="{ 'hidden-sm-and-up': item.surface }"
|
||||
:data-cy="item.key"
|
||||
@click="clickMenuItem(item)"
|
||||
>
|
||||
<v-list-item-action>
|
||||
<v-icon
|
||||
v-if="item.icon"
|
||||
:color="item.color ? item.color : ''"
|
||||
>{{ item.icon }}</v-icon
|
||||
>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
<span v-if="item.notrans">{{ item.title }}</span>
|
||||
<span v-else>{{ $sock.t(item.title) }}</span>
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-toolbar-items>
|
||||
</v-app-bar>
|
||||
<v-main>
|
||||
<v-container fluid class="my-8">
|
||||
<transition name="fade" mode="out-in" @after-leave="afterLeave">
|
||||
<router-view :key="$route.fullPath" class="view"></router-view>
|
||||
</transition>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
<script>
|
||||
import gzconfirm from "./components/gzconfirm";
|
||||
import gznotify from "./components/gznotify";
|
||||
import openObjectHandler from "./api/open-object-handler";
|
||||
import notifyPoll from "./api/notifypoll";
|
||||
import { processLogout } from "./api/authutil";
|
||||
export default {
|
||||
components: {
|
||||
gzconfirm,
|
||||
gznotify
|
||||
},
|
||||
props: {
|
||||
source: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drawer: null,
|
||||
appBar: {
|
||||
isMain: true,
|
||||
icon: "",
|
||||
title: "",
|
||||
helpUrl: "user-intro",
|
||||
menuItems: []
|
||||
},
|
||||
//pwa update
|
||||
//https://medium.com/@dougallrich/give-users-control-over-app-updates-in-vue-cli-3-pwas-20453aedc1f2
|
||||
refreshing: false,
|
||||
registration: null,
|
||||
updateExists: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isAuthenticated() {
|
||||
return this.$store.state.authenticated === true;
|
||||
},
|
||||
navItems() {
|
||||
return this.$store.state.navItems;
|
||||
},
|
||||
// helpUrl() {
|
||||
// return window.$gz.api.helpUrl();
|
||||
// },
|
||||
titleDisplay() {
|
||||
if (this.appBar.title == null || this.appBar.title == "") {
|
||||
return null;
|
||||
}
|
||||
return this.appBar.title;
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
//Don't call this unless there is a service worker (https or localhost only, not private network) or else vuetify goes snakey and things start breaking
|
||||
if (navigator.serviceWorker) {
|
||||
//pwa update
|
||||
//https://medium.com/@dougallrich/give-users-control-over-app-updates-in-vue-cli-3-pwas-20453aedc1f2
|
||||
document.addEventListener("swUpdated", await this.showRefreshUI, {
|
||||
once: true
|
||||
});
|
||||
|
||||
//for the record, this is the breaking code if there is no serviceworker
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
if (this.refreshing) return;
|
||||
this.refreshing = true;
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// WIRE UP
|
||||
// EVENT HANDLERS ON GZEVENTBUS
|
||||
//
|
||||
//
|
||||
window.$gz.menu.wireUpEventHandlers(this);
|
||||
window.$gz.dialog.wireUpEventHandlers(this);
|
||||
openObjectHandler.wireUpEventHandlers(this);
|
||||
window.$gz.translation.setVuetifyDefaultLanguageElements(this);
|
||||
},
|
||||
beforeDestroy() {
|
||||
//UNWIRE ALL EVENT HANDLERS FROM GZEVENTBUS
|
||||
window.$gz.eventBus.$off();
|
||||
},
|
||||
mounted() {
|
||||
const vm = this;
|
||||
|
||||
vm.$vuetify.theme.dark = vm.$store.state.darkMode;
|
||||
vm.$root.$gzconfirm = vm.$refs.gzconfirm.open;
|
||||
vm.$root.$gznotify = vm.$refs.gznotify.addNotification;
|
||||
vm.$root.$gz = window.$gz;
|
||||
|
||||
//direct open path?
|
||||
let toPath = window.location.pathname;
|
||||
|
||||
if (toPath != undefined && toPath.length > 4 && !toPath.includes("login")) {
|
||||
//must be at least 4 to be a valid Sockeye url
|
||||
window.$gz.store.commit(
|
||||
"logItem",
|
||||
`App::Mounted - preset path presented: ${toPath}`
|
||||
);
|
||||
} else {
|
||||
toPath = undefined;
|
||||
}
|
||||
|
||||
const isReset = toPath && toPath.includes("home-reset");
|
||||
if (isReset && vm.$store.state.authenticated) {
|
||||
processLogout();
|
||||
}
|
||||
|
||||
//redirect to login if not authenticated
|
||||
if (!vm.$store.state.authenticated && !isReset) {
|
||||
//If a direct open path was being used but user is not logged in this will catch it
|
||||
//otherwise they will just go on to that path directly
|
||||
|
||||
if (toPath != undefined) {
|
||||
vm.$router.push({
|
||||
name: "login",
|
||||
params: {
|
||||
topath: window.location.pathname,
|
||||
search: window.location.search
|
||||
}
|
||||
});
|
||||
} else {
|
||||
vm.$router.push({
|
||||
name: "login",
|
||||
params: {
|
||||
search: window.location.search
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
//RELOAD / REFRESH HANDLING
|
||||
//Restart notification polling due to refresh?
|
||||
if (window.$gz.store.state.authenticated) {
|
||||
notifyPoll.startPolling();
|
||||
}
|
||||
|
||||
//FUTURE: If need to detect a reload, this works reliably
|
||||
//OK if here then is this a reliable way to detect a reload or refresh or re-open of the app from a closed window but still authenticated?
|
||||
},
|
||||
methods: {
|
||||
afterLeave() {
|
||||
this.$root.$emit("triggerScroll");
|
||||
},
|
||||
clickMenuItem(item) {
|
||||
window.$gz.eventBus.$emit("menu-click", item);
|
||||
},
|
||||
newNotificationCount() {
|
||||
return this.$store.state.newNotificationCount;
|
||||
},
|
||||
test() {
|
||||
alert("App.vue::test() method");
|
||||
},
|
||||
//PWA update
|
||||
async showRefreshUI(e) {
|
||||
this.registration = e.detail;
|
||||
this.updateExists = true;
|
||||
|
||||
//Ok, if not logged in just force the update immediately
|
||||
if (!this.isAuthenticated) {
|
||||
this.refreshApp();
|
||||
return;
|
||||
}
|
||||
|
||||
//User is logged in offer to update in a dialog with translated text
|
||||
const dialogResult = await window.$gz.dialog.confirmGeneric(
|
||||
"UpdateAvailable"
|
||||
);
|
||||
if (dialogResult == false) {
|
||||
return;
|
||||
}
|
||||
this.refreshApp();
|
||||
},
|
||||
refreshApp() {
|
||||
this.updateExists = false;
|
||||
if (!this.registration || !this.registration.waiting) {
|
||||
return;
|
||||
}
|
||||
this.registration.waiting.postMessage("skipWaiting");
|
||||
//todo clear stale data here?
|
||||
//maybe needs better control, i.e. conditionally clear only if needs to and then force re-login after
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
184
client/src/api/authorizationroles.js
Normal file
@@ -0,0 +1,184 @@
|
||||
import bizrolerights from "./biz-role-rights";
|
||||
|
||||
export default {
|
||||
ROLE_RIGHTS: bizrolerights,
|
||||
AUTHORIZATION_ROLES: {
|
||||
///<summary>No role set</summary>
|
||||
NoRole: 0,
|
||||
///<summary>BizAdminRestricted</summary>
|
||||
BizAdminRestricted: 1,
|
||||
///<summary>BizAdmin</summary>
|
||||
BizAdmin: 2,
|
||||
///<summary>ServiceRestricted</summary>
|
||||
ServiceRestricted: 4,
|
||||
///<summary>Service</summary>
|
||||
Service: 8,
|
||||
///<summary>InventoryRestricted</summary>
|
||||
InventoryRestricted: 16,
|
||||
///<summary>Inventory</summary>
|
||||
Inventory: 32,
|
||||
///<summary>Accounting</summary>
|
||||
Accounting: 64, //No restricted role, not sure if there is a need
|
||||
///<summary>TechRestricted</summary>
|
||||
TechRestricted: 128,
|
||||
///<summary>Tech</summary>
|
||||
Tech: 256,
|
||||
///<summary>SubContractorRestricted</summary>
|
||||
SubContractorRestricted: 512,
|
||||
///<summary>SubContractor</summary>
|
||||
SubContractor: 1024,
|
||||
///<summary>CustomerRestricted</summary>
|
||||
CustomerRestricted: 2048,
|
||||
///<summary>Customer</summary>
|
||||
Customer: 4096,
|
||||
///<summary>OpsAdminRestricted</summary>
|
||||
OpsAdminRestricted: 8192,
|
||||
///<summary>OpsAdmin</summary>
|
||||
OpsAdmin: 16384,
|
||||
///<summary>Sales</summary>
|
||||
Sales: 32768,
|
||||
///<summary>SalesRestricted</summary>
|
||||
SalesRestricted: 65536
|
||||
},
|
||||
//////////////////////////////////////////////////////////
|
||||
// Does current logged in user have role?
|
||||
// (Can be an array of roles or a single role, if array returns true if any of the array roles are present for this user)
|
||||
//
|
||||
hasRole(desiredRole) {
|
||||
if (!window.$gz.store.state.roles || window.$gz.store.state.roles === 0) {
|
||||
return false;
|
||||
}
|
||||
//array form?
|
||||
|
||||
if (Array.isArray(desiredRole)) {
|
||||
//it's an array of roles, iterate and if any are present then return true
|
||||
for (let i = 0; i < desiredRole.length; i++) {
|
||||
if ((window.$gz.store.state.roles & desiredRole[i]) != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return (window.$gz.store.state.roles & desiredRole) != 0;
|
||||
}
|
||||
},
|
||||
//////////////////////////////////////////////////////////
|
||||
// Does current logged in user have *ANY* role?
|
||||
//
|
||||
//
|
||||
hasAnyRole() {
|
||||
if (!window.$gz.store.state.roles || window.$gz.store.state.roles === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Get a default empty rights object so that it can be present when a
|
||||
// form first loads
|
||||
//
|
||||
defaultRightsObject() {
|
||||
return {
|
||||
change: false,
|
||||
read: false,
|
||||
delete: false
|
||||
};
|
||||
},
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Get a default FULL rights object for forms that don't really need
|
||||
// to check rights but fits into system for forms in place (e.g. change password)
|
||||
//
|
||||
fullRightsObject() {
|
||||
return {
|
||||
change: true,
|
||||
read: true,
|
||||
delete: true
|
||||
};
|
||||
},
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Get a read only rights object (customer workorder for example)
|
||||
//
|
||||
readOnlyRightsObject() {
|
||||
return {
|
||||
change: false,
|
||||
read: true,
|
||||
delete: false
|
||||
};
|
||||
},
|
||||
/////////////////////////////////
|
||||
// aType is the name of the object type as defined in socktype.js
|
||||
//
|
||||
getRights(aType) {
|
||||
//from bizroles.cs:
|
||||
//HOW THIS WORKS / WHATS EXPECTED
|
||||
//Change = CREATE, RETRIEVE, UPDATE, DELETE - Full rights
|
||||
//
|
||||
//ReadFullRecord = You can read *all* the fields of the record, but can't modify it. Change is automatically checked for so only add different roles from change
|
||||
//PICKLIST NOTE: this does not control getting a list of names for selection which is role independent because it's required for so much indirectly
|
||||
//DELETE = SAME AS CHANGE FOR NOW (There is no specific delete right for now though it's checked for by routes in Authorized.cs in case we want to add it in future as a separate right from create.)
|
||||
//NOTE: biz rules can supersede this, this is just for general rights purposes, if an object has restrictive business rules they will take precedence every time.
|
||||
|
||||
const ret = this.defaultRightsObject();
|
||||
|
||||
//Get the type name from the type enum value
|
||||
let typeName = undefined;
|
||||
for (const [key, value] of Object.entries(window.$gz.type)) {
|
||||
if (value == aType) {
|
||||
typeName = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//Get the Sockeye stock REQUIRED role rights for that object
|
||||
const objectRoleRights = this.ROLE_RIGHTS[typeName];
|
||||
if (!objectRoleRights) {
|
||||
throw new Error(
|
||||
`authorizationroles::getRights type ${aType} not found in roles collection`
|
||||
);
|
||||
}
|
||||
|
||||
//get the logged in user's role
|
||||
const userRole = window.$gz.store.state.roles;
|
||||
//calculate the effective rights
|
||||
//a non zero result of the bitwise calculation means true and zero means false so using !! to force it into a boolean value
|
||||
//(contrary to some style guides that say !! is obscure but I say it saves a lot of typing)
|
||||
|
||||
const canChange = !!(userRole & objectRoleRights.Change);
|
||||
|
||||
//sometimes rights to read are false if change is true since change trumps read anyway so accordingly:
|
||||
let canReadFullRecord = canChange;
|
||||
if (!canReadFullRecord) {
|
||||
//can't change but might have special rights to full record:
|
||||
canReadFullRecord = !!(userRole & objectRoleRights.ReadFullRecord);
|
||||
}
|
||||
|
||||
ret.change = canChange;
|
||||
ret.delete = ret.change; //FOR NOW
|
||||
ret.read = canReadFullRecord;
|
||||
|
||||
// console.log("authorizationroles::canOpen", {
|
||||
// typeName: typeName,
|
||||
// userRole: userRole,
|
||||
// objectRoleRights: objectRoleRights,
|
||||
// retResultIs: ret
|
||||
// });
|
||||
|
||||
return ret;
|
||||
},
|
||||
/////////////////////////////////
|
||||
// convenience method for forms that deal with multiple object types
|
||||
// (i.e. grids, history etc, initialization of main menu etc)
|
||||
//
|
||||
canOpen(aType) {
|
||||
const r = this.getRights(aType);
|
||||
//convention is change might be defined but not read so canOpen is true eitehr way
|
||||
return r.change == true || r.read == true;
|
||||
},
|
||||
/////////////////////////////////
|
||||
// convenience method for forms that deal with multiple object types
|
||||
// (i.e. grids, history etc, initialization of main menu etc)
|
||||
//
|
||||
canChange(aType) {
|
||||
const r = this.getRights(aType);
|
||||
return r.change == true;
|
||||
}
|
||||
};
|
||||
124
client/src/api/authutil.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import jwt_decode from "jwt-decode";
|
||||
import initialize from "./initialize";
|
||||
import notifypoll from "./notifypoll";
|
||||
|
||||
export function processLogin(authResponse, loggedInWithKnownPassword) {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async function(resolve, reject) {
|
||||
try {
|
||||
//check there is a response of some kind
|
||||
if (!authResponse) {
|
||||
window.$gz.store.commit("logItem", "auth::processLogin -> no response");
|
||||
return reject();
|
||||
}
|
||||
|
||||
//is token present?
|
||||
if (!authResponse || !authResponse.token) {
|
||||
window.$gz.store.commit(
|
||||
"logItem",
|
||||
"auth::processLogin -> response contains no data"
|
||||
);
|
||||
return reject();
|
||||
}
|
||||
const token = jwt_decode(authResponse.token);
|
||||
|
||||
if (!token || !token.iss) {
|
||||
window.$gz.store.commit(
|
||||
"logItem",
|
||||
"auth::processLogin -> response token empty"
|
||||
);
|
||||
return reject();
|
||||
}
|
||||
|
||||
if (token.iss != "rockfish.ayanova.com") {
|
||||
window.$gz.store.commit(
|
||||
"logItem",
|
||||
"auth::processLogin -> token invalid (iss): " + token.iss
|
||||
);
|
||||
return reject();
|
||||
}
|
||||
|
||||
//ensure the store is clean first in case we didn't come here from a clean logout
|
||||
window.$gz.store.commit("logout");
|
||||
sessionStorage.clear(); //clear all temporary session storage data
|
||||
|
||||
//encourage password changing if a purchased license
|
||||
if (loggedInWithKnownPassword)
|
||||
window.$gz.store.commit("setKnownPassword", true);
|
||||
|
||||
//Put app relevant items into vuex store so app can use them
|
||||
window.$gz.store.commit("login", {
|
||||
apiToken: authResponse.token,
|
||||
authenticated: true,
|
||||
userId: Number(token.id),
|
||||
translationId: authResponse.tid,
|
||||
userName: authResponse.name,
|
||||
roles: authResponse.roles,
|
||||
userType: authResponse.usertype,
|
||||
dlt: authResponse.dlt,
|
||||
l: authResponse.l,
|
||||
tfaEnabled: authResponse.tfa,
|
||||
customerRights: authResponse.customerRights
|
||||
});
|
||||
|
||||
//decided to remove this as it is not an out of the ordinary scenario to log
|
||||
// however left this block here in case in future becomes necessary for some common issue
|
||||
// //log the login
|
||||
// window.$gz.store.commit(
|
||||
// "logItem",
|
||||
// "auth::processLogin -> User " + token.id + " logged in"
|
||||
// );
|
||||
|
||||
//Get global settings
|
||||
const gsets = await window.$gz.api.get("global-biz-setting/client");
|
||||
if (gsets.error) {
|
||||
//In a form this would trigger a bunch of validation or error display code but for here and now:
|
||||
//convert error to human readable string for display and popup a notification to user
|
||||
const msg = window.$gz.api.apiErrorToHumanString(gsets.error);
|
||||
window.$gz.eventBus.$emit("notify-error", msg);
|
||||
} else {
|
||||
//Check if overrides and use them here
|
||||
//or else use browser defaults
|
||||
window.$gz.store.commit("setGlobalSettings", gsets.data);
|
||||
}
|
||||
await initialize();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
//start notification polling
|
||||
notifypoll.startPolling();
|
||||
resolve();
|
||||
//-------------------------------------------------
|
||||
});
|
||||
}
|
||||
|
||||
export function processLogout() {
|
||||
notifypoll.stopPolling();
|
||||
window.$gz.store.commit("logout");
|
||||
sessionStorage.clear(); //clear all temporary session storage data
|
||||
}
|
||||
|
||||
export function isLoggedIn() {
|
||||
return (
|
||||
!!window.$gz.store.state.apiToken &&
|
||||
!isTokenExpired(window.$gz.store.state.apiToken)
|
||||
);
|
||||
}
|
||||
|
||||
function getTokenExpirationDate(encodedToken) {
|
||||
const token = jwt_decode(encodedToken);
|
||||
if (!token.exp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = new Date(0);
|
||||
date.setUTCSeconds(token.exp);
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
function isTokenExpired(token) {
|
||||
const expirationDate = getTokenExpirationDate(token);
|
||||
return expirationDate < new Date();
|
||||
}
|
||||
129
client/src/api/biz-role-rights.js
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
*
|
||||
* Auto generated by BizRoles.cs in server project, update here whenever that changes
|
||||
*
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
Customer: { Change: 32842, ReadFullRecord: 65797, Select: 131071 },
|
||||
CustomerNote: { Change: 32842, ReadFullRecord: 65797, Select: 131071 },
|
||||
CustomerNotifySubscription: {
|
||||
Change: 10,
|
||||
ReadFullRecord: 65797,
|
||||
Select: 131071
|
||||
},
|
||||
Contract: { Change: 74, ReadFullRecord: 98565, Select: 131071 },
|
||||
HeadOffice: { Change: 32842, ReadFullRecord: 65797, Select: 131071 },
|
||||
LoanUnit: { Change: 74, ReadFullRecord: 98565, Select: 131071 },
|
||||
Part: { Change: 98, ReadFullRecord: 29, Select: 131071 },
|
||||
PartInventory: { Change: 98, ReadFullRecord: 29, Select: 131071 },
|
||||
PartWarehouse: { Change: 98, ReadFullRecord: 29, Select: 131071 },
|
||||
PartAssembly: { Change: 98, ReadFullRecord: 29, Select: 131071 },
|
||||
PurchaseOrder: { Change: 98, ReadFullRecord: 29, Select: 131071 },
|
||||
PartInventoryRequest: { Change: 98, ReadFullRecord: 29, Select: 131071 },
|
||||
PartInventoryRestock: { Change: 98, ReadFullRecord: 29, Select: 131071 },
|
||||
PartInventoryDataList: { Change: 98, ReadFullRecord: 29, Select: 131071 },
|
||||
PartInventoryRequestDataList: {
|
||||
Change: 98,
|
||||
ReadFullRecord: 29,
|
||||
Select: 131071
|
||||
},
|
||||
Project: { Change: 74, ReadFullRecord: 98565, Select: 131071 },
|
||||
ServiceRate: { Change: 74, ReadFullRecord: 33037, Select: 131071 },
|
||||
TravelRate: { Change: 74, ReadFullRecord: 33037, Select: 131071 },
|
||||
TaxCode: { Change: 66, ReadFullRecord: 98573, Select: 131071 },
|
||||
Unit: { Change: 330, ReadFullRecord: 98309, Select: 131071 },
|
||||
UnitModel: { Change: 74, ReadFullRecord: 98565, Select: 131071 },
|
||||
UnitMeterReading: { Change: 330, ReadFullRecord: 98309, Select: 131071 },
|
||||
Vendor: { Change: 106, ReadFullRecord: 98565, Select: 131071 },
|
||||
TaskGroup: { Change: 10, ReadFullRecord: 131071, Select: 131071 },
|
||||
WorkOrderStatus: { Change: 74, ReadFullRecord: 98565, Select: 131071 },
|
||||
WorkOrderItemStatus: { Change: 74, ReadFullRecord: 98565, Select: 131071 },
|
||||
WorkOrderItemPriority: { Change: 74, ReadFullRecord: 98565, Select: 131071 },
|
||||
WorkOrder: { Change: 1994, ReadFullRecord: 98949, Select: 131071 },
|
||||
WorkOrderItem: { Change: 330, ReadFullRecord: 98949, Select: 131071 },
|
||||
WorkOrderItemExpense: { Change: 458, ReadFullRecord: 98949, Select: 131071 },
|
||||
WorkOrderItemLabor: { Change: 1994, ReadFullRecord: 98949, Select: 131071 },
|
||||
WorkOrderItemLoan: { Change: 330, ReadFullRecord: 99461, Select: 131071 },
|
||||
WorkOrderItemPart: { Change: 330, ReadFullRecord: 99461, Select: 131071 },
|
||||
WorkOrderItemPartRequest: {
|
||||
Change: 330,
|
||||
ReadFullRecord: 99461,
|
||||
Select: 131071
|
||||
},
|
||||
WorkOrderItemScheduledUser: {
|
||||
Change: 330,
|
||||
ReadFullRecord: 99973,
|
||||
Select: 131071
|
||||
},
|
||||
WorkOrderItemTask: { Change: 1994, ReadFullRecord: 98949, Select: 131071 },
|
||||
WorkOrderItemTravel: { Change: 1994, ReadFullRecord: 98949, Select: 131071 },
|
||||
WorkOrderItemUnit: { Change: 330, ReadFullRecord: 99461, Select: 131071 },
|
||||
WorkOrderItemOutsideService: {
|
||||
Change: 330,
|
||||
ReadFullRecord: 98437,
|
||||
Select: 131071
|
||||
},
|
||||
Quote: { Change: 32842, ReadFullRecord: 65541, Select: 131071 },
|
||||
QuoteItem: { Change: 32842, ReadFullRecord: 65541, Select: 131071 },
|
||||
QuoteItemExpense: { Change: 32842, ReadFullRecord: 65541, Select: 131071 },
|
||||
QuoteItemLabor: { Change: 32842, ReadFullRecord: 65541, Select: 131071 },
|
||||
QuoteItemLoan: { Change: 32842, ReadFullRecord: 65541, Select: 131071 },
|
||||
QuoteItemPart: { Change: 32842, ReadFullRecord: 65541, Select: 131071 },
|
||||
QuoteItemScheduledUser: {
|
||||
Change: 32842,
|
||||
ReadFullRecord: 65541,
|
||||
Select: 131071
|
||||
},
|
||||
QuoteItemTask: { Change: 32842, ReadFullRecord: 65541, Select: 131071 },
|
||||
QuoteItemTravel: { Change: 32842, ReadFullRecord: 65541, Select: 131071 },
|
||||
QuoteItemUnit: { Change: 32842, ReadFullRecord: 65541, Select: 131071 },
|
||||
QuoteItemOutsideService: {
|
||||
Change: 32842,
|
||||
ReadFullRecord: 65541,
|
||||
Select: 131071
|
||||
},
|
||||
QuoteStatus: { Change: 32842, ReadFullRecord: 131071, Select: 131071 },
|
||||
PM: { Change: 10, ReadFullRecord: 98309, Select: 131071 },
|
||||
PMItem: { Change: 10, ReadFullRecord: 98309, Select: 131071 },
|
||||
PMItemExpense: { Change: 10, ReadFullRecord: 98309, Select: 131071 },
|
||||
PMItemLabor: { Change: 10, ReadFullRecord: 98309, Select: 131071 },
|
||||
PMItemLoan: { Change: 10, ReadFullRecord: 98309, Select: 131071 },
|
||||
PMItemPart: { Change: 10, ReadFullRecord: 98309, Select: 131071 },
|
||||
PMItemScheduledUser: { Change: 10, ReadFullRecord: 98309, Select: 131071 },
|
||||
PMItemTask: { Change: 10, ReadFullRecord: 98309, Select: 131071 },
|
||||
PMItemTravel: { Change: 10, ReadFullRecord: 98309, Select: 131071 },
|
||||
PMItemUnit: { Change: 10, ReadFullRecord: 98309, Select: 131071 },
|
||||
PMItemOutsideService: { Change: 10, ReadFullRecord: 98309, Select: 131071 },
|
||||
Global: { Change: 2, ReadFullRecord: 1, Select: 0 },
|
||||
GlobalOps: { Change: 16384, ReadFullRecord: 8192, Select: 0 },
|
||||
User: { Change: 2, ReadFullRecord: 1, Select: 131071 },
|
||||
UserOptions: { Change: 2, ReadFullRecord: 1, Select: 0 },
|
||||
ServerState: { Change: 16384, ReadFullRecord: 131071, Select: 0 },
|
||||
License: { Change: 2, ReadFullRecord: 49515, Select: 0 },
|
||||
TrialSeeder: { Change: 16386, ReadFullRecord: 8193, Select: 0 },
|
||||
LogFile: { Change: 0, ReadFullRecord: 24576, Select: 0 },
|
||||
Backup: { Change: 16384, ReadFullRecord: 8195, Select: 0 },
|
||||
FileAttachment: { Change: 2, ReadFullRecord: 3, Select: 0 },
|
||||
ServerJob: { Change: 16384, ReadFullRecord: 8195, Select: 0 },
|
||||
OpsNotificationSettings: { Change: 16384, ReadFullRecord: 8195, Select: 0 },
|
||||
ServerMetrics: { Change: 16384, ReadFullRecord: 24576, Select: 0 },
|
||||
Translation: { Change: 2, ReadFullRecord: 1, Select: 131071 },
|
||||
DataListSavedFilter: { Change: 2, ReadFullRecord: 131071, Select: 0 },
|
||||
FormUserOptions: { Change: 131071, ReadFullRecord: 131071, Select: 0 },
|
||||
FormCustom: { Change: 2, ReadFullRecord: 131071, Select: 0 },
|
||||
PickListTemplate: { Change: 2, ReadFullRecord: 131071, Select: 0 },
|
||||
BizMetrics: { Change: 2, ReadFullRecord: 98369, Select: 0 },
|
||||
Notification: { Change: 131071, ReadFullRecord: 131071, Select: 0 },
|
||||
NotifySubscription: { Change: 131071, ReadFullRecord: 131071, Select: 0 },
|
||||
Report: { Change: 3, ReadFullRecord: 131071, Select: 131071 },
|
||||
CustomerServiceRequest: {
|
||||
Change: 4106,
|
||||
ReadFullRecord: 2309,
|
||||
Select: 131071
|
||||
},
|
||||
Memo: { Change: 124927, ReadFullRecord: 124927, Select: 124927 },
|
||||
Reminder: { Change: 124927, ReadFullRecord: 124927, Select: 124927 },
|
||||
Review: { Change: 124927, ReadFullRecord: 124927, Select: 124927 },
|
||||
Integration: { Change: 49514, ReadFullRecord: 49514, Select: 49514 }
|
||||
};
|
||||
400
client/src/api/dash-registry.js
Normal file
@@ -0,0 +1,400 @@
|
||||
import authorizationroles from "./authorizationroles";
|
||||
const role = authorizationroles.AUTHORIZATION_ROLES;
|
||||
/*
|
||||
|
||||
*/
|
||||
export default {
|
||||
registry: [
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.Accounting,
|
||||
role.Tech,
|
||||
role.TechRestricted
|
||||
],
|
||||
title: "DashboardWorkOrderByStatusList",
|
||||
icon: "$sockiListAlt",
|
||||
type: "GzDashWorkorderByStatusList",
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
timeSpan: "*thisyear*",
|
||||
interval: "month",
|
||||
wostatus: null,
|
||||
wotags: [],
|
||||
wotagsany: true,
|
||||
woitemtags: [],
|
||||
woitemtagsany: true
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.Accounting
|
||||
],
|
||||
title: "DashboardWorkOrderStatusCount",
|
||||
icon: "$sockiChartBar",
|
||||
type: "GzDashWorkOrderStatusCount",
|
||||
scheduleableUserOnly: false,
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
timeSpan: "*thisyear*",
|
||||
interval: "month",
|
||||
wotags: [],
|
||||
wotagsany: true
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.Accounting
|
||||
],
|
||||
title: "DashboardWorkOrderStatusPct",
|
||||
icon: "$sockiChartBar",
|
||||
type: "GzDashWorkOrderStatusPct",
|
||||
scheduleableUserOnly: false,
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
timeSpan: "*thisyear*",
|
||||
interval: "month",
|
||||
wotags: [],
|
||||
wotagsany: true
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.Accounting
|
||||
],
|
||||
title: "DashboardPctWorkOrderCompletedOnTime",
|
||||
icon: "$sockiChartBar",
|
||||
type: "GzDashPctWorkOrderCompletedOnTimeBar",
|
||||
scheduleableUserOnly: false,
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
timeSpan: "*thisyear*",
|
||||
interval: "month",
|
||||
wotags: [],
|
||||
wotagsany: true,
|
||||
color: "#00205BFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.Accounting
|
||||
],
|
||||
title: "DashboardCountWorkOrdersCreated",
|
||||
icon: "$sockiChartLine",
|
||||
type: "GzDashWorkOrderCreatedCountLine",
|
||||
scheduleableUserOnly: false,
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
timeSpan: "*thisyear*",
|
||||
interval: "day",
|
||||
wotags: [],
|
||||
wotagsany: true,
|
||||
woitemtags: [],
|
||||
woitemtagsany: true,
|
||||
color: "#00205BFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.Accounting
|
||||
],
|
||||
title: "DashboardCountWorkOrdersCreated",
|
||||
icon: "$sockiChartBar",
|
||||
type: "GzDashWorkOrderCreatedCountBar",
|
||||
scheduleableUserOnly: false,
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
timeSpan: "*thisyear*",
|
||||
wotags: [],
|
||||
wotagsany: true,
|
||||
woitemtags: [],
|
||||
woitemtagsany: true,
|
||||
interval: "month",
|
||||
color: "#00205BFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.Accounting
|
||||
],
|
||||
title: "DashboardOverdueAll",
|
||||
icon: "$sockiListAlt",
|
||||
type: "GzDashWorkorderOverdueAllList",
|
||||
scheduleableUserOnly: false,
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
wotags: [],
|
||||
wotagsany: true,
|
||||
woitemtags: [],
|
||||
woitemtagsany: true
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [role.Tech, role.TechRestricted],
|
||||
title: "DashboardOverdue",
|
||||
icon: "$sockiListAlt",
|
||||
type: "GzDashWorkorderOverduePersonalList",
|
||||
scheduleableUserOnly: true,
|
||||
singleOnly: true,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
wotags: [],
|
||||
wotagsany: true,
|
||||
woitemtags: [],
|
||||
woitemtagsany: true
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.Tech,
|
||||
role.TechRestricted
|
||||
],
|
||||
title: "DashboardOpenCSR",
|
||||
icon: "$sockiListAlt",
|
||||
type: "GzDashCSROpenList",
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
custtags: [],
|
||||
custtagsany: true
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.Accounting,
|
||||
role.Tech,
|
||||
role.TechRestricted
|
||||
],
|
||||
title: "DashboardNotScheduled",
|
||||
icon: "$sockiListAlt",
|
||||
type: "GzDashWorkorderUnscheduledOpenList",
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
wostatus: null,
|
||||
wotags: [],
|
||||
wotagsany: true,
|
||||
woitemtags: [],
|
||||
woitemtagsany: true
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.InventoryRestricted,
|
||||
role.Inventory,
|
||||
role.Accounting,
|
||||
role.Tech,
|
||||
role.TechRestricted,
|
||||
role.OpsAdmin,
|
||||
role.OpsAdminRestricted,
|
||||
role.Sales,
|
||||
role.SalesRestricted
|
||||
],
|
||||
title: "ReminderList",
|
||||
icon: "$sockiCalendarDay",
|
||||
type: "GzDashTodayReminders",
|
||||
singleOnly: true,
|
||||
settings: {}
|
||||
},
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.InventoryRestricted,
|
||||
role.Inventory,
|
||||
role.Accounting,
|
||||
role.Tech,
|
||||
role.TechRestricted,
|
||||
role.OpsAdmin,
|
||||
role.OpsAdminRestricted,
|
||||
role.Sales,
|
||||
role.SalesRestricted
|
||||
],
|
||||
title: "ReviewList",
|
||||
icon: "$sockiCalendarDay",
|
||||
type: "GzDashTodayReviews",
|
||||
singleOnly: true,
|
||||
settings: {}
|
||||
},
|
||||
{
|
||||
roles: [role.Tech, role.TechRestricted],
|
||||
title: "DashboardScheduled",
|
||||
icon: "$sockiCalendarDay",
|
||||
type: "GzDashTodayScheduledWo",
|
||||
scheduleableUserOnly: true,
|
||||
singleOnly: true,
|
||||
settings: {}
|
||||
},
|
||||
{
|
||||
roles: [role.Tech, role.TechRestricted],
|
||||
title: "WorkOrderItemLaborServiceRateQuantity",
|
||||
icon: "$sockiChartLine",
|
||||
type: "GzDashLaborHoursPersonalLine",
|
||||
scheduleableUserOnly: true,
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
timeSpan: "*thisyear*",
|
||||
interval: "day",
|
||||
wotags: [],
|
||||
wotagsany: true,
|
||||
woitemtags: [],
|
||||
woitemtagsany: true,
|
||||
color: "#00205BFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [role.Tech, role.TechRestricted],
|
||||
title: "WorkOrderItemLaborServiceRateQuantity",
|
||||
icon: "$sockiChartBar",
|
||||
type: "GzDashLaborHoursPersonalBar",
|
||||
scheduleableUserOnly: true,
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
timeSpan: "*thisyear*",
|
||||
wotags: [],
|
||||
wotagsany: true,
|
||||
woitemtags: [],
|
||||
woitemtagsany: true,
|
||||
interval: "month",
|
||||
color: "#00205BFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.Accounting
|
||||
],
|
||||
title: "DashboardServiceRateQuantityAllUsers",
|
||||
icon: "$sockiChartLine",
|
||||
type: "GzDashLaborHoursEveryoneLine",
|
||||
scheduleableUserOnly: false,
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
timeSpan: "*thisyear*",
|
||||
interval: "month",
|
||||
wotags: [],
|
||||
wotagsany: true,
|
||||
woitemtags: [],
|
||||
woitemtagsany: true,
|
||||
techtags: [],
|
||||
techtagsany: true,
|
||||
userid: null,
|
||||
color: "#00205BFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
roles: [
|
||||
role.BizAdmin,
|
||||
role.BizAdminRestricted,
|
||||
role.ServiceRestricted,
|
||||
role.Service,
|
||||
role.Accounting
|
||||
],
|
||||
title: "DashboardServiceRateQuantityAllUsers",
|
||||
icon: "$sockiChartBar",
|
||||
type: "GzDashLaborHoursEveryoneBar",
|
||||
scheduleableUserOnly: false,
|
||||
singleOnly: false,
|
||||
settings: {
|
||||
customTitle: null,
|
||||
timeSpan: "*thisyear*",
|
||||
wotags: [],
|
||||
wotagsany: true,
|
||||
woitemtags: [],
|
||||
woitemtagsany: true,
|
||||
techtags: [],
|
||||
techtagsany: true,
|
||||
userid: null,
|
||||
interval: "month",
|
||||
color: "#00205BFF"
|
||||
}
|
||||
}
|
||||
],
|
||||
availableItems() {
|
||||
const ret = [];
|
||||
for (let i = 0; i < this.registry.length; i++) {
|
||||
const item = this.registry[i];
|
||||
|
||||
if (authorizationroles.hasRole(item.roles)) {
|
||||
//if it's only for sched users and not then skip
|
||||
if (
|
||||
item.scheduleableUserOnly &&
|
||||
!window.$gz.store.getters.isScheduleableUser
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
ret.push({
|
||||
id: i,
|
||||
title: item.title,
|
||||
icon: item.icon,
|
||||
type: item.type,
|
||||
singleOnly: item.singleOnly,
|
||||
settings: item.settings
|
||||
});
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
async cacheTranslationsForAvailableItems() {
|
||||
const items = this.availableItems();
|
||||
|
||||
//await window.$gz.translation.cacheTranslations(items.map(z => z.title));
|
||||
await window.$gz.translation.cacheTranslations([
|
||||
...new Set(items.map(z => z.title))
|
||||
]);
|
||||
}
|
||||
};
|
||||
101
client/src/api/enums.js
Normal file
@@ -0,0 +1,101 @@
|
||||
export default {
|
||||
get(enumKey, enumValue) {
|
||||
enumKey = enumKey.toLowerCase();
|
||||
if (enumKey != "authorizationroles") {
|
||||
if (window.$gz.store.state.enums[enumKey] == undefined) {
|
||||
throw new Error(
|
||||
"ERROR enums::get -> enumKey " + enumKey + " is missing from store"
|
||||
);
|
||||
}
|
||||
|
||||
const ret = window.$gz.store.state.enums[enumKey][enumValue];
|
||||
if (ret == undefined) {
|
||||
return "";
|
||||
} else {
|
||||
return ret;
|
||||
}
|
||||
} else {
|
||||
const ret = [];
|
||||
if (enumValue == null || enumValue == 0) {
|
||||
return "";
|
||||
}
|
||||
const availableRoles = this.getSelectionList("AuthorizationRoles");
|
||||
for (let i = 0; i < availableRoles.length; i++) {
|
||||
const role = availableRoles[i];
|
||||
if (enumValue & role.id) {
|
||||
ret.push(role.name);
|
||||
}
|
||||
}
|
||||
return ret.join(", ");
|
||||
}
|
||||
},
|
||||
//////////////////////////////////
|
||||
//
|
||||
// Used by forms to fetch selection list data
|
||||
// Sorts alphabetically by default but can be turned off with do not sort
|
||||
//
|
||||
getSelectionList(enumKey, noSort) {
|
||||
enumKey = enumKey.toLowerCase();
|
||||
const e = window.$gz.store.state.enums[enumKey];
|
||||
if (!e) {
|
||||
throw new Error(
|
||||
"ERROR enums::getSelectionList -> enumKey " +
|
||||
enumKey +
|
||||
" is missing from store"
|
||||
);
|
||||
}
|
||||
const ret = [];
|
||||
|
||||
//turn it into an array suitable for selection lists
|
||||
for (const [key, value] of Object.entries(e)) {
|
||||
ret.push({ id: Number(key), name: value });
|
||||
}
|
||||
//sort by name
|
||||
if (!noSort) {
|
||||
ret.sort(window.$gz.util.sortByKey("name"));
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
///////////////////////////////////
|
||||
//
|
||||
// Fetches enum list from server
|
||||
// and puts in store. if necessary
|
||||
// ACCEPTS an ARRAY or a single STRING KEY
|
||||
//
|
||||
async fetchEnumList(enumKey) {
|
||||
if (!Array.isArray(enumKey)) {
|
||||
enumKey = [enumKey];
|
||||
}
|
||||
for (let i = 0; i < enumKey.length; i++) {
|
||||
//check if list
|
||||
//if not then fetch it and store it
|
||||
const k = enumKey[i].toLowerCase();
|
||||
|
||||
//de-lodash
|
||||
// if (!window.$gz. _.has(window.$gz.store.state.enums, k)) {
|
||||
//enums is an object this is checking if that object has a key with the name in k
|
||||
if (!window.$gz.util.has(window.$gz.store.state.enums, k)) {
|
||||
const that = this;
|
||||
|
||||
const dat = await that.fetchEnumKey(k);
|
||||
//massage the data as necessary
|
||||
const e = { enumKey: k, items: {} };
|
||||
for (let i = 0; i < dat.length; i++) {
|
||||
const o = dat[i];
|
||||
e.items[o.id] = o.name;
|
||||
}
|
||||
//stuff the data into the store
|
||||
window.$gz.store.commit("setEnum", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
async fetchEnumKey(enumKey) {
|
||||
const res = await window.$gz.api.get("enum-list/list/" + enumKey);
|
||||
//We never expect there to be no data here
|
||||
//if (!Object.prototype.hasOwnProperty.call(res, "data")) {
|
||||
if (!Object.prototype.hasOwnProperty.call(res, "data")) {
|
||||
return Promise.reject(res);
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
};
|
||||
230
client/src/api/errorhandler.js
Normal file
@@ -0,0 +1,230 @@
|
||||
let lastMessageHash = 0;
|
||||
let lastMessageTimeStamp = new Date();
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
//
|
||||
// translate, Log and optionally display errors
|
||||
// return translated message in case caller needs it
|
||||
async function dealWithError(msg, vm) {
|
||||
//Check if this is the same message again as last time within a short time span to avoid endless looping errors of same message
|
||||
//but still allow for user to repeat operation that causes error so they can view it
|
||||
const newHash = window.$gz.util.quickHash(msg);
|
||||
if (newHash == lastMessageHash) {
|
||||
const tsnow = new Date();
|
||||
//don't show the same exact message if it was just shown less than 1 second ago
|
||||
if (tsnow - lastMessageTimeStamp < 1000) return;
|
||||
}
|
||||
lastMessageHash = newHash;
|
||||
lastMessageTimeStamp = new Date();
|
||||
|
||||
//translate as necessary
|
||||
msg = await window.$gz.translation.translateStringWithMultipleKeysAsync(msg);
|
||||
|
||||
//In some cases the error may not be translatable, if this is not a debug run then it should show without the ?? that translating puts in keys not found
|
||||
//so it's not as weird looking to the user
|
||||
//vm may be null here so check window gz for dev
|
||||
if (!window.$gz.dev && msg.includes("??")) {
|
||||
msg = msg.replace("??", "");
|
||||
}
|
||||
window.$gz.store.commit("logItem", msg);
|
||||
if (window.$gz.dev) {
|
||||
const errMsg = "DEV MODE errorHandler.js:: Unexpected error: \r\n" + msg;
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(errMsg);
|
||||
|
||||
// eslint-disable-next-line no-debugger
|
||||
debugger;
|
||||
}
|
||||
|
||||
//If a form instance was provided (vue instance)
|
||||
//and it can display and error then put the error into it
|
||||
if (!vm || vm.formState == undefined) {
|
||||
//Special work around to not redundantly display errors when Sockeye job fails
|
||||
// and Vue decides to throw it's own error into the mix when we've already displayed appropriate message
|
||||
if (msg.includes("Vue error") && msg.includes("Job failed")) {
|
||||
return;
|
||||
}
|
||||
|
||||
//popup if no place to display it elsewise
|
||||
window.$gz.eventBus.$emit("notify-error", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
//should be able to display in form...
|
||||
if (vm.$sock.dev) {
|
||||
//make sure formState.appError is defined on data
|
||||
if (!window.$gz.util.has(vm, "formState.appError")) {
|
||||
throw new Error(
|
||||
"DEV ERROR errorHandler::dealWithError -> formState.appError seems to be missing from form's vue data object"
|
||||
);
|
||||
}
|
||||
}
|
||||
vm.formState.appError = msg;
|
||||
|
||||
//TODO: What is this doing exactly?
|
||||
//it's related to server errors but I'm setting appError above
|
||||
//why two error properties?
|
||||
window.$gz.form.setErrorBoxErrors(vm);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// DECODE ERROR TO TEXT
|
||||
// accept an unknown type of error variable
|
||||
// and return human readable text
|
||||
//
|
||||
function decodeError(e, vm) {
|
||||
// console.log("decodeError full e object as is: ");
|
||||
// console.log(e);
|
||||
// console.log("decodeError full e object stringified: ", JSON.stringify(e));
|
||||
// console.log("decodeError is typeof:", typeof e);
|
||||
// console.log("decodeError e is instanceof Error ", e instanceof Error);
|
||||
// console.log(
|
||||
// "decodeError e is a string already: ",
|
||||
// window.$gz.util.isString(e)
|
||||
// );
|
||||
|
||||
//already a string?
|
||||
if (window.$gz.util.isString(e)) {
|
||||
return e; //nothing to do here, already a string
|
||||
}
|
||||
|
||||
if (e instanceof Error) {
|
||||
//an Error object?
|
||||
return `Error - Name:${e.name}, Message:${e.message}`;
|
||||
}
|
||||
|
||||
if (
|
||||
e == null ||
|
||||
e == "" ||
|
||||
(typeof e === "object" && Object.keys(e).length === 0)
|
||||
) {
|
||||
return `errorHandler::decodeError - Error is unknown / empty (e:${e})`;
|
||||
}
|
||||
|
||||
//API error object or error RESPONSE object?
|
||||
if (e.error || e.code) {
|
||||
let err = null;
|
||||
//could be the error RESPONSE or it could be the error object *inside* the error response so sort out here
|
||||
if (e.error) {
|
||||
//it's the entire resopnse object
|
||||
err = e.error;
|
||||
} else {
|
||||
//it's the inner error object only
|
||||
err = e;
|
||||
}
|
||||
let msg = "";
|
||||
if (err.code) {
|
||||
msg += err.code;
|
||||
msg += " - ";
|
||||
if (vm) {
|
||||
msg += vm.$sock.t("ErrorAPI" + err.code);
|
||||
}
|
||||
msg += "\n";
|
||||
}
|
||||
if (err.target) {
|
||||
msg += err.target;
|
||||
msg += "\n";
|
||||
}
|
||||
|
||||
if (err.message && !err.message.startsWith("ErrorAPI")) {
|
||||
//errapi already dealt with above no need to repeat it here
|
||||
msg += err.message;
|
||||
msg += "\n";
|
||||
}
|
||||
|
||||
if (err.details) {
|
||||
err.details.forEach(z => {
|
||||
let zerror = null;
|
||||
if (z.error) {
|
||||
zerror = z.error + " - ";
|
||||
}
|
||||
msg += `${zerror}${z.message}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
//console.log("errorhandler:decodeError returning message:", msg);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
//Javascript Fetch API Response object?
|
||||
if (e instanceof Response) {
|
||||
return `http error: ${e.statusText} - ${e.status} Url: ${e.url}`;
|
||||
}
|
||||
|
||||
//last resort
|
||||
return JSON.stringify(e);
|
||||
}
|
||||
export default {
|
||||
handleGeneralError(message, source, lineno, colno, error) {
|
||||
let msg = "General error: \n" + message;
|
||||
if (source) {
|
||||
msg += "\nsource: " + source;
|
||||
}
|
||||
if (lineno) {
|
||||
msg += "\nlineno: " + lineno;
|
||||
}
|
||||
if (colno) {
|
||||
msg += "\ncolno: " + colno;
|
||||
}
|
||||
if (error) {
|
||||
if (typeof error === "object") {
|
||||
error = JSON.stringify(error);
|
||||
}
|
||||
msg += "\nerror: " + error;
|
||||
}
|
||||
dealWithError(msg);
|
||||
},
|
||||
handleVueError(err, vm, info) {
|
||||
let msg = "Vue error: \n" + decodeError(err, vm);
|
||||
if (err.fileName) {
|
||||
msg += "\nfilename: " + err.fileName;
|
||||
}
|
||||
if (err.lineNumber) {
|
||||
msg += "\nlineNumber: " + err.lineNumber;
|
||||
}
|
||||
if (info) {
|
||||
msg += "\ninfo: " + info;
|
||||
}
|
||||
if (err.stack) {
|
||||
msg += "\nSTACK:\n " + err.stack;
|
||||
}
|
||||
dealWithError(msg, vm);
|
||||
},
|
||||
handleVueWarning(wmsg, vm, trace) {
|
||||
let msg = "Vue warning: \n" + decodeError(wmsg, vm);
|
||||
if (trace) {
|
||||
msg += "\ntrace: " + trace;
|
||||
}
|
||||
dealWithError(msg, vm);
|
||||
},
|
||||
/////////////////////////////////////////////////
|
||||
// translate, log and return error
|
||||
//
|
||||
handleFormError(err, vm) {
|
||||
if (window.$gz.dev) {
|
||||
console.trace(err);
|
||||
}
|
||||
//called inside forms when things go unexpectedly wrong
|
||||
dealWithError(decodeError(err, vm), vm);
|
||||
},
|
||||
/////////////////////////////////////////////////
|
||||
// decode error into string suitable to display
|
||||
//
|
||||
errorToString(err, vm) {
|
||||
//called inside forms when things go unexpectedly wrong
|
||||
return decodeError(err, vm);
|
||||
}
|
||||
};
|
||||
/*
|
||||
ERROR CODES USED:
|
||||
Client error codes are all in the range of E16 to E999
|
||||
Server error codes are all in the range of E1000 to E1999
|
||||
API specific (logic) error codes are all in the range of 2000 to 3000
|
||||
|
||||
CLIENT ERROR CODES:
|
||||
E16 - ErrorUserNotAuthenticated
|
||||
E17 - ErrorServerUnresponsive
|
||||
E18 - Misc error without a translation key, unexpected throws etc or api error during server call, details in the message / Any error without a translation key defined basically
|
||||
|
||||
*/
|
||||
2
client/src/api/eventbus.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Vue from "vue";
|
||||
export default new Vue();
|
||||
83
client/src/api/form-custom-template.js
Normal file
@@ -0,0 +1,83 @@
|
||||
///Add data key names which make the custom fields control work more easily
|
||||
///Since the names can be inferred from the data that comes from the server it saves bandwidth to do it here at the client
|
||||
function addDataKeyNames(obj) {
|
||||
//iterate the array of objects
|
||||
//if it has a "type" property then it's a custom field so add its data key name
|
||||
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
if (obj[i].type) {
|
||||
obj[i]["dataKey"] = "c" + parseInt(obj[i].fld.replace(/^\D+/g, ""));
|
||||
}
|
||||
}
|
||||
|
||||
//return the whole thing again now translated
|
||||
return obj;
|
||||
}
|
||||
|
||||
export default {
|
||||
////////////////////////////////
|
||||
// Cache the form customization data if it's not already present
|
||||
// NOTE: FORM KEY **MUST** BE THE AYATYPE NAME WHERE POSSIBLE, IF NO TYPE THEN AN EXCEPTION NEEDS TO BE CODED IN
|
||||
//SERVER FormFieldReference.cs -> public static List<string> FormFieldKeys
|
||||
//
|
||||
async get(formKey, vm, forceRefresh) {
|
||||
if (
|
||||
forceRefresh ||
|
||||
!window.$gz.util.has(window.$gz.store.state.formCustomTemplate, formKey)
|
||||
) {
|
||||
//fetch and populate the store
|
||||
const res = await window.$gz.api.get("form-custom/" + formKey);
|
||||
if (res.error) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(res, vm));
|
||||
}
|
||||
|
||||
window.$gz.store.commit("setFormCustomTemplateItem", {
|
||||
formKey: formKey,
|
||||
concurrency: res.data.concurrency,
|
||||
value: addDataKeyNames(JSON.parse(res.data.template))
|
||||
});
|
||||
}
|
||||
},
|
||||
set(formKey, token, template) {
|
||||
window.$gz.store.commit("setFormCustomTemplateItem", {
|
||||
formKey: formKey,
|
||||
concurrency: token,
|
||||
value: addDataKeyNames(JSON.parse(template))
|
||||
});
|
||||
},
|
||||
getFieldTemplateValue(formKey, fieldKey) {
|
||||
if (fieldKey === undefined) {
|
||||
throw new Error(
|
||||
"ERROR form-custom-template::getFieldTemplateValue -> fieldKey not specified for template for form [" +
|
||||
formKey +
|
||||
"]"
|
||||
);
|
||||
}
|
||||
|
||||
const template = window.$gz.store.state.formCustomTemplate[formKey];
|
||||
if (template === undefined) {
|
||||
throw new Error(
|
||||
"ERROR form-custom-template::getFieldTemplateValue -> Store is missing form template for [" +
|
||||
formKey +
|
||||
"]"
|
||||
);
|
||||
}
|
||||
|
||||
//Note that not every field being requested will exist so it's valid to return undefined
|
||||
//template is an array of objects that contain a key called "fld"
|
||||
return template.find(z => z.fld == fieldKey);
|
||||
},
|
||||
getTemplateConcurrencyToken(formKey) {
|
||||
const tok =
|
||||
window.$gz.store.state.formCustomTemplate[formKey + "_concurrencyToken"];
|
||||
if (tok === undefined) {
|
||||
throw new Error(
|
||||
"ERROR form-custom-template::getTemplateConcurrencyToken -> Store is missing concurrency token for [" +
|
||||
formKey +
|
||||
"]"
|
||||
);
|
||||
}
|
||||
|
||||
return tok;
|
||||
}
|
||||
};
|
||||
672
client/src/api/gzapi.js
Normal file
@@ -0,0 +1,672 @@
|
||||
import router from "../router";
|
||||
|
||||
function stringifyPrimitive(v) {
|
||||
switch (typeof v) {
|
||||
case "string":
|
||||
return v;
|
||||
|
||||
case "boolean":
|
||||
return v ? "true" : "false";
|
||||
|
||||
case "number":
|
||||
return isFinite(v) ? v : "";
|
||||
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////
|
||||
// Try to handle an api error
|
||||
// return true if handled or false if not
|
||||
//
|
||||
function handleError(action, error, route) {
|
||||
const errorMessage =
|
||||
"API error: " + action + " route =" + route + ", message =" + error.message;
|
||||
window.$gz.store.commit("logItem", errorMessage);
|
||||
|
||||
//Handle 403 not authorized
|
||||
//popup not authorized, log, then go to HOME
|
||||
//was going to go back one page, but realized most of the time a not authorized is in
|
||||
//reaction to directly entered or opened link, not application logic driving it, so home is safest choice
|
||||
//
|
||||
if (error.message && error.message.includes("NotAuthorized")) {
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-warning",
|
||||
window.$gz.translation.get("ErrorUserNotAuthorized")
|
||||
);
|
||||
router.push(window.$gz.store.state.homePage);
|
||||
|
||||
throw new Error("LT:ErrorUserNotAuthorized");
|
||||
}
|
||||
|
||||
//Handle 401 not authenticated
|
||||
if (error.message && error.message.includes("NotAuthenticated")) {
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-error",
|
||||
window.$gz.translation.get("ErrorUserNotAuthenticated")
|
||||
);
|
||||
|
||||
router.push("/login");
|
||||
|
||||
throw new Error("LT:ErrorUserNotAuthenticated");
|
||||
}
|
||||
|
||||
//is it a network error?
|
||||
//https://medium.com/@vinhlh/how-to-handle-networkerror-when-using-fetch-ff2663220435
|
||||
if (error instanceof TypeError) {
|
||||
if (
|
||||
error.message.includes("Failed to fetch") ||
|
||||
error.message.includes("NetworkError") ||
|
||||
error.message.includes("Network request failed")
|
||||
) {
|
||||
let msg = "";
|
||||
|
||||
if (window.$gz.store.state.authenticated) {
|
||||
msg = window.$gz.translation.get("ErrorServerUnresponsive");
|
||||
} else {
|
||||
msg = "Could not connect to Sockeye server ";
|
||||
}
|
||||
|
||||
msg += window.$gz.api.APIUrl("") + "\r\nError: " + error.message;
|
||||
|
||||
window.$gz.eventBus.$emit("notify-error", msg);
|
||||
//note: using translation key in square brackets
|
||||
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
//Ideally this should never get called because any issue should be addressed above
|
||||
window.$gz.errorHandler.handleFormError(error);
|
||||
}
|
||||
|
||||
export default {
|
||||
status(response) {
|
||||
//Handle expected api errors
|
||||
if (response.status == 401) {
|
||||
throw new Error("LT:ErrorUserNotAuthenticated");
|
||||
}
|
||||
|
||||
if (response.status == 403) {
|
||||
throw new Error("LT:ErrorUserNotAuthorized");
|
||||
}
|
||||
|
||||
//404 not found is an expected status not worth logging allow to bubble up
|
||||
//for client code to deal with
|
||||
if (response.status == 404) {
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
if (response.status == 405) {
|
||||
//Probably a development error
|
||||
|
||||
throw new Error("Method Not Allowed (route issue?) " + response.url);
|
||||
}
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return Promise.resolve(response);
|
||||
} else {
|
||||
//log unhandled api error
|
||||
window.$gz.store.commit(
|
||||
"logItem",
|
||||
"API error: status=" +
|
||||
response.status +
|
||||
", statusText=" +
|
||||
response.statusText +
|
||||
", url=" +
|
||||
response.url
|
||||
);
|
||||
//let it float up for dealing with by caller(s)
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
},
|
||||
statusEx(response) {
|
||||
//Handle expected api errors
|
||||
if (response.status == 401) {
|
||||
throw new Error("LT:ErrorUserNotAuthenticated");
|
||||
}
|
||||
|
||||
if (response.status == 403) {
|
||||
throw new Error("LT:ErrorUserNotAuthorized");
|
||||
}
|
||||
|
||||
//404 not found is an expected status not worth logging allow to bubble up
|
||||
//for client code to deal with
|
||||
if (response.status == 404) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status == 405) {
|
||||
//Probably a development error
|
||||
|
||||
throw new Error("Method Not Allowed (route issue?) " + response.url);
|
||||
}
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return;
|
||||
} else {
|
||||
//log unhandled api error
|
||||
|
||||
window.$gz.store.commit(
|
||||
"logItem",
|
||||
"API error: status=" +
|
||||
response.status +
|
||||
", statusText=" +
|
||||
response.statusText +
|
||||
", url=" +
|
||||
response.url
|
||||
);
|
||||
}
|
||||
},
|
||||
async extractBodyEx(response) {
|
||||
if (response.status == 204) {
|
||||
//no content, nothing to process
|
||||
return response;
|
||||
}
|
||||
const contentType = response.headers.get("content-type");
|
||||
|
||||
if (!contentType) {
|
||||
return response;
|
||||
}
|
||||
if (contentType.includes("json")) {
|
||||
return await response.json();
|
||||
}
|
||||
if (contentType.includes("text/plain")) {
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
if (contentType.includes("application/pdf")) {
|
||||
return await response.blob();
|
||||
}
|
||||
return response;
|
||||
},
|
||||
extractBody(response) {
|
||||
if (response.status == 204) {
|
||||
//no content, nothing to process
|
||||
return response;
|
||||
}
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (!contentType) {
|
||||
return response;
|
||||
}
|
||||
if (contentType.includes("json")) {
|
||||
return response.json();
|
||||
}
|
||||
if (contentType.includes("text/plain")) {
|
||||
return response.text();
|
||||
}
|
||||
return response;
|
||||
},
|
||||
apiErrorToHumanString(apiError) {
|
||||
//empty error object?
|
||||
if (!apiError) {
|
||||
return "(E18) - apiErrorToHumanString():: Empty API eror, unknown";
|
||||
}
|
||||
//convert to readable string
|
||||
return "(E18) - " + JSON.stringify(apiError);
|
||||
},
|
||||
patchAuthorizedHeaders() {
|
||||
return {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json-patch+json",
|
||||
Authorization: "Bearer " + window.$gz.store.state.apiToken
|
||||
};
|
||||
},
|
||||
postAuthorizedHeaders() {
|
||||
return {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + window.$gz.store.state.apiToken
|
||||
//this maybe useful in future like batch ops etc so keeping as a reminder
|
||||
//,"X-AY-Import-Mode": true
|
||||
};
|
||||
},
|
||||
postUnAuthorizedHeaders() {
|
||||
return {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
},
|
||||
fetchPostNoAuthOptions(data) {
|
||||
return {
|
||||
method: "post",
|
||||
mode: "cors",
|
||||
headers: this.postUnAuthorizedHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
},
|
||||
fetchPostOptions(data) {
|
||||
return {
|
||||
method: "post",
|
||||
mode: "cors",
|
||||
headers: this.postAuthorizedHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
},
|
||||
fetchPutOptions(data) {
|
||||
return {
|
||||
method: "put",
|
||||
mode: "cors",
|
||||
headers: this.postAuthorizedHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
},
|
||||
fetchGetOptions() {
|
||||
/* GET WITH AUTH */
|
||||
return {
|
||||
method: "get",
|
||||
mode: "cors",
|
||||
headers: this.postAuthorizedHeaders()
|
||||
};
|
||||
},
|
||||
fetchRemoveOptions() {
|
||||
/* REMOVE WITH AUTH */
|
||||
return {
|
||||
method: "delete",
|
||||
mode: "cors",
|
||||
headers: this.postAuthorizedHeaders()
|
||||
};
|
||||
},
|
||||
APIUrl(apiPath) {
|
||||
if (
|
||||
window.$gz.dev &&
|
||||
window.location.hostname == "localhost" &&
|
||||
window.location.port == "8080"
|
||||
) {
|
||||
return "http://localhost:7676/api/v8.0/" + apiPath;
|
||||
}
|
||||
|
||||
return (
|
||||
window.location.protocol +
|
||||
"//" +
|
||||
window.location.host +
|
||||
"/api/v8.0/" +
|
||||
apiPath
|
||||
);
|
||||
},
|
||||
helpUrl() {
|
||||
if (
|
||||
window.$gz.dev &&
|
||||
window.location.hostname == "localhost" &&
|
||||
window.location.port == "8080"
|
||||
) {
|
||||
return "http://localhost:7676/docs/";
|
||||
}
|
||||
return window.location.protocol + "//" + window.location.host + "/docs/";
|
||||
},
|
||||
helpUrlCustomer() {
|
||||
if (
|
||||
window.$gz.dev &&
|
||||
window.location.hostname == "localhost" &&
|
||||
window.location.port == "8080"
|
||||
) {
|
||||
return "http://localhost:7676/cust/";
|
||||
}
|
||||
return window.location.protocol + "//" + window.location.host + "/cust/";
|
||||
},
|
||||
/////////////////////////////
|
||||
// Just the server itself
|
||||
// used by profiler etc
|
||||
//
|
||||
ServerBaseUrl() {
|
||||
return this.helpUrl().replace("/docs/", "/");
|
||||
},
|
||||
/////////////////////////////
|
||||
// generic routed download URL
|
||||
//
|
||||
genericDownloadUrl(route) {
|
||||
//http://localhost:7676/api/v8/backup/download/100?t=sssss
|
||||
return this.APIUrl(route + "?t=" + window.$gz.store.state.downloadToken);
|
||||
},
|
||||
/////////////////////////////
|
||||
// report file download URL
|
||||
//
|
||||
reportDownloadUrl(fileName) {
|
||||
//http://localhost:7676/api/v8/report/download/filename.pdf?t=sssss
|
||||
|
||||
return this.APIUrl(
|
||||
"report/download/" +
|
||||
fileName +
|
||||
"?t=" +
|
||||
window.$gz.store.state.downloadToken
|
||||
);
|
||||
},
|
||||
/////////////////////////////
|
||||
// backup file download URL
|
||||
//
|
||||
backupDownloadUrl(fileName) {
|
||||
//http://localhost:7676/api/v8/backup/download/100?t=sssss
|
||||
|
||||
return this.APIUrl(
|
||||
"backup/download/" +
|
||||
fileName +
|
||||
"?t=" +
|
||||
window.$gz.store.state.downloadToken
|
||||
);
|
||||
},
|
||||
/////////////////////////////
|
||||
// attachment download URL
|
||||
//
|
||||
attachmentDownloadUrl(fileId, ctype) {
|
||||
//http://localhost:7676/api/v8/attachment/download/100?t=sssss
|
||||
//Ctype is optional and is the MIME content type, used to detect image urls at client for drag and drop ops
|
||||
//in wiki but ignored by server
|
||||
|
||||
let url =
|
||||
"attachment/download/" +
|
||||
fileId +
|
||||
"?t=" +
|
||||
window.$gz.store.state.downloadToken;
|
||||
|
||||
if (ctype && ctype.includes("image")) {
|
||||
url += "&i=1";
|
||||
}
|
||||
|
||||
return this.APIUrl(url);
|
||||
},
|
||||
/////////////////////////////
|
||||
// logo download URL
|
||||
// (size= 'small', 'medium', 'large')
|
||||
logoUrl(size) {
|
||||
//http://localhost:7676/api/v8/logo/small
|
||||
return this.APIUrl("logo/" + size);
|
||||
},
|
||||
/////////////////////////////
|
||||
// REPLACE END OF URL
|
||||
// (used to change ID in url)
|
||||
replaceAfterLastSlash(theUrl, theReplacement) {
|
||||
return theUrl.substr(0, theUrl.lastIndexOf("\\") + 1) + theReplacement;
|
||||
},
|
||||
/////////////////////////////
|
||||
// ENCODE QUERY STRING
|
||||
//
|
||||
buildQuery(obj, sep, eq, name) {
|
||||
sep = sep || "&";
|
||||
eq = eq || "=";
|
||||
if (obj === null) {
|
||||
obj = undefined;
|
||||
}
|
||||
|
||||
if (typeof obj === "object") {
|
||||
return Object.keys(obj)
|
||||
.map(function(k) {
|
||||
const ks = encodeURIComponent(stringifyPrimitive(k)) + eq;
|
||||
if (Array.isArray(obj[k])) {
|
||||
return obj[k]
|
||||
.map(function(v) {
|
||||
return ks + encodeURIComponent(stringifyPrimitive(v));
|
||||
})
|
||||
.join(sep);
|
||||
} else {
|
||||
return ks + encodeURIComponent(stringifyPrimitive(obj[k]));
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(sep);
|
||||
}
|
||||
|
||||
if (!name) return "";
|
||||
return (
|
||||
encodeURIComponent(stringifyPrimitive(name)) +
|
||||
eq +
|
||||
encodeURIComponent(stringifyPrimitive(obj))
|
||||
);
|
||||
},
|
||||
///////////////////////////////////
|
||||
// GET DATA FROM API SERVER
|
||||
//
|
||||
async get(route) {
|
||||
try {
|
||||
const that = this;
|
||||
|
||||
let r = await fetch(that.APIUrl(route), that.fetchGetOptions());
|
||||
that.statusEx(r);
|
||||
r = await that.extractBodyEx(r);
|
||||
return r;
|
||||
} catch (error) {
|
||||
//fundamental error, can't proceed with this call
|
||||
handleError("GET", error, route);
|
||||
}
|
||||
},
|
||||
|
||||
//////////////////////////////////////
|
||||
// Test delay for troubleshooting
|
||||
//
|
||||
doDelayAsync: () => {
|
||||
// eslint-disable-next-line
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve("I did something"), 10000);
|
||||
});
|
||||
},
|
||||
///////////////////////////////////
|
||||
// POST / PUT DATA TO API SERVER
|
||||
//
|
||||
async upsert(route, data, isLogin = false) {
|
||||
try {
|
||||
const that = this;
|
||||
//determine if this is a new or existing record
|
||||
let fetchOptions = undefined;
|
||||
//put?
|
||||
if (data && data.concurrency) {
|
||||
fetchOptions = that.fetchPutOptions(data);
|
||||
} else {
|
||||
//post
|
||||
//ensure the route doesn't end in /0 which will happen if it's a new record
|
||||
//since the edit forms just send the url here with the ID regardless
|
||||
if (route.endsWith("/0")) {
|
||||
route = route.slice(0, -2);
|
||||
}
|
||||
if (isLogin == false) {
|
||||
fetchOptions = that.fetchPostOptions(data);
|
||||
} else {
|
||||
fetchOptions = that.fetchPostNoAuthOptions(data);
|
||||
}
|
||||
}
|
||||
|
||||
let r = await fetch(that.APIUrl(route), fetchOptions);
|
||||
that.statusEx(r);
|
||||
r = await that.extractBodyEx(r);
|
||||
return r;
|
||||
} catch (error) {
|
||||
if (isLogin == false) {
|
||||
handleError("UPSERT", error, route);
|
||||
} else {
|
||||
//specifically this is for the login page
|
||||
console.log("upser error is: ", error);
|
||||
throw new Error(window.$gz.errorHandler.errorToString(error));
|
||||
}
|
||||
}
|
||||
},
|
||||
///////////////////////////////////
|
||||
// DELETE DATA FROM API SERVER
|
||||
//
|
||||
async remove(route) {
|
||||
const that = this;
|
||||
try {
|
||||
let r = await fetch(that.APIUrl(route), that.fetchRemoveOptions());
|
||||
that.statusEx(r);
|
||||
//delete will return a body if there is an error of some kind with the request
|
||||
r = await that.extractBodyEx(r);
|
||||
return r;
|
||||
} catch (error) {
|
||||
//fundamental error, can't proceed with this call
|
||||
handleError("DELETE", error, route);
|
||||
}
|
||||
},
|
||||
///////////////////////////////////
|
||||
// PUT DATA TO API SERVER
|
||||
// (used for puts that can't have a concurrency token like above)
|
||||
async put(route, data) {
|
||||
try {
|
||||
const that = this;
|
||||
let r = await fetch(that.APIUrl(route), that.fetchPutOptions(data));
|
||||
that.statusEx(r);
|
||||
r = await that.extractBodyEx(r);
|
||||
return r;
|
||||
} catch (error) {
|
||||
handleError("PUT", error, route);
|
||||
}
|
||||
},
|
||||
///////////////////////////////////
|
||||
// POST DATA TO API SERVER
|
||||
// (used for post only routes not needing upserts)
|
||||
async post(route, data) {
|
||||
try {
|
||||
const that = this;
|
||||
let r = await fetch(that.APIUrl(route), that.fetchPostOptions(data));
|
||||
that.statusEx(r);
|
||||
r = await that.extractBodyEx(r);
|
||||
return r;
|
||||
} catch (error) {
|
||||
handleError("POST", error, route);
|
||||
}
|
||||
},
|
||||
///////////////////////////////////
|
||||
// POST FILE ATTACHMENTS
|
||||
// @param {sockId:objectid, sockType:aType, files:[array of files]}
|
||||
//
|
||||
async uploadAttachment(at) {
|
||||
const that = this;
|
||||
try {
|
||||
var files = at.files;
|
||||
var data = new FormData();
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
data.append(files[i].name, files[i]);
|
||||
}
|
||||
|
||||
data.append("AttachToAType", at.sockType);
|
||||
data.append("AttachToObjectId", at.sockId);
|
||||
data.append("Notes", at.notes);
|
||||
data.append("FileData", at.fileData);
|
||||
|
||||
//-----------------
|
||||
|
||||
const fetchOptions = {
|
||||
method: "post",
|
||||
mode: "cors",
|
||||
headers: {
|
||||
Authorization: "Bearer " + window.$gz.store.state.apiToken
|
||||
},
|
||||
body: data
|
||||
};
|
||||
|
||||
let r = await fetch(that.APIUrl("attachment"), fetchOptions);
|
||||
that.statusEx(r);
|
||||
r = await that.extractBodyEx(r);
|
||||
return r;
|
||||
} catch (error) {
|
||||
handleError("POSTATTACHMENT", error, "uploadAttachmentRoute");
|
||||
}
|
||||
},
|
||||
//////////////////////////////////////////////
|
||||
// POST (UPLOAD) FILE TO ARBITRARY ROUTE
|
||||
// for various things that require an upload
|
||||
// e.g. translation import etc
|
||||
//
|
||||
//
|
||||
async upload(route, at) {
|
||||
const that = this;
|
||||
try {
|
||||
var files = at.files;
|
||||
var data = new FormData();
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
data.append(files[i].name, files[i]);
|
||||
}
|
||||
if (at.sockType) {
|
||||
data.append("SockType", at.sockType);
|
||||
}
|
||||
if (at.sockId) {
|
||||
data.append("ObjectId", at.sockId);
|
||||
}
|
||||
if (at.notes) {
|
||||
data.append("Notes", at.notes);
|
||||
}
|
||||
data.append("FileData", at.fileData);
|
||||
|
||||
//-----------------
|
||||
|
||||
const fetchOptions = {
|
||||
method: "post",
|
||||
mode: "cors",
|
||||
headers: {
|
||||
Authorization: "Bearer " + window.$gz.store.state.apiToken
|
||||
},
|
||||
body: data
|
||||
};
|
||||
|
||||
let r = await fetch(that.APIUrl(route), fetchOptions);
|
||||
that.statusEx(r);
|
||||
r = await that.extractBodyEx(r);
|
||||
return r;
|
||||
} catch (error) {
|
||||
handleError("POSTATTACHMENT", error, route);
|
||||
}
|
||||
},
|
||||
|
||||
///////////////////////////////////
|
||||
// POST LOGO
|
||||
//
|
||||
//
|
||||
async uploadLogo(fileData, size) {
|
||||
const that = this;
|
||||
try {
|
||||
const data = new FormData();
|
||||
data.append(fileData.name, fileData);
|
||||
|
||||
//-----------------
|
||||
|
||||
const fetchOptions = {
|
||||
method: "post",
|
||||
mode: "cors",
|
||||
headers: {
|
||||
Authorization: "Bearer " + window.$gz.store.state.apiToken
|
||||
},
|
||||
body: data
|
||||
};
|
||||
|
||||
let r = await fetch(that.APIUrl("logo/" + size), fetchOptions);
|
||||
that.statusEx(r);
|
||||
r = await that.extractBodyEx(r);
|
||||
return r;
|
||||
} catch (error) {
|
||||
handleError("uploadLogo", error, "postLogoRoute");
|
||||
}
|
||||
},
|
||||
///////////////////////////////////
|
||||
// REPORT CLIENT META DATA
|
||||
//
|
||||
//
|
||||
reportClientMetaData() {
|
||||
const nowUtc = window.$gz.locale.nowUTC8601String();
|
||||
return {
|
||||
UserName: window.$gz.store.state.userName,
|
||||
UserId: window.$gz.store.state.userId,
|
||||
Authorization: "Bearer " + window.$gz.store.state.apiToken, //api token for using api methods as current user viewing report
|
||||
DownloadToken: window.$gz.store.state.downloadToken,
|
||||
TimeZoneName: window.$gz.locale.getResolvedTimeZoneName(),
|
||||
LanguageName: window.$gz.locale.getResolvedLanguage(),
|
||||
Hour12: window.$gz.locale.getHour12(),
|
||||
CurrencyName: window.$gz.locale.getCurrencyName(),
|
||||
DefaultLocale: window.$gz.locale.getResolvedLanguage().split("-", 1)[0], //kind of suspect, maybe it can be removed
|
||||
PDFDate: window.$gz.locale.utcDateToShortDateLocalized(nowUtc),
|
||||
PDFTime: window.$gz.locale.utcDateToShortTimeLocalized(nowUtc)
|
||||
};
|
||||
},
|
||||
///////////////////////////////////
|
||||
// FETCH BIZ OBJECT NAME
|
||||
//
|
||||
//
|
||||
async fetchBizObjectName(sockType, objectId) {
|
||||
const res = await this.get(`name/${sockType}/${objectId}`);
|
||||
//We never expect there to be no data here
|
||||
if (!Object.prototype.hasOwnProperty.call(res, "data")) {
|
||||
return Promise.reject(res);
|
||||
} else {
|
||||
return res.data;
|
||||
}
|
||||
}
|
||||
//---------------
|
||||
|
||||
//new functions above here
|
||||
};
|
||||
188
client/src/api/gzdialog.js
Normal file
@@ -0,0 +1,188 @@
|
||||
let VM_LOCAL = null;
|
||||
|
||||
//Calculate a reasonable time to show the alert based on the size of the message and some sane bounds
|
||||
//https://ux.stackexchange.com/a/85898
|
||||
function CalculateDelay(msg) {
|
||||
//Min 2 seconds max 8 seconds
|
||||
return Math.min(Math.max(msg.length * 50, 3000), 8000);
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
// Dialog, toast, notification
|
||||
// utils and handlers
|
||||
//
|
||||
export default {
|
||||
///////////////////////////////////
|
||||
// WIRE UP DIALOG EVENTS
|
||||
//
|
||||
// called once from app.vue only
|
||||
//
|
||||
wireUpEventHandlers(vm) {
|
||||
//###########################################
|
||||
//Notifications: pops up and slowly disappears
|
||||
//ACTUAL UI IN gznotify.vue
|
||||
//###########################################
|
||||
|
||||
///////////
|
||||
//ERROR
|
||||
window.$gz.eventBus.$on("notify-error", function handleNotifyWarn(
|
||||
msg,
|
||||
helpUrl
|
||||
) {
|
||||
//log full message
|
||||
window.$gz.store.commit("logItem", "notify-error: " + msg);
|
||||
//trim really long message as it's likely useless beyond the first few lines (stack trace etc)
|
||||
msg = msg.substring(0, 600);
|
||||
vm.$root.$gznotify({
|
||||
message: msg,
|
||||
type: "error",
|
||||
timeout: CalculateDelay(msg),
|
||||
helpUrl: helpUrl
|
||||
});
|
||||
});
|
||||
|
||||
///////////
|
||||
//WARNING
|
||||
window.$gz.eventBus.$on("notify-warning", function handleNotifyWarn(
|
||||
msg,
|
||||
helpUrl
|
||||
) {
|
||||
window.$gz.store.commit("logItem", "notify-warning: " + msg);
|
||||
msg = msg.substring(0, 600);
|
||||
vm.$root.$gznotify({
|
||||
message: msg,
|
||||
type: "warning",
|
||||
timeout: CalculateDelay(msg),
|
||||
helpUrl: helpUrl
|
||||
});
|
||||
});
|
||||
|
||||
///////////
|
||||
//INFO
|
||||
window.$gz.eventBus.$on("notify-info", function handleNotifyInfo(
|
||||
msg,
|
||||
helpUrl
|
||||
) {
|
||||
window.$gz.store.commit("logItem", "notify-info: " + msg);
|
||||
msg = msg.substring(0, 600);
|
||||
vm.$root.$gznotify({
|
||||
message: msg,
|
||||
type: "info",
|
||||
timeout: CalculateDelay(msg),
|
||||
helpUrl: helpUrl
|
||||
});
|
||||
});
|
||||
|
||||
///////////
|
||||
//SUCCESS
|
||||
window.$gz.eventBus.$on("notify-success", function handleNotifySuccess(
|
||||
msg,
|
||||
helpUrl
|
||||
) {
|
||||
vm.$root.$gznotify({
|
||||
message: msg,
|
||||
type: "success",
|
||||
timeout: CalculateDelay(msg),
|
||||
helpUrl: helpUrl
|
||||
});
|
||||
});
|
||||
|
||||
VM_LOCAL = vm;
|
||||
},
|
||||
//###########################################
|
||||
//CONFIRMATION DIALOGS
|
||||
//ACTUAL UI IN gzconfirm.vue
|
||||
//###########################################
|
||||
/////////////////////////////////////
|
||||
// Are you sure you want to delete?
|
||||
//
|
||||
confirmDelete() {
|
||||
return VM_LOCAL.$root.$gzconfirm({
|
||||
message: window.$gz.translation.get("DeletePrompt"),
|
||||
yesButtonText: window.$gz.translation.get("Delete"),
|
||||
noButtonText: window.$gz.translation.get("Cancel"),
|
||||
type: "warning"
|
||||
});
|
||||
},
|
||||
/////////////////////////////////////
|
||||
// Are you sure you want to leave unsaved?
|
||||
//
|
||||
confirmLeaveUnsaved() {
|
||||
return VM_LOCAL.$root.$gzconfirm({
|
||||
message: window.$gz.translation.get("AreYouSureUnsavedChanges"),
|
||||
yesButtonText: window.$gz.translation.get("Leave"),
|
||||
noButtonText: window.$gz.translation.get("Cancel"),
|
||||
type: "warning"
|
||||
});
|
||||
},
|
||||
/////////////////////////////////////
|
||||
// Display LT message with wait for ok
|
||||
//
|
||||
displayLTErrorMessage(tKeyText, tKeyTitle = undefined) {
|
||||
return VM_LOCAL.$root.$gzconfirm({
|
||||
message: tKeyText ? window.$gz.translation.get(tKeyText) : "",
|
||||
title: tKeyTitle ? window.$gz.translation.get(tKeyTitle) : "",
|
||||
yesButtonText: window.$gz.translation.get("OK"),
|
||||
type: "error"
|
||||
});
|
||||
},
|
||||
/////////////////////////////////////
|
||||
// Display LT message with wait for ok
|
||||
//
|
||||
displayLTModalNotificationMessage(
|
||||
tKeyText,
|
||||
tKeyTitle = undefined,
|
||||
ttype = "info",
|
||||
tHelpUrl = undefined
|
||||
) {
|
||||
return VM_LOCAL.$root.$gzconfirm({
|
||||
message: tKeyText ? window.$gz.translation.get(tKeyText) : "",
|
||||
title: tKeyTitle ? window.$gz.translation.get(tKeyTitle) : "",
|
||||
yesButtonText: window.$gz.translation.get("OK"),
|
||||
type: ttype,
|
||||
helpUrl: tHelpUrl
|
||||
});
|
||||
},
|
||||
/////////////////////////////////////
|
||||
// Custom confirmation
|
||||
//
|
||||
confirmGeneric(tKey, ttype = "info") {
|
||||
return VM_LOCAL.$root.$gzconfirm({
|
||||
message: window.$gz.translation.get(tKey),
|
||||
yesButtonText: window.$gz.translation.get("OK"),
|
||||
noButtonText: window.$gz.translation.get("Cancel"),
|
||||
type: ttype
|
||||
});
|
||||
},
|
||||
/////////////////////////////////////
|
||||
// Custom confirmation pre-translated
|
||||
//
|
||||
confirmGenericPreTranslated(msg, ttype = "info") {
|
||||
return VM_LOCAL.$root.$gzconfirm({
|
||||
message: msg,
|
||||
yesButtonText: window.$gz.translation.get("OK"),
|
||||
noButtonText: window.$gz.translation.get("Cancel"),
|
||||
type: ttype
|
||||
});
|
||||
},
|
||||
/////////////////////////////////////
|
||||
// Custom confirmation no translation
|
||||
// with all options available
|
||||
//
|
||||
displayNoTranslationModalNotificationMessage(
|
||||
tKeyText,
|
||||
tKeyTitle = undefined,
|
||||
ttype = "info",
|
||||
tHelpUrl = undefined
|
||||
) {
|
||||
return VM_LOCAL.$root.$gzconfirm({
|
||||
message: tKeyText,
|
||||
title: tKeyTitle,
|
||||
yesButtonText: window.$gz.translation.get("OK"),
|
||||
type: ttype,
|
||||
helpUrl: tHelpUrl
|
||||
});
|
||||
}
|
||||
|
||||
//new functions above here
|
||||
};
|
||||
1002
client/src/api/gzform.js
Normal file
438
client/src/api/gzmenu.js
Normal file
@@ -0,0 +1,438 @@
|
||||
/////////////////////////////////
|
||||
// Menu utils and handlers
|
||||
//
|
||||
export default {
|
||||
///////////////////////////////////////////
|
||||
// TECH SUPPORT / CONTACT FORUM URL
|
||||
//
|
||||
contactSupportUrl() {
|
||||
const dbId = encodeURIComponent(
|
||||
window.$gz.store.state.globalSettings.serverDbId
|
||||
);
|
||||
const company = encodeURIComponent(
|
||||
window.$gz.store.state.globalSettings.company
|
||||
);
|
||||
return `https://contact.ayanova.com/contact?dbid=${dbId}&company=${company}`;
|
||||
},
|
||||
///////////////////////////////
|
||||
// CHANGE HANDLER
|
||||
//
|
||||
// Deal with a menu change request
|
||||
// called from App.vue
|
||||
handleMenuChange(vm, ctx) {
|
||||
const UTILITY_TYPES = [
|
||||
window.$gz.type.NoType,
|
||||
window.$gz.type.Global,
|
||||
window.$gz.type.NoType,
|
||||
window.$gz.type.ServerState,
|
||||
window.$gz.type.License,
|
||||
window.$gz.type.LogFile,
|
||||
window.$gz.type.ServerJob,
|
||||
window.$gz.type.TrialSeeder,
|
||||
window.$gz.type.ServerMetrics,
|
||||
window.$gz.type.UserOptions,
|
||||
window.$gz.type.FormCustom,
|
||||
window.$gz.type.DataListSavedFilter,
|
||||
window.$gz.type.GlobalOps,
|
||||
window.$gz.type.BizMetrics,
|
||||
window.$gz.type.Backup,
|
||||
window.$gz.type.Notification,
|
||||
window.$gz.type.NotifySubscription
|
||||
];
|
||||
|
||||
vm.appBar.isMain = ctx.isMain;
|
||||
vm.appBar.icon = ctx.icon;
|
||||
|
||||
vm.appBar.title = ""; //this prevents fou[translated]c
|
||||
vm.appBar.readOnly = ctx.readOnly;
|
||||
|
||||
if (ctx.readOnly === true) {
|
||||
vm.appBar.color = "readonlybanner";
|
||||
} else {
|
||||
vm.appBar.color = ctx.isMain ? "primary" : "secondary";
|
||||
}
|
||||
|
||||
//ctx.title if set is a Translation key
|
||||
//ctx.formData.recordName is the object name or serial number or whatever identifies it uniquely
|
||||
let recordName = "";
|
||||
if (
|
||||
ctx &&
|
||||
ctx.formData &&
|
||||
ctx.formData.recordName &&
|
||||
ctx.formData.recordName != "null" //some forms (part) present "null" as the record name due to attempts to format a name so if that's the case just turn it into null here to bypass
|
||||
) {
|
||||
recordName = ctx.formData.recordName;
|
||||
}
|
||||
const hasRecordName = !window.$gz.util.stringIsNullOrEmpty(recordName);
|
||||
if (ctx.title) {
|
||||
//it has a title translation key
|
||||
const translatedTitle = vm.$sock.t(ctx.title);
|
||||
if (hasRecordName) {
|
||||
//recordname takes all precedence in AppBar in order to conserve space (narrow view etc)
|
||||
//also it just looks cleaner, the icon is already there to indicate where the user is at
|
||||
vm.appBar.title = recordName;
|
||||
document.title = `${recordName} - ${translatedTitle} Sockeye `;
|
||||
} else {
|
||||
vm.appBar.title = translatedTitle;
|
||||
document.title = `${translatedTitle} ${recordName}`;
|
||||
}
|
||||
} else {
|
||||
if (hasRecordName) {
|
||||
//not title but has record name
|
||||
vm.appBar.title = recordName;
|
||||
document.title = `${recordName} Sockeye`;
|
||||
} else {
|
||||
document.title = "Sockeye";
|
||||
}
|
||||
}
|
||||
|
||||
//Parse the formdata if present
|
||||
//FORMDATA is OPTIONAL and only required for forms that need to allow
|
||||
//viewing object history, attachments, custom fields, etc that kind of thing
|
||||
//usually CORE objects with an id, NOT utility type forms
|
||||
let formSockType = 0;
|
||||
let formRecordId = 0;
|
||||
if (ctx.formData) {
|
||||
if (ctx.formData.sockType != null) {
|
||||
formSockType = ctx.formData.sockType;
|
||||
}
|
||||
if (ctx.formData.recordId != null) {
|
||||
formRecordId = ctx.formData.recordId;
|
||||
}
|
||||
}
|
||||
|
||||
//flag for if it's wikiable, reviewable, attachable, searchable, historical
|
||||
const isCoreBizObject = formSockType != 0 && formRecordId != 0;
|
||||
|
||||
//set the help url if presented or default to the User section intro
|
||||
vm.appBar.helpUrl = ctx.helpUrl ? ctx.helpUrl : "user-intro";
|
||||
|
||||
vm.appBar.menuItems = [];
|
||||
|
||||
//CONTEXT TOP PORTION
|
||||
//populate the context portion of the menu so handle accordingly
|
||||
if (ctx.menuItems) {
|
||||
vm.appBar.menuItems = ctx.menuItems;
|
||||
}
|
||||
|
||||
//STANDARD BIZ OBJECT OPTIONS
|
||||
//NOTE: This applies equally to all core business object types that are basically real world and have an id and a type (all are wikiable, attachable and reviewable)
|
||||
//Not utility type objects like datalist etc
|
||||
//there will be few exceptions so they will be coded in later if needed but assume anything with an id and a type
|
||||
if (isCoreBizObject && !ctx.hideCoreBizStandardOptions) {
|
||||
//"Review" was follow up type of schedule marker
|
||||
//basically it's now a "Reminder" type of object but it's own thing with separate collection
|
||||
|
||||
vm.appBar.menuItems.push({
|
||||
title: "Review",
|
||||
icon: "$sockiCalendarCheck",
|
||||
key: "app:review",
|
||||
data: {
|
||||
sockType: formSockType,
|
||||
recordId: formRecordId,
|
||||
recordName: recordName
|
||||
}
|
||||
});
|
||||
|
||||
//AFAIK right now any item with an id and a type can have a history
|
||||
//anything not would be the exception rather than the rule
|
||||
vm.appBar.menuItems.push({
|
||||
title: "History",
|
||||
icon: "$sockiHistory",
|
||||
key: "app:history",
|
||||
data: { sockType: formSockType, recordId: formRecordId }
|
||||
});
|
||||
}
|
||||
|
||||
//CUSTOMIZE
|
||||
//set custom fields and link to translation text editor
|
||||
|
||||
if (
|
||||
isCoreBizObject &&
|
||||
ctx.formData &&
|
||||
ctx.formData.formCustomTemplateKey != undefined &&
|
||||
window.$gz.role.hasRole([
|
||||
window.$gz.role.AUTHORIZATION_ROLES.BizAdmin,
|
||||
window.$gz.role.AUTHORIZATION_ROLES.BizAdminRestricted
|
||||
])
|
||||
) {
|
||||
//NOTE: BizAdmin can edit, BizAdminRestricted can read only
|
||||
//add customize menu item
|
||||
|
||||
//customize
|
||||
vm.appBar.menuItems.push({
|
||||
title: "Customize",
|
||||
icon: "$sockiCustomize",
|
||||
data: ctx.formData.formCustomTemplateKey,
|
||||
key: "app:customize"
|
||||
});
|
||||
}
|
||||
|
||||
//GLOBAL BOTTOM PORTION
|
||||
|
||||
//SEARCH
|
||||
//all forms except the search form
|
||||
if (!ctx.hideSearch && !UTILITY_TYPES.includes(formSockType)) {
|
||||
//For all forms but not on the search form itself; if this is necessary for others then make a nosearch or something flag controlled by incoming ctx but if not then this should suffice
|
||||
vm.appBar.menuItems.push({
|
||||
title: "Search",
|
||||
icon: "$sockiSearch",
|
||||
key: "app:search",
|
||||
data: formSockType
|
||||
});
|
||||
}
|
||||
|
||||
//HELP
|
||||
vm.appBar.menuItems.push({
|
||||
title: "MenuHelp",
|
||||
icon: "$sockiQuestionCircle",
|
||||
key: "app:help",
|
||||
data: vm.appBar.helpUrl
|
||||
});
|
||||
|
||||
//ABOUT
|
||||
if (!isCoreBizObject && ctx.helpUrl != "sock-about") {
|
||||
vm.appBar.menuItems.push({
|
||||
title: "HelpAboutSockeye",
|
||||
icon: "$sockiInfoCircle",
|
||||
key: "app:nav:abt",
|
||||
data: "sock-about"
|
||||
});
|
||||
}
|
||||
},
|
||||
//Unused to date of beta 0.9
|
||||
// ///////////////////////////////
|
||||
// // CHANGE HANDLER
|
||||
// //
|
||||
// // Deal with a menu item update request
|
||||
// // called from App.vue
|
||||
// handleReplaceMenuItem(vm, newItem) {
|
||||
// if (!vm.appBar.menuItems || !newItem) {
|
||||
// return;
|
||||
// }
|
||||
// //Find the key that is in the collection and replace it
|
||||
// for (let i = 0; i < vm.appBar.menuItems.length; i++) {
|
||||
// if (vm.appBar.menuItems[i].key == newItem.key) {
|
||||
// //NOTE: since we are adding a new object, it has no reactivity in it so we need to use the Vue.Set to set it which
|
||||
// //automatically adds the setters and getters that trigger reactivity
|
||||
// //If it was set directly on the array it wouldn't update the UI
|
||||
// vm.$set(vm.appBar.menuItems, i, newItem);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
//////////////////////////////////////////////
|
||||
// LAST REPORT CHANGE HANDLER
|
||||
// update / add last report menu item
|
||||
//
|
||||
handleUpsertLastReport(vm, newItem) {
|
||||
if (!vm.appBar.menuItems || !newItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
window.$gz.eventBus.$emit("menu-upsert-last-report", {
|
||||
title: reportSelected.name,
|
||||
notrans: true,
|
||||
icon: "$sockiFileAlt",
|
||||
key: formKey + ":report:" + reportSelected.id,
|
||||
vm: vm
|
||||
});
|
||||
*/
|
||||
let key = null;
|
||||
//Find the last report key and update it if present
|
||||
for (let i = 0; i < vm.appBar.menuItems.length; i++) {
|
||||
key = vm.appBar.menuItems[i].key;
|
||||
if (key && key.includes(":report:")) {
|
||||
vm.appBar.menuItems[i].key = newItem.key;
|
||||
vm.appBar.menuItems[i].title = newItem.title;
|
||||
return;
|
||||
}
|
||||
}
|
||||
//No prior last report so slot it in under the report one
|
||||
for (let i = 0; i < vm.appBar.menuItems.length; i++) {
|
||||
key = vm.appBar.menuItems[i].key;
|
||||
if (key && key.endsWith(":report")) {
|
||||
vm.appBar.menuItems.splice(i + 1, 0, newItem);
|
||||
}
|
||||
}
|
||||
},
|
||||
///////////////////////////////
|
||||
// ENABLE / DISABLE HANDLER
|
||||
//
|
||||
// Deal with a menu item enable / disable
|
||||
// called from App.vue
|
||||
handleDisableMenuItem(vm, key, disabled) {
|
||||
if (!vm.appBar.menuItems || !key) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Find the menu item and set it to disabled and recolor it to disabled color and return
|
||||
for (let i = 0; i < vm.appBar.menuItems.length; i++) {
|
||||
const menuItem = vm.appBar.menuItems[i];
|
||||
if (menuItem.key == key) {
|
||||
vm.$set(vm.appBar.menuItems[i], "disabled", disabled);
|
||||
//menuItem.disabled = disabled;
|
||||
vm.$set(vm.appBar.menuItems[i], "color", disabled ? "disabled" : "");
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
///////////////////////////////
|
||||
// CHANGE ICON HANDLER
|
||||
// Change icon dymanically
|
||||
// (note, can pass null for new icon to clear it)
|
||||
//
|
||||
handleChangeMenuItemIcon(vm, key, newIcon) {
|
||||
if (!vm.appBar.menuItems || !key) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Find the menu item and change it's icon
|
||||
for (let i = 0; i < vm.appBar.menuItems.length; i++) {
|
||||
const menuItem = vm.appBar.menuItems[i];
|
||||
if (menuItem.key == key) {
|
||||
vm.$set(vm.appBar.menuItems[i], "icon", newIcon);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
///////////////////////////////
|
||||
// APP (GLOBAL) CLICK HANDLER
|
||||
//
|
||||
// Deal with a menu change request
|
||||
// called from App.vue
|
||||
handleAppClick(vm, menuItem) {
|
||||
//Key will start with the string "app:" if it's a global application command that should be handled here,
|
||||
//otherwise it's a local command for a local form only
|
||||
//If there is any extended information required for the command it will be in the data property of the menu item
|
||||
//split a key into component parts, part one is the responsible party, part two is the command, part three only exists to make it unique if necessary
|
||||
//each part is separated by a colon
|
||||
|
||||
//Handle different items
|
||||
const item = this.parseMenuItem(menuItem);
|
||||
if (!item.disabled && item.owner == "app") {
|
||||
switch (item.key) {
|
||||
case "help":
|
||||
if (item.data.includes("~customer~")) {
|
||||
window.open(
|
||||
window.$gz.api.helpUrlCustomer() +
|
||||
item.data.replace("~customer~", ""),
|
||||
"_blank"
|
||||
);
|
||||
} else {
|
||||
window.open(window.$gz.api.helpUrl() + item.data, "_blank");
|
||||
}
|
||||
break;
|
||||
|
||||
case "search":
|
||||
vm.$router.push({
|
||||
name: "home-search",
|
||||
params: { socktype: item.data }
|
||||
});
|
||||
break;
|
||||
case "review":
|
||||
//go to list
|
||||
// path: "/home-reviews/:aType?/:objectId?",
|
||||
vm.$router.push({
|
||||
name: "home-reviews",
|
||||
params: {
|
||||
aType: window.$gz.util.stringToIntOrNull(item.data.sockType),
|
||||
objectId: window.$gz.util.stringToIntOrNull(item.data.recordId),
|
||||
name: item.data.recordName
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "history":
|
||||
vm.$router.push({
|
||||
name: "sock-history",
|
||||
params: {
|
||||
socktype: item.data.sockType,
|
||||
recordid: item.data.recordId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "customize":
|
||||
vm.$router.push({
|
||||
name: "sock-customize",
|
||||
params: { formCustomTemplateKey: item.data }
|
||||
});
|
||||
break;
|
||||
case "nav":
|
||||
vm.$router.push({ name: item.data });
|
||||
break;
|
||||
default:
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-warning",
|
||||
"gzmenu:handleAppClick - unrecognized command [" +
|
||||
menuItem.key +
|
||||
"]"
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
///////////////////////////////
|
||||
// PARSE MENU ITEM CLICK
|
||||
//
|
||||
// parse out the parts of a
|
||||
// menu item from a click event
|
||||
//
|
||||
parseMenuItem(menuItem) {
|
||||
//format is "AREA:KEY:UNIQUEID"
|
||||
//and data is in data portion
|
||||
const keyparts = menuItem.key.split(":");
|
||||
const ret = {
|
||||
owner: keyparts[0],
|
||||
key: keyparts[1],
|
||||
data: menuItem.data,
|
||||
disabled: menuItem.disabled,
|
||||
vm: menuItem.vm ? menuItem.vm : null
|
||||
};
|
||||
if (keyparts.length > 2) {
|
||||
ret.id = keyparts[2];
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
///////////////////////////////////
|
||||
// WIRE UP MENU EVENTS
|
||||
//
|
||||
// called once from app.vue only
|
||||
//
|
||||
wireUpEventHandlers(vm) {
|
||||
const that = this;
|
||||
window.$gz.eventBus.$on("menu-change", function handleMenuChange(ctx) {
|
||||
that.handleMenuChange(vm, ctx);
|
||||
});
|
||||
|
||||
window.$gz.eventBus.$on(
|
||||
"menu-upsert-last-report",
|
||||
function handleUpsertLastReport(newItem) {
|
||||
that.handleUpsertLastReport(vm, newItem);
|
||||
}
|
||||
);
|
||||
|
||||
window.$gz.eventBus.$on("menu-disable-item", function handleDisableMenuItem(
|
||||
key
|
||||
) {
|
||||
that.handleDisableMenuItem(vm, key, true);
|
||||
});
|
||||
|
||||
window.$gz.eventBus.$on("menu-enable-item", function handleDisableMenuItem(
|
||||
key
|
||||
) {
|
||||
that.handleDisableMenuItem(vm, key, false);
|
||||
});
|
||||
|
||||
window.$gz.eventBus.$on(
|
||||
"menu-change-item-icon",
|
||||
function handleChangeMenuItemIcon(key, newIcon) {
|
||||
that.handleChangeMenuItemIcon(vm, key, newIcon);
|
||||
}
|
||||
);
|
||||
|
||||
window.$gz.eventBus.$on("menu-click", function handleMenuClick(menuitem) {
|
||||
that.handleAppClick(vm, menuitem);
|
||||
});
|
||||
}
|
||||
//new functions above here
|
||||
};
|
||||
916
client/src/api/gzutil.js
Normal file
@@ -0,0 +1,916 @@
|
||||
/////////////////////////////////
|
||||
// General utility library
|
||||
//
|
||||
|
||||
const icons = {
|
||||
image: "$sockiFileImage",
|
||||
pdf: "$sockiFilePdf",
|
||||
word: "$sockiFileWord",
|
||||
powerpoint: "$sockiFilePowerpoint",
|
||||
excel: "$sockiFileExcel",
|
||||
csv: "$sockiFileCsv",
|
||||
audio: "$sockiFileAudio",
|
||||
video: "$sockiFileVidio",
|
||||
archive: "$sockiFileArchive",
|
||||
code: "$sockiFileCode",
|
||||
text: "$sockiFileAlt",
|
||||
file: "$sockiFile"
|
||||
};
|
||||
const mimeTypes = {
|
||||
"image/gif": icons.image,
|
||||
"image/jpeg": icons.image,
|
||||
"image/png": icons.image,
|
||||
"image/webp": icons.image,
|
||||
|
||||
"application/pdf": icons.pdf,
|
||||
|
||||
"application/msword": icons.word,
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||
icons.word,
|
||||
|
||||
"application/mspowerpoint": icons.powerpoint,
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
||||
icons.powerpoint,
|
||||
|
||||
"application/msexcel": icons.excel,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
|
||||
icons.excel,
|
||||
|
||||
"text/csv": icons.csv,
|
||||
|
||||
"audio/aac": icons.audio,
|
||||
"audio/wav": icons.audio,
|
||||
"audio/mpeg": icons.audio,
|
||||
"audio/mp4": icons.audio,
|
||||
"audio/ogg": icons.audio,
|
||||
|
||||
"video/x-msvideo": icons.video,
|
||||
"video/mpeg": icons.video,
|
||||
"video/mp4": icons.video,
|
||||
"video/ogg": icons.video,
|
||||
"video/quicktime": icons.video,
|
||||
"video/webm": icons.video,
|
||||
|
||||
"application/gzip": icons.archive,
|
||||
"application/zip": icons.archive,
|
||||
"application/x-tar": icons.archive,
|
||||
|
||||
"text/css": icons.code,
|
||||
"text/html": icons.code,
|
||||
"text/javascript": icons.code,
|
||||
"application/javascript": icons.code,
|
||||
|
||||
"text/plain": icons.text,
|
||||
"text/richtext": icons.text,
|
||||
"text/rtf": icons.text,
|
||||
"application/rtf": icons.text,
|
||||
"application/json": icons.text
|
||||
};
|
||||
|
||||
const extensions = {
|
||||
gif: icons.image,
|
||||
jpeg: icons.image,
|
||||
jpg: icons.image,
|
||||
png: icons.image,
|
||||
webp: icons.image,
|
||||
|
||||
pdf: icons.pdf,
|
||||
|
||||
doc: icons.word,
|
||||
docx: icons.word,
|
||||
|
||||
ppt: icons.powerpoint,
|
||||
pptx: icons.powerpoint,
|
||||
|
||||
xls: icons.excel,
|
||||
xlsx: icons.excel,
|
||||
|
||||
csv: icons.csv,
|
||||
|
||||
aac: icons.audio,
|
||||
mp3: icons.audio,
|
||||
ogg: icons.audio,
|
||||
|
||||
avi: icons.video,
|
||||
flv: icons.video,
|
||||
mkv: icons.video,
|
||||
mp4: icons.video,
|
||||
|
||||
gz: icons.archive,
|
||||
zip: icons.archive,
|
||||
tar: icons.archive,
|
||||
"7z": icons.archive,
|
||||
|
||||
css: icons.code,
|
||||
html: icons.code,
|
||||
js: icons.code,
|
||||
|
||||
txt: icons.text,
|
||||
json: icons.text,
|
||||
rtf: icons.text
|
||||
};
|
||||
|
||||
export default {
|
||||
///////////////////////////////
|
||||
// CLEAN OBJECT
|
||||
// Clear all properties from object without resorting to assigning a new object (o={})
|
||||
// which can be problematic in some cases (IE bugs, watched data items in forms etc)
|
||||
|
||||
removeAllPropertiesFromObject: function(o) {
|
||||
for (let variableKey in o) {
|
||||
if (Object.prototype.hasOwnProperty.call(o, variableKey)) {
|
||||
delete o[variableKey];
|
||||
}
|
||||
}
|
||||
},
|
||||
///////////////////////////////
|
||||
// DEEP COPY FOR API UPDATE
|
||||
// Deep copy an object skipping all *Viz and named properties from object
|
||||
//
|
||||
deepCopySkip: function(source, skipNames) {
|
||||
if (skipNames == null) {
|
||||
skipNames = [];
|
||||
}
|
||||
let o = {};
|
||||
for (let key in source) {
|
||||
if (
|
||||
!key.endsWith("Viz") &&
|
||||
!skipNames.some(x => x == key) &&
|
||||
Object.prototype.hasOwnProperty.call(source, key)
|
||||
) {
|
||||
o[key] = source[key];
|
||||
}
|
||||
}
|
||||
return o;
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy a string to clipboard
|
||||
* @param {String} string The string to be copied to clipboard
|
||||
* @return {Boolean} returns a boolean correspondent to the success of the copy operation.
|
||||
* Modified from an example here: https://stackoverflow.com/a/53951634/8939
|
||||
* Basically a fallback if navigator.clipboard is not available
|
||||
*/
|
||||
copyToClipboard: function(string) {
|
||||
let textarea;
|
||||
let result;
|
||||
|
||||
if (navigator && navigator.clipboard) {
|
||||
navigator.clipboard.writeText(string);
|
||||
} else {
|
||||
try {
|
||||
textarea = document.createElement("textarea");
|
||||
textarea.setAttribute("readonly", true);
|
||||
textarea.setAttribute("contenteditable", true);
|
||||
textarea.style.position = "fixed"; // prevent scroll from jumping to the bottom when focus is set.
|
||||
textarea.value = string;
|
||||
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(textarea);
|
||||
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
|
||||
textarea.setSelectionRange(0, textarea.value.length);
|
||||
result = document.execCommand("copy");
|
||||
} catch (err) {
|
||||
result = null;
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
// manual copy fallback using prompt
|
||||
if (!result) {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||
const copyHotkey = isMac ? "⌘C" : "CTRL+C";
|
||||
result = prompt(`Press ${copyHotkey}`, string);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
///////////////////////////////
|
||||
// ROUNDING
|
||||
// //https://medium.com/swlh/how-to-round-to-a-certain-number-of-decimal-places-in-javascript-ed74c471c1b8
|
||||
roundAccurately: function(number, decimalPlaces) {
|
||||
if (!number || number == 0 || Number.isNaN(number)) {
|
||||
return number;
|
||||
}
|
||||
const wasNegative = number < 0;
|
||||
if (wasNegative) {
|
||||
number = Math.abs(number); //make sure it's positive because rounding negative numbers is weird in JS
|
||||
}
|
||||
number = Number(
|
||||
Math.round(number + "e" + decimalPlaces) + "e-" + decimalPlaces
|
||||
);
|
||||
if (wasNegative) {
|
||||
number = 0 - number;
|
||||
}
|
||||
return number;
|
||||
},
|
||||
///////////////////////////////
|
||||
// CLEAN TAG NAME
|
||||
// Clean up a tag with same rules as server
|
||||
//
|
||||
normalizeTag: function(tagName) {
|
||||
if (!tagName || tagName == "") {
|
||||
return null;
|
||||
}
|
||||
tagName = tagName.toLowerCase();
|
||||
|
||||
//spaces to dashes
|
||||
tagName = tagName.replace(/ /gi, "-");
|
||||
|
||||
//multiple dashes to single dashes
|
||||
tagName = tagName.replace(/-+/g, "-");
|
||||
|
||||
//ensure doesn't start or end with a dash
|
||||
tagName = this.trimSpecific(tagName, "-");
|
||||
|
||||
//No longer than 255 characters
|
||||
tagName = tagName.length > 255 ? tagName.substr(0, 255 - 1) : tagName;
|
||||
|
||||
return tagName;
|
||||
},
|
||||
///////////////////////////////
|
||||
// Quick hash for trivial purposes
|
||||
// not cryptographic
|
||||
// https://stackoverflow.com/a/7616484/8939
|
||||
//
|
||||
quickHash: function(theString) {
|
||||
let hash = 0;
|
||||
let i;
|
||||
let chr;
|
||||
if (theString.length === 0) return hash;
|
||||
for (i = 0; i < theString.length; i++) {
|
||||
chr = theString.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
},
|
||||
|
||||
////////////////////////////////////////
|
||||
// Random password / login generator
|
||||
// https://stackoverflow.com/a/51540480/8939
|
||||
// using 32 character (128 bit) as default
|
||||
//
|
||||
getRandomPassword: function() {
|
||||
const wishlist = "0123456789abcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
return Array.from(crypto.getRandomValues(new Uint32Array(32)))
|
||||
.map(x => wishlist[x % wishlist.length])
|
||||
.join("");
|
||||
},
|
||||
///////////////////////////////
|
||||
// CONVERT STRING TO BOOLEAN
|
||||
// https://stackoverflow.com/a/1414175/8939
|
||||
//
|
||||
stringToBoolean: function(string) {
|
||||
switch (string.toLowerCase().trim()) {
|
||||
case "true":
|
||||
case "yes":
|
||||
case "1":
|
||||
return true;
|
||||
case "false":
|
||||
case "no":
|
||||
case "0":
|
||||
case null:
|
||||
return false;
|
||||
default:
|
||||
return Boolean(string);
|
||||
}
|
||||
}, ///////////////////////////////
|
||||
// CONVERT STRING TO FLOAT
|
||||
// https://stackoverflow.com/a/9409894/8939
|
||||
//
|
||||
stringToFloat: function(string) {
|
||||
//null or empty then zero
|
||||
if (!string) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
//A number already then parse and return
|
||||
if (this.isNumeric(string)) {
|
||||
if (Number.isNaN(string)) {
|
||||
return 0;
|
||||
}
|
||||
return parseFloat(string);
|
||||
}
|
||||
|
||||
//Not a string at all?
|
||||
if (!this.isString(string)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ret = parseFloat(string.replace(/[^\d.-]/g, ""));
|
||||
if (Number.isNaN(ret)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
///////////////////////////////
|
||||
// Is negative number
|
||||
//
|
||||
//
|
||||
isNegative: function(v) {
|
||||
//null or empty then zero
|
||||
if (!v || v == 0 || Number.isNaN(v)) {
|
||||
return false;
|
||||
}
|
||||
return parseFloat(v) < 0;
|
||||
},
|
||||
///////////////////////////////
|
||||
// Splice a string
|
||||
//changes the content of a string by removing a range of
|
||||
// characters and/or adding new characters.
|
||||
//
|
||||
// @param {String} source string
|
||||
// @param {number} start Index at which to start changing the string.
|
||||
// @param {number} delCount An integer indicating the number of old chars to remove.
|
||||
// @param {string} newSubStr The String that is spliced in.
|
||||
// @return {string} A new string with the spliced substring.
|
||||
stringSplice: function(source, start, delCount, newSubStr) {
|
||||
if (source == null || source == "") {
|
||||
if (newSubStr) {
|
||||
return newSubStr;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
source.slice(0, start) +
|
||||
newSubStr +
|
||||
source.slice(start + Math.abs(delCount))
|
||||
);
|
||||
},
|
||||
///////////////////////////////
|
||||
// Truncate a string
|
||||
//truncates and adds ellipses
|
||||
//
|
||||
// @param {String} source string
|
||||
// @param {number} length desired
|
||||
// @return {string} A new string truncated with ellipses at end
|
||||
truncateString: function(s, len) {
|
||||
if (this.stringIsNullOrEmpty(s)) {
|
||||
return s;
|
||||
}
|
||||
if (s.length > len) {
|
||||
return s.substring(0, len) + "...";
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
},
|
||||
///////////////////////////////
|
||||
// Format tags for display
|
||||
//
|
||||
//
|
||||
// @param {String} tags raw from server
|
||||
// @return {string} A new string with the tags formatted or an empty string if no tags
|
||||
formatTags: function(tags) {
|
||||
if (tags && tags.length > 0) {
|
||||
return tags.join(", ");
|
||||
}
|
||||
return "";
|
||||
},
|
||||
///////////////////////////////
|
||||
// ICON FOR *ALL* OBJECT TYPES
|
||||
//(used for search results and event log / history)
|
||||
//NOTE: Any object type could appear in event log, they all need to be supported where possible
|
||||
//CoreBizObject add here
|
||||
iconForType: function(sockType) {
|
||||
switch (sockType) {
|
||||
case window.$gz.type.NoType:
|
||||
case null:
|
||||
return "$sockiGenderless";
|
||||
case window.$gz.type.Global:
|
||||
return "$sockiGlobe";
|
||||
case window.$gz.type.User:
|
||||
return "$sockiUser";
|
||||
case window.$gz.type.ServerState:
|
||||
return "$sockiDoorOpen";
|
||||
|
||||
case window.$gz.type.LogFile:
|
||||
return "$sockiGlasses";
|
||||
case window.$gz.type.PickListTemplate:
|
||||
return "$sockiPencilRuler";
|
||||
case window.$gz.type.Customer:
|
||||
return "$sockiAddressCard";
|
||||
case window.$gz.type.ServerJob:
|
||||
return "$sockiRobot";
|
||||
|
||||
case window.$gz.type.Metrics:
|
||||
return "$sockiFileMedicalAlt";
|
||||
case window.$gz.type.Translation:
|
||||
return "$sockiLanguage";
|
||||
case window.$gz.type.UserOptions:
|
||||
return "$sockiUserCog";
|
||||
case window.$gz.type.HeadOffice:
|
||||
return "$sockiSitemap";
|
||||
|
||||
case window.$gz.type.FileAttachment:
|
||||
return "$sockiPaperclip";
|
||||
case window.$gz.type.DataListSavedFilter:
|
||||
return "$sockiFilter";
|
||||
case window.$gz.type.FormCustom:
|
||||
return "$sockiCustomize";
|
||||
|
||||
case window.$gz.type.Backup:
|
||||
return "$sockiFileArchive";
|
||||
case window.$gz.type.Notification:
|
||||
return "$sockiBell";
|
||||
case window.$gz.type.NotifySubscription:
|
||||
return "$sockiBullhorn";
|
||||
case window.$gz.type.Reminder:
|
||||
return "$sockiStickyNote";
|
||||
|
||||
case window.$gz.type.OpsNotificationSettings:
|
||||
return "$sockiBullhorn";
|
||||
case window.$gz.type.Report:
|
||||
return "$sockiThList";
|
||||
case window.$gz.type.DashboardView:
|
||||
return "$sockiTachometer";
|
||||
case window.$gz.type.CustomerNote:
|
||||
return "$sockiClipboard";
|
||||
case window.$gz.type.Memo:
|
||||
return "$sockiInbox";
|
||||
case window.$gz.type.Review:
|
||||
return "$sockiCalendarCheck";
|
||||
|
||||
//scroll icon is good one for something
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
//https://gist.github.com/colemanw/9c9a12aae16a4bfe2678de86b661d922
|
||||
iconForFile: function(fileName, mimeType) {
|
||||
// List of official MIME Types: http://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
|
||||
let extension = null;
|
||||
if (fileName && fileName.includes(".")) {
|
||||
extension = fileName.split(".").pop();
|
||||
extension = extension.toLowerCase();
|
||||
}
|
||||
if (!extension && !mimeType) {
|
||||
console.log(
|
||||
"gzutil:iconForFile -> No mime or extension for " +
|
||||
fileName +
|
||||
" " +
|
||||
mimeType
|
||||
);
|
||||
return "$sockiFile";
|
||||
}
|
||||
|
||||
if (!mimeType) {
|
||||
mimeType = "";
|
||||
}
|
||||
mimeType = mimeType.toLowerCase();
|
||||
|
||||
const iconFromExtension = extensions[extension];
|
||||
const iconFromMIME = mimeTypes[mimeType];
|
||||
|
||||
if (iconFromMIME) {
|
||||
return iconFromMIME;
|
||||
}
|
||||
if (iconFromExtension) {
|
||||
return iconFromExtension;
|
||||
}
|
||||
|
||||
return "$sockiFile";
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// attempt to detect image extension name
|
||||
//
|
||||
isImageAttachment: function(fileName, mimeType) {
|
||||
return this.iconForFile(fileName, mimeType) == "$sockiFileImage";
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Sleep async
|
||||
//
|
||||
sleepAsync: function(milliseconds) {
|
||||
// eslint-disable-next-line
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// sortByKey lodash "sortBy" replacement
|
||||
// https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_sortby-and-_orderby
|
||||
//usage:
|
||||
// The native sort modifies the array in place. `_.orderBy` and `_.sortBy` do not, so we use `.concat()` to
|
||||
// copy the array, then sort.
|
||||
// fruits.concat().sort(sortBy("name"));
|
||||
// => [{name:"apple", amount: 4}, {name:"banana", amount: 2}, {name:"mango", amount: 1}, {name:"pineapple", amount: 2}]
|
||||
sortByKey: key => {
|
||||
return (a, b) => {
|
||||
const aaa = a[key].toUpperCase();
|
||||
const bbb = b[key].toUpperCase();
|
||||
return aaa > bbb ? 1 : bbb > aaa ? -1 : 0;
|
||||
//this was the original but it was sorting weird as it was taking case into account with uppercase higher than lowercase
|
||||
//so PMItem came before Part in the object lists
|
||||
//return a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0;
|
||||
};
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// "has" lodash replacement
|
||||
// https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_has
|
||||
//
|
||||
has: function(obj, key) {
|
||||
var keyParts = key.split(".");
|
||||
return (
|
||||
!!obj &&
|
||||
(keyParts.length > 1
|
||||
? this.has(obj[key.split(".")[0]], keyParts.slice(1).join("."))
|
||||
: hasOwnProperty.call(obj, key))
|
||||
);
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Check if object is empty
|
||||
//
|
||||
objectIsEmpty: function(obj) {
|
||||
//https://stackoverflow.com/a/4994265/8939
|
||||
return !obj || Object.keys(obj).length === 0;
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Trim specific character from start and end
|
||||
// https://stackoverflow.com/a/55292366/8939
|
||||
//
|
||||
trimSpecific: function trim(str, ch) {
|
||||
var start = 0;
|
||||
var end = str.length;
|
||||
while (start < end && str[start] === ch) ++start;
|
||||
while (end > start && str[end - 1] === ch) --end;
|
||||
return start > 0 || end < str.length ? str.substring(start, end) : str;
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// is numeric replacement for lodash
|
||||
// https://stackoverflow.com/a/52986361/8939
|
||||
//
|
||||
isNumeric: function(n) {
|
||||
//lodash isNumber returned false if it's a string and that's what the rest of the code expects even though it's parseable to a number
|
||||
return !this.isString(n) && !isNaN(parseFloat(n)) && isFinite(n);
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// is string replacement for lodash
|
||||
// https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_isString
|
||||
//
|
||||
isString: function(str) {
|
||||
return str != null && typeof str.valueOf() === "string";
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
//
|
||||
//
|
||||
//
|
||||
stringIsNullOrEmpty: function(str) {
|
||||
if (str === null || str === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (this.isString(str)) {
|
||||
if (str.trim() == "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// is Boolean replacement for lodash
|
||||
// https://stackoverflow.com/a/43718478/8939
|
||||
//
|
||||
isBoolean: function(obj) {
|
||||
return obj === true || obj === false || typeof variable === "boolean";
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// parse to number or null if not a number
|
||||
// used because route params can turn into strings
|
||||
// on their own
|
||||
//
|
||||
stringToIntOrNull: function(n) {
|
||||
const ret = Number.parseInt(n, 10);
|
||||
if (Number.isNaN(ret)) {
|
||||
return null;
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Simple array equality comparison
|
||||
// (will NOT work on arrays of objects)
|
||||
// Array order is relevant here as they are not sorted
|
||||
// change of order will equal change of array
|
||||
// as this is required for datatable sortby
|
||||
//
|
||||
isEqualArraysOfPrimitives: function(a, b) {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
|
||||
// If you don't care about the order of the elements inside
|
||||
// the array, you should sort both arrays here.
|
||||
// Please note that calling sort on an array will modify that array.
|
||||
// you might want to clone your array first.
|
||||
|
||||
for (var i = 0; i < a.length; ++i) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Use geolocation api to attempt to get current location
|
||||
// try high accuracy first and downgrade if unavailable
|
||||
//https://www.openstreetmap.org/?mlat=48.3911&mlon=-124.7353#map=12/48.3910/-124.7353
|
||||
//https://www.openstreetmap.org/#map=18/49.68155/-125.00435
|
||||
//https://www.openstreetmap.org/?mlat=49.71236&mlon=-124.96961#map=17/49.71236/-124.96961
|
||||
//https://www.google.com/maps/search/?api=1&query=47.5951518,-122.3316393
|
||||
getGeoLocation: async function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function successHigh(pos) {
|
||||
resolve({
|
||||
latitude: pos.coords.latitude,
|
||||
longitude: pos.coords.longitude
|
||||
});
|
||||
},
|
||||
function error(err) {
|
||||
//if here due to timeout getting high accuracy then try again with low accuracy
|
||||
if (error.code == error.TIMEOUT) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function successLow(pos) {
|
||||
resolve({
|
||||
latitude: pos.coords.latitude,
|
||||
longitude: pos.coords.longitude
|
||||
});
|
||||
},
|
||||
function error(err) {
|
||||
reject(
|
||||
new Error(
|
||||
`ERROR getting location(low_accuracy: ${err.code}): ${err.message}`
|
||||
)
|
||||
);
|
||||
},
|
||||
{
|
||||
maximumAge: 600000,
|
||||
timeout: 10000,
|
||||
enableHighAccuracy: false
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(
|
||||
new Error(
|
||||
`ERROR GETTING LOCATION(high_accuracy:${err.code}): ${err.message}`
|
||||
)
|
||||
);
|
||||
},
|
||||
{ maximumAge: 600000, timeout: 5000, enableHighAccuracy: true }
|
||||
);
|
||||
});
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Open map url
|
||||
//
|
||||
//
|
||||
viewGeoLocation: function(obj) {
|
||||
const hasGeo =
|
||||
obj.latitude != null &&
|
||||
obj.latitude != 0 &&
|
||||
obj.longitude != null &&
|
||||
obj.longitude != 0;
|
||||
|
||||
const hasAddress =
|
||||
!this.stringIsNullOrEmpty(obj.address) &&
|
||||
!this.stringIsNullOrEmpty(obj.city) &&
|
||||
!this.stringIsNullOrEmpty(obj.region) &&
|
||||
!this.stringIsNullOrEmpty(obj.country) &&
|
||||
!this.stringIsNullOrEmpty(obj.postCode);
|
||||
|
||||
if (!hasGeo && !hasAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mapUrl = window.$gz.store.state.userOptions.mapUrlTemplate;
|
||||
|
||||
//No pre-set?
|
||||
if (!mapUrl || mapUrl == "") {
|
||||
mapUrl =
|
||||
"https://www.google.com/maps/search/?api=1&query={ayaddress}<|>https://www.google.com/maps/search/?api=1&query={aylatitude},{aylongitude}";
|
||||
}
|
||||
|
||||
let geoMapUrl = null;
|
||||
let addressMapUrl = null;
|
||||
|
||||
//Parse the map url
|
||||
let mapUrls = [mapUrl];
|
||||
if (mapUrl.includes("<|>")) {
|
||||
mapUrls = mapUrl.split("<|>");
|
||||
}
|
||||
|
||||
mapUrls.forEach(z => {
|
||||
if (!geoMapUrl && z.includes("{aylatitude}")) {
|
||||
geoMapUrl = z;
|
||||
}
|
||||
if (!addressMapUrl && z.includes("{ayaddress}")) {
|
||||
addressMapUrl = z;
|
||||
}
|
||||
});
|
||||
|
||||
//decide which map to use here, favor geocode
|
||||
if (hasGeo && geoMapUrl) {
|
||||
//geo view
|
||||
mapUrl = geoMapUrl;
|
||||
mapUrl = mapUrl.split("{aylatitude}").join(obj.latitude);
|
||||
mapUrl = mapUrl.split("{aylongitude}").join(obj.longitude);
|
||||
} else if (hasAddress && addressMapUrl) {
|
||||
mapUrl = addressMapUrl;
|
||||
//compile address fields together
|
||||
//order street to country seems to be standard
|
||||
//note, if google need plus symbol delimiter, if bing, need comma delimiter
|
||||
//but both might accept one big string space delimited and url encoded so test that on all first
|
||||
const delimiter = " ";
|
||||
let q = "";
|
||||
if (obj.address) {
|
||||
q += obj.address + delimiter;
|
||||
}
|
||||
if (obj.city) {
|
||||
q += obj.city + delimiter;
|
||||
}
|
||||
|
||||
if (obj.region) {
|
||||
q += obj.region + delimiter;
|
||||
}
|
||||
|
||||
if (obj.country) {
|
||||
q += obj.country + delimiter;
|
||||
}
|
||||
|
||||
if (obj.postCode) {
|
||||
q += obj.postCode + delimiter;
|
||||
}
|
||||
|
||||
if (obj.addressPostal) {
|
||||
q += obj.addressPostal + delimiter;
|
||||
}
|
||||
|
||||
if (q.length > 1) {
|
||||
q = q.substring(0, q.length - 1);
|
||||
}
|
||||
//url encode the query
|
||||
q = encodeURIComponent(q);
|
||||
mapUrl = mapUrl.split("{ayaddress}").join(q);
|
||||
} else {
|
||||
throw new Error(
|
||||
"View map: error - no matching mapurl / address / geo coordinates set for display, nothing to view"
|
||||
);
|
||||
}
|
||||
window.open(mapUrl, "map");
|
||||
//This is not valid to do as some platforms don't open a new web browser window
|
||||
//but rather a map application in which case this is null and throws up the exception even though it's working
|
||||
// if (window.open(mapUrl, "map") == null) {
|
||||
// throw new Error(
|
||||
// "Problem displaying map in new window. Browser must allow pop-ups to view maps; check your browser setting"
|
||||
// );
|
||||
// }
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Online mapping service url formats
|
||||
//
|
||||
//
|
||||
mapProviderUrls: function() {
|
||||
return [
|
||||
{
|
||||
name: "Apple",
|
||||
value:
|
||||
"http://maps.apple.com/?q={ayaddress}<|>http://maps.apple.com/?ll={aylatitude},{aylongitude}"
|
||||
},
|
||||
{
|
||||
name: "Bing",
|
||||
value:
|
||||
"https://bing.com/maps/default.aspx?where1={ayaddress}<|>https://bing.com/maps/default.aspx?cp={aylatitude}~{aylongitude}&lvl=17&style=r&sp=point.{aylatitude}_{aylongitude}"
|
||||
},
|
||||
{
|
||||
name: "Google",
|
||||
value:
|
||||
"https://www.google.com/maps/search/?api=1&query={ayaddress}<|>https://www.google.com/maps/search/?api=1&query={aylatitude},{aylongitude}"
|
||||
},
|
||||
{
|
||||
name: "MapQuest",
|
||||
value:
|
||||
"https://mapquest.com/?center={ayaddress}&zoom=17<|>https://mapquest.com/?center={aylatitude},{aylongitude}&zoom=17"
|
||||
},
|
||||
{
|
||||
name: "Open Street Map",
|
||||
value:
|
||||
"https://www.openstreetmap.org/search?query={ayaddress}<|>https://www.openstreetmap.org/?mlat={aylatitude}&mlon={aylongitude}#map=17/{aylatitude}/{aylongitude}"
|
||||
},
|
||||
{
|
||||
name: "geo URI",
|
||||
value: "geo:{aylatitude},{aylongitude}"
|
||||
},
|
||||
{
|
||||
name: "Waze",
|
||||
value:
|
||||
"https://waze.com/ul?q={ayaddress}<|>https://www.waze.com/ul?ll={aylatitude},{aylongitude}&navigate=yes&zoom=17"
|
||||
},
|
||||
{
|
||||
name: "Yandex",
|
||||
value:
|
||||
"https://yandex.ru/maps/?mode=search&text={ayaddress}&z=17<|>https://yandex.ru/maps/?ll={aylatitude},{aylongitude}&z=12&l=map"
|
||||
}
|
||||
];
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// v-calendar view to Sockeye scheduleview enum
|
||||
//
|
||||
//
|
||||
calendarViewToSockeyeEnum: function(view) {
|
||||
switch (view) {
|
||||
case "day":
|
||||
return 1;
|
||||
case "week":
|
||||
return 2;
|
||||
case "month":
|
||||
return 3;
|
||||
case "4day":
|
||||
return 4;
|
||||
case "category":
|
||||
return 5;
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`gzutil->calendarViewtoSockeyeEnum - Unknown view type '${view}'`
|
||||
);
|
||||
}
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// GZDaysOfWeek to VCalendar weekdays
|
||||
//
|
||||
//
|
||||
DaysOfWeekToWeekdays: function(dow) {
|
||||
/*
|
||||
AyaDaysOfWeek
|
||||
Monday = 1,
|
||||
Tuesday = 2,
|
||||
Wednesday = 4,
|
||||
Thursday = 8,
|
||||
Friday = 16,
|
||||
Saturday = 32,
|
||||
Sunday = 64
|
||||
|
||||
vCalendar [
|
||||
0,//sunday
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6//saturday
|
||||
]
|
||||
*/
|
||||
if (dow == null || dow == 0) {
|
||||
return [0, 1, 2, 3, 4, 5, 6]; //all the days
|
||||
}
|
||||
const ret = [];
|
||||
|
||||
//turn EXCLUDE selected gzDaysOfWeek into INCLUDE selected days for vCalendar
|
||||
if (!(dow & 64)) {
|
||||
ret.push(0);
|
||||
}
|
||||
if (!(dow & 1)) {
|
||||
ret.push(1);
|
||||
}
|
||||
if (!(dow & 2)) {
|
||||
ret.push(2);
|
||||
}
|
||||
if (!(dow & 4)) {
|
||||
ret.push(3);
|
||||
}
|
||||
if (!(dow & 8)) {
|
||||
ret.push(4);
|
||||
}
|
||||
if (!(dow & 16)) {
|
||||
ret.push(5);
|
||||
}
|
||||
if (!(dow & 32)) {
|
||||
ret.push(6);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Random integer from 0 to max
|
||||
//
|
||||
//
|
||||
getRandomInt: function(max) {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
//new functions above here
|
||||
};
|
||||
584
client/src/api/initialize.js
Normal file
@@ -0,0 +1,584 @@
|
||||
function addNavItem(title, icon, route, navItems, key, testid, color = null) {
|
||||
if (!testid) {
|
||||
testid = route;
|
||||
}
|
||||
|
||||
const o = {
|
||||
title,
|
||||
icon,
|
||||
route,
|
||||
navItems,
|
||||
key: key,
|
||||
testid: testid
|
||||
};
|
||||
if (color != null) {
|
||||
o["color"] = color;
|
||||
}
|
||||
o.navItems.forEach(z => {
|
||||
if (z.testid == null) {
|
||||
z.testid = z.route;
|
||||
}
|
||||
});
|
||||
|
||||
window.$gz.store.commit("addNavItem", o);
|
||||
}
|
||||
|
||||
function initNavPanel() {
|
||||
let key = 0;
|
||||
let sub = [];
|
||||
|
||||
/*Service = 1,
|
||||
NotService = 2,
|
||||
Customer = 3,
|
||||
HeadOffice = 4,
|
||||
ServiceContractor = 5 */
|
||||
|
||||
//########## OUTSIDE USERS GROUP (CUSTOMER / HEADOFFICE) ###
|
||||
if (window.$gz.store.getters.isCustomerUser == true) {
|
||||
//clear sublevel array
|
||||
sub = [];
|
||||
|
||||
//Set homePage in store to customer csr for this user type
|
||||
let CustomerHomePageSet = false;
|
||||
|
||||
//USER SETTINGS
|
||||
if (window.$gz.store.state.customerRights.userSettings == true) {
|
||||
sub.push({
|
||||
title: "UserSettings",
|
||||
icon: "$sockiUserCog",
|
||||
route: "/home-user-settings",
|
||||
key: key++
|
||||
});
|
||||
|
||||
window.$gz.store.commit("setHomePage", "/home-user-settings");
|
||||
CustomerHomePageSet = true;
|
||||
}
|
||||
|
||||
if (window.$gz.store.getters.canSubscribeToNotifications) {
|
||||
sub.push({
|
||||
title: "NotifySubscriptionList",
|
||||
icon: "$sockiBullhorn",
|
||||
route: "/home-notify-subscriptions",
|
||||
key: key++
|
||||
});
|
||||
|
||||
window.$gz.store.commit("setHomePage", "/home-notify-subscriptions");
|
||||
CustomerHomePageSet = true;
|
||||
}
|
||||
|
||||
//** CUSTOMER LOGIN HOME (TOP)
|
||||
addNavItem("Home", "$sockiHome", undefined, sub, key++, "homecustomer");
|
||||
|
||||
//last resort home page if nothing else kicked in
|
||||
if (!CustomerHomePageSet) {
|
||||
window.$gz.store.commit("setHomePage", "/no-features-available");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//###### ALL INSIDE USERS FROM HERE DOWN ###############
|
||||
|
||||
//####### HOME GROUP
|
||||
|
||||
//DASHBOARD
|
||||
|
||||
sub.push({
|
||||
title: "Dashboard",
|
||||
icon: "$sockiTachometer",
|
||||
route: "/home-dashboard",
|
||||
key: key++
|
||||
});
|
||||
|
||||
//SEARCH
|
||||
sub.push({
|
||||
title: "Search",
|
||||
icon: "$sockiSearch",
|
||||
route: "/home-search",
|
||||
key: key++
|
||||
});
|
||||
|
||||
//SCHEDULE (personal)
|
||||
sub.push({
|
||||
title: "Schedule",
|
||||
icon: "$sockiCalendarDay",
|
||||
route: "/home-schedule",
|
||||
key: key++
|
||||
});
|
||||
|
||||
//MEMOS
|
||||
sub.push({
|
||||
title: "MemoList",
|
||||
icon: "$sockiInbox",
|
||||
route: "/home-memos",
|
||||
key: key++
|
||||
});
|
||||
|
||||
//REMINDERS
|
||||
sub.push({
|
||||
title: "ReminderList",
|
||||
icon: "$sockiStickyNote",
|
||||
route: "/home-reminders",
|
||||
key: key++
|
||||
});
|
||||
|
||||
//REVIEWS
|
||||
sub.push({
|
||||
title: "ReviewList",
|
||||
icon: "$sockiCalendarCheck",
|
||||
route: "/home-reviews",
|
||||
key: key++
|
||||
});
|
||||
|
||||
//USER SETTINGS
|
||||
sub.push({
|
||||
title: "UserSettings",
|
||||
icon: "$sockiUserCog",
|
||||
route: "/home-user-settings",
|
||||
key: key++
|
||||
});
|
||||
|
||||
//USER NOTIFICATION SUBSCRIPTIONS
|
||||
sub.push({
|
||||
title: "NotifySubscriptionList",
|
||||
icon: "$sockiBullhorn",
|
||||
route: "/home-notify-subscriptions",
|
||||
key: key++
|
||||
});
|
||||
|
||||
//HISTORY / MRU / ACTIVITY (personal)
|
||||
sub.push({
|
||||
title: "History",
|
||||
icon: "$sockiHistory",
|
||||
route: `/history/3/${window.$gz.store.state.userId}/true`,
|
||||
key: key++,
|
||||
testid: "/home-history"
|
||||
});
|
||||
|
||||
//HOME
|
||||
if (sub.length > 0) {
|
||||
//Set homePage in store to dashboard
|
||||
window.$gz.store.commit("setHomePage", "/home-dashboard");
|
||||
addNavItem("Home", "$sockiHome", undefined, sub, key++, "home");
|
||||
}
|
||||
|
||||
//######### CUSTOMER GROUP
|
||||
if (window.$gz.role.canOpen(window.$gz.type.Customer)) {
|
||||
//these all require Customer rights so all in the same block
|
||||
|
||||
//clear sublevel array
|
||||
sub = [];
|
||||
|
||||
//CUSTOMERS subitem
|
||||
sub.push({
|
||||
title: "CustomerList",
|
||||
icon: "$sockiAddressCard",
|
||||
route: "/cust-customers",
|
||||
key: key++
|
||||
});
|
||||
|
||||
//HEAD OFFICES subitem
|
||||
sub.push({
|
||||
title: "HeadOfficeList",
|
||||
icon: "$sockiSitemap",
|
||||
route: "/cust-head-offices",
|
||||
key: key++
|
||||
});
|
||||
|
||||
//Customer / Headoffice Users subitem
|
||||
sub.push({
|
||||
title: "Contacts",
|
||||
icon: "$sockiUsers",
|
||||
route: "/cust-users",
|
||||
key: key++
|
||||
});
|
||||
|
||||
sub.push({
|
||||
title: "CustomerNotifySubscriptionList",
|
||||
icon: "$sockiBullhorn",
|
||||
route: "/cust-notify-subscriptions",
|
||||
key: key++
|
||||
});
|
||||
|
||||
// ** CUSTOMER (TOP)
|
||||
addNavItem(
|
||||
"CustomerList",
|
||||
"$sockiAddressBook",
|
||||
undefined,
|
||||
sub,
|
||||
key++,
|
||||
"customer"
|
||||
);
|
||||
}
|
||||
|
||||
//####### SERVICE GROUP
|
||||
|
||||
sub = [];
|
||||
|
||||
//SCHEDULE (service)
|
||||
if (
|
||||
window.$gz.role.canOpen(window.$gz.type.WorkOrder) ||
|
||||
window.$gz.role.canOpen(window.$gz.type.Quote) ||
|
||||
window.$gz.role.canOpen(window.$gz.type.PM)
|
||||
) {
|
||||
sub.push({
|
||||
title: "Schedule",
|
||||
icon: "$sockiCalendarAlt",
|
||||
route: "/svc-schedule",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
//**** Service (TOP GROUP)
|
||||
if (
|
||||
sub.length > 0 &&
|
||||
!window.$gz.role.hasRole([
|
||||
window.$gz.role.AUTHORIZATION_ROLES.TechRestricted
|
||||
])
|
||||
) {
|
||||
addNavItem("Service", "$sockiToolbox", undefined, sub, key++, "service");
|
||||
}
|
||||
|
||||
//######### INVENTORY GROUP
|
||||
|
||||
//clear sublevel array
|
||||
sub = [];
|
||||
|
||||
//PARTS (part list)
|
||||
if (window.$gz.role.canOpen(window.$gz.type.Part)) {
|
||||
sub.push({
|
||||
title: "PartList",
|
||||
icon: "$sockiBoxes",
|
||||
route: "/inv-parts",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
//PURCHASE ORDERS / PART REQUESTS
|
||||
if (window.$gz.role.canOpen(window.$gz.type.PurchaseOrder)) {
|
||||
sub.push({
|
||||
title: "InventoryPurchaseOrders",
|
||||
icon: "$sockiTruckLoading",
|
||||
route: "/inv-purchase-orders",
|
||||
key: key++
|
||||
});
|
||||
|
||||
sub.push({
|
||||
title: "WorkOrderItemPartRequestList",
|
||||
icon: "$sockiParachuteBox",
|
||||
route: "/inv-part-requests",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
//****************** ACCOUNTING
|
||||
|
||||
//SOCKEYE Keeping this for very likely future accounting functionality
|
||||
sub = [];
|
||||
|
||||
// ** ACCOUNTING (TOP)
|
||||
if (sub.length > 0) {
|
||||
addNavItem(
|
||||
"Accounting",
|
||||
"$sockiCoins",
|
||||
undefined,
|
||||
sub,
|
||||
key++,
|
||||
"accounting"
|
||||
);
|
||||
}
|
||||
|
||||
//############# ADMINISTRATION
|
||||
|
||||
//clear sublevel array
|
||||
sub = [];
|
||||
|
||||
// GLOBAL SETTINGS
|
||||
if (window.$gz.role.canOpen(window.$gz.type.Global)) {
|
||||
sub.push({
|
||||
title: "AdministrationGlobalSettings",
|
||||
icon: "$sockiCogs",
|
||||
route: "/adm-global-settings",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
// USERS
|
||||
if (window.$gz.role.canOpen(window.$gz.type.User)) {
|
||||
sub.push({
|
||||
title: "UserList",
|
||||
icon: "$sockiUsers",
|
||||
route: "/adm-users",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
//TRANSLATION
|
||||
if (window.$gz.role.canOpen(window.$gz.type.Translation)) {
|
||||
sub.push({
|
||||
title: "TranslationList",
|
||||
icon: "$sockiLanguage",
|
||||
route: "/adm-translations",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
//REPORT TEMPLATES
|
||||
if (window.$gz.role.canChange(window.$gz.type.Report)) {
|
||||
sub.push({
|
||||
title: "ReportList",
|
||||
icon: "$sockiThList",
|
||||
route: "/adm-report-templates",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
//FILES IN DATABASE
|
||||
if (window.$gz.role.canOpen(window.$gz.type.FileAttachment)) {
|
||||
sub.push({
|
||||
title: "Attachments",
|
||||
icon: "$sockiPaperclip",
|
||||
route: "/adm-attachments",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
//EVENT LOG / HISTORY
|
||||
if (window.$gz.role.canOpen(window.$gz.type.Global)) {
|
||||
//not really an appropriate object here just guessing
|
||||
sub.push({
|
||||
title: "History",
|
||||
icon: "$sockiHistory",
|
||||
route: "/adm-history",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
//IMPORT
|
||||
if (window.$gz.role.canOpen(window.$gz.type.Global)) {
|
||||
//again, not really an appropriate object type
|
||||
sub.push({
|
||||
title: "Import",
|
||||
icon: "$sockiFileImport",
|
||||
route: "/adm-import",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
//INTEGRATION
|
||||
//decision here is that only teh biz admin can *control* or remove an integration
|
||||
//even though all full role inside users can create or edit integrations (just not through the Sockeye user interface)
|
||||
//this is required to support integrations made for various roles like inventory accounting etc
|
||||
if (window.$gz.role.canOpen(window.$gz.type.Global)) {
|
||||
sub.push({
|
||||
title: "IntegrationList",
|
||||
icon: "$sockiCampground",
|
||||
route: "/adm-integrations",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
// ** ADMINISTRATION (TOP)
|
||||
if (sub.length > 0) {
|
||||
addNavItem(
|
||||
"Administration",
|
||||
"$sockiUserTie",
|
||||
undefined,
|
||||
sub,
|
||||
key++,
|
||||
"administration"
|
||||
);
|
||||
}
|
||||
|
||||
//############ OPERATIONS
|
||||
|
||||
//clear sublevel array
|
||||
sub = [];
|
||||
|
||||
// BACKUP
|
||||
if (window.$gz.role.canOpen(window.$gz.type.Backup)) {
|
||||
sub.push({
|
||||
title: "Backup",
|
||||
icon: "$sockiFileArchive",
|
||||
route: "/ops-backup",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
// SERVER STATE
|
||||
if (window.$gz.role.canChange(window.$gz.type.ServerState)) {
|
||||
sub.push({
|
||||
title: "ServerState",
|
||||
icon: "$sockiDoorOpen",
|
||||
route: "/ops-server-state",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
// JOBS
|
||||
if (window.$gz.role.canOpen(window.$gz.type.ServerJob)) {
|
||||
sub.push({
|
||||
title: "ServerJobs",
|
||||
icon: "$sockiRobot",
|
||||
route: "/ops-jobs",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
// LOGS
|
||||
if (window.$gz.role.canOpen(window.$gz.type.LogFile)) {
|
||||
sub.push({
|
||||
title: "ServerLog",
|
||||
icon: "$sockiHistory",
|
||||
route: "/ops-log",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
//METRICS
|
||||
if (window.$gz.role.canOpen(window.$gz.type.ServerMetrics)) {
|
||||
sub.push({
|
||||
title: "ServerMetrics",
|
||||
icon: "$sockiFileMedicalAlt",
|
||||
route: "/ops-metrics",
|
||||
key: key++
|
||||
});
|
||||
|
||||
// //PROFILE
|
||||
// //metrics rights
|
||||
// sub.push({
|
||||
// title: "ServerProfiler",
|
||||
// icon: "$sockiBinoculars",
|
||||
// route: "/ops-profile",
|
||||
// key: key++
|
||||
// });
|
||||
}
|
||||
|
||||
//NOTIFICATION CONFIG AND HISTORY
|
||||
if (window.$gz.role.canOpen(window.$gz.type.OpsNotificationSettings)) {
|
||||
sub.push({
|
||||
title: "OpsNotificationSettings",
|
||||
icon: "$sockiBullhorn",
|
||||
route: "/ops-notification-settings",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
if (window.$gz.role.canOpen(window.$gz.type.OpsNotificationSettings)) {
|
||||
sub.push({
|
||||
title: "NotificationDeliveryLog",
|
||||
icon: "$sockiHistory",
|
||||
route: "/ops-notify-log",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
if (window.$gz.role.canOpen(window.$gz.type.OpsNotificationSettings)) {
|
||||
sub.push({
|
||||
title: "NotificationCustomerDeliveryLog",
|
||||
icon: "$sockiHistory",
|
||||
route: "/ops-customer-notify-log",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
// OPS VIEW SERVER CONFIGURATION
|
||||
if (window.$gz.role.canOpen(window.$gz.type.GlobalOps)) {
|
||||
sub.push({
|
||||
title: "ViewServerConfiguration",
|
||||
icon: "$sockiInfoCircle",
|
||||
route: "/ops-view-configuration",
|
||||
key: key++
|
||||
});
|
||||
}
|
||||
|
||||
// ** OPERATIONS (TOP)
|
||||
if (sub.length > 0) {
|
||||
addNavItem(
|
||||
"Operations",
|
||||
"$sockiServer",
|
||||
undefined,
|
||||
sub,
|
||||
key++,
|
||||
"operations"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserOptions() {
|
||||
try {
|
||||
const res = await window.$gz.api.get(
|
||||
"user-option/" + window.$gz.store.state.userId
|
||||
);
|
||||
|
||||
if (res.error) {
|
||||
//In a form this would trigger a bunch of validation or error display code but for here and now:
|
||||
//convert error to human readable string for display and popup a notification to user
|
||||
const msg = window.$gz.api.apiErrorToHumanString(res.error);
|
||||
window.$gz.store.commit(
|
||||
"logItem",
|
||||
"Initialize::() fetch useroptions -> error" + msg
|
||||
);
|
||||
window.$gz.eventBus.$emit("notify-error", msg);
|
||||
} else {
|
||||
//Check if overrides and use them here
|
||||
//or else use browser defaults
|
||||
|
||||
const l = {
|
||||
languageOverride: null,
|
||||
timeZoneOverride: null,
|
||||
currencyName: null,
|
||||
hour12: true,
|
||||
//uiColor: "#000000ff",
|
||||
emailAddress: null,
|
||||
mapUrlTemplate: null
|
||||
};
|
||||
|
||||
l.languageOverride = res.data.languageOverride;
|
||||
l.timeZoneOverride = res.data.timeZoneOverride;
|
||||
|
||||
//No browser setting for this so meh
|
||||
l.currencyName = res.data.currencyName;
|
||||
|
||||
if (res.data.hour12 != null) {
|
||||
l.hour12 = res.data.hour12;
|
||||
}
|
||||
|
||||
// l.uiColor = res.data.uiColor || "#000000ff";
|
||||
l.emailAddress = res.data.emailAddress || null;
|
||||
|
||||
l.mapUrlTemplate = res.data.mapUrlTemplate || null;
|
||||
|
||||
window.$gz.store.commit("setUserOptions", l);
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.store.commit(
|
||||
"logItem",
|
||||
"Initialize::() fetch useroptions -> error" + error
|
||||
);
|
||||
throw new Error(window.$gz.errorHandler.errorToString(error));
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
// Initialize the app
|
||||
// on change of authentication status
|
||||
export default function initialize() {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async function(resolve, reject) {
|
||||
if (!window.$gz.store.state.authenticated) {
|
||||
throw new Error("initialize: Error, called but user not authenticated!");
|
||||
}
|
||||
try {
|
||||
await window.$gz.translation.cacheTranslations(
|
||||
window.$gz.translation.coreKeys
|
||||
);
|
||||
initNavPanel();
|
||||
await getUserOptions();
|
||||
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
604
client/src/api/locale.js
Normal file
@@ -0,0 +1,604 @@
|
||||
//Browser Locale conversion utilities
|
||||
//dates,numbers currency etc
|
||||
export default {
|
||||
////////////////////////////////////////////////////////
|
||||
// attempt to determine user's preferred language settings
|
||||
// As of Jan 2020 all major browsers support
|
||||
// navigator.languages
|
||||
// but some use navigator.language (singular) to denote UI language preference
|
||||
// not browsing language preference
|
||||
// so the ideal way to do this is to use navigator.languages[0] for the preferred language
|
||||
// and ignore the singular property since we don't care about the actual browser UI language
|
||||
// only how the user expects to see the page itself
|
||||
//
|
||||
// also for sake of future proofing and edge cases need to have it be manually settable as well
|
||||
//
|
||||
//https://appmakers.dev/bcp-47-language-codes-list/
|
||||
///////////////////////////////////////////
|
||||
// Get users default language code
|
||||
// first check if overriden in useroptions
|
||||
// if not then use browsers own setting
|
||||
//if not that then final default of en-US
|
||||
getResolvedLanguage() {
|
||||
let l = window.$gz.store.state.userOptions.languageOverride;
|
||||
if (!window.$gz.util.stringIsNullOrEmpty(l)) {
|
||||
return l;
|
||||
} else {
|
||||
l = window.navigator.languages[0];
|
||||
if (!window.$gz.util.stringIsNullOrEmpty(l)) {
|
||||
return l;
|
||||
} else {
|
||||
return "en-US";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
///////////////////////////////////////////
|
||||
// Get users default time zone
|
||||
// first check if overriden in useroptions
|
||||
// if not then use browsers own setting
|
||||
// if that is empty then final default of "America/New_York"
|
||||
//https://www.iana.org/time-zones
|
||||
//https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
getResolvedTimeZoneName() {
|
||||
let tz = window.$gz.store.state.userOptions.timeZoneOverride;
|
||||
if (!window.$gz.util.stringIsNullOrEmpty(tz)) {
|
||||
return tz;
|
||||
} else {
|
||||
tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (!window.$gz.util.stringIsNullOrEmpty(tz)) {
|
||||
return tz;
|
||||
} else {
|
||||
return "America/New_York";
|
||||
}
|
||||
}
|
||||
},
|
||||
//////////////////////////////////////////////////
|
||||
// Get the user's chosen currency name
|
||||
//https://en.wikipedia.org/wiki/ISO_4217
|
||||
//default to USD if nothing specified
|
||||
getCurrencyName() {
|
||||
const cur = window.$gz.store.state.userOptions.currencyName;
|
||||
if (!window.$gz.util.stringIsNullOrEmpty(cur)) {
|
||||
return cur;
|
||||
} else {
|
||||
return "USD";
|
||||
}
|
||||
},
|
||||
//////////////////////////////////////////////////
|
||||
// Get the user's chosen 12hr clock
|
||||
//
|
||||
getHour12() {
|
||||
return window.$gz.store.state.userOptions.hour12;
|
||||
},
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// Turn a utc ISO date from server into a vuetify calendar
|
||||
// schedule control compatible (epoch) format
|
||||
// localized.
|
||||
// For ease of use in schedule the epoch (milliseconds) is the best format
|
||||
// "It must be a Date, number of seconds since Epoch, or a string in the format of YYYY-MM-DD or YYYY-MM-DD hh:mm. Zero-padding is optional and seconds are ignored.""
|
||||
//
|
||||
//
|
||||
utcDateToScheduleCompatibleFormatLocalized(value, timeZoneName) {
|
||||
//This function takes a UTC iso format date string, parses it into a date then converts that date to the User's configured time zone
|
||||
//outputs that in a format close to ISO, fixes the space in the middle of the output to match ISO 8601 format then returns as an
|
||||
//epoch
|
||||
//this is to support controls that are not time zone settable so they are always in local browser time zone of os, however user may be operating
|
||||
//sockeye in another desired time zone so this is all to support that scenario
|
||||
|
||||
if (!value) {
|
||||
if (window.$gz.dev) {
|
||||
throw new Error(
|
||||
`locale::utcDateToScheduleCompatibleFormatLocalized - Value is empty`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date(
|
||||
new Date(value) //convert to locale timezone and output in the closest thing to iso-8601 format
|
||||
.toLocaleString("sv-SE", {
|
||||
timeZone: timeZoneName
|
||||
})
|
||||
.replace(" ", "T") //Safari can't parse the date from here because sv-SE puts a space between date and time and Safari will only parse if it has a T between
|
||||
).getTime();
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Convert a local schedule epoch timestamp
|
||||
// to specified time zone equivalent then
|
||||
// to UTC and output as ISO 8601
|
||||
//
|
||||
//
|
||||
localScheduleFormatToUTC8601String(value, timeZoneName) {
|
||||
if (!timeZoneName) {
|
||||
timeZoneName = this.getResolvedTimeZoneName();
|
||||
}
|
||||
|
||||
//input: epoch in local browser time zone
|
||||
//output: transform to date and time string
|
||||
//convert to desired time zone but at same time and date
|
||||
//(i.e. if it browser is vancouver and 1pm is selected but desired is new york's 1pm
|
||||
// so convert the string as if it was new york then back to iso so that the time is adjusted forward
|
||||
// as if the user was in new york in their browser default)
|
||||
|
||||
//parse in the time in to the specified timezone
|
||||
let ret = window.$gz.DateTime.fromISO(
|
||||
//output the sched epoch as local time string without zone
|
||||
new Date(value).toLocaleString("sv-SE").replace(" ", "T"),
|
||||
{
|
||||
zone: timeZoneName
|
||||
}
|
||||
);
|
||||
|
||||
ret = ret.setZone("utc"); //convert to UTC
|
||||
ret = ret.toISO(); //output as ISO 8601
|
||||
return ret;
|
||||
},
|
||||
///////////////////////////////////////////
|
||||
// Turn a utc date into a displayable
|
||||
// short date and time
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString
|
||||
//
|
||||
utcDateToShortDateAndTimeLocalized(
|
||||
value,
|
||||
timeZoneName,
|
||||
languageName,
|
||||
hour12
|
||||
) {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
if (!timeZoneName) {
|
||||
timeZoneName = this.getResolvedTimeZoneName();
|
||||
}
|
||||
if (!languageName) {
|
||||
languageName = this.getResolvedLanguage();
|
||||
}
|
||||
|
||||
if (!hour12) {
|
||||
hour12 = this.getHour12();
|
||||
}
|
||||
|
||||
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
|
||||
const parsedDate = new Date(value);
|
||||
|
||||
//is it a valid date?
|
||||
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
|
||||
return "not valid";
|
||||
}
|
||||
|
||||
return parsedDate.toLocaleString(languageName, {
|
||||
timeZone: timeZoneName,
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
hour12: hour12
|
||||
});
|
||||
},
|
||||
///////////////////////////////////////////
|
||||
// Turn a utc date into a displayable
|
||||
// date and time with specific formats
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString
|
||||
//
|
||||
utcDateToSpecifiedDateAndTimeLocalized(
|
||||
value,
|
||||
timeZoneName,
|
||||
languageName,
|
||||
hour12,
|
||||
dateStyle,
|
||||
timeStyle
|
||||
) {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
if (!timeZoneName) {
|
||||
timeZoneName = this.getResolvedTimeZoneName();
|
||||
}
|
||||
if (!languageName) {
|
||||
languageName = this.getResolvedLanguage();
|
||||
}
|
||||
|
||||
if (!hour12) {
|
||||
hour12 = this.getHour12();
|
||||
}
|
||||
|
||||
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
|
||||
const parsedDate = new Date(value);
|
||||
|
||||
//is it a valid date?
|
||||
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
|
||||
return "not valid";
|
||||
}
|
||||
|
||||
return parsedDate.toLocaleString(languageName, {
|
||||
timeZone: timeZoneName,
|
||||
dateStyle: dateStyle,
|
||||
timeStyle: timeStyle,
|
||||
hour12: hour12
|
||||
});
|
||||
},
|
||||
///////////////////////////////////////////
|
||||
// Turn a utc date into a displayable
|
||||
// short date
|
||||
//
|
||||
utcDateToShortDateLocalized(value, timeZoneName, languageName) {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
if (!timeZoneName) {
|
||||
timeZoneName = this.getResolvedTimeZoneName();
|
||||
}
|
||||
if (!languageName) {
|
||||
languageName = this.getResolvedLanguage();
|
||||
}
|
||||
|
||||
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
|
||||
const parsedDate = new Date(value);
|
||||
|
||||
//is it a valid date?
|
||||
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
|
||||
return "not valid";
|
||||
}
|
||||
|
||||
return parsedDate.toLocaleDateString(languageName, {
|
||||
timeZone: timeZoneName,
|
||||
dateStyle: "short"
|
||||
});
|
||||
}, ///////////////////////////////////////////
|
||||
// Turn a utc date into a displayable
|
||||
// short time
|
||||
//
|
||||
utcDateToShortTimeLocalized(value, timeZoneName, languageName, hour12) {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
if (!timeZoneName) {
|
||||
timeZoneName = this.getResolvedTimeZoneName();
|
||||
}
|
||||
if (!languageName) {
|
||||
languageName = this.getResolvedLanguage();
|
||||
}
|
||||
|
||||
if (!hour12) {
|
||||
hour12 = this.getHour12();
|
||||
}
|
||||
|
||||
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
|
||||
const parsedDate = new Date(value);
|
||||
|
||||
//is it a valid date?
|
||||
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
|
||||
return "not valid";
|
||||
}
|
||||
|
||||
return parsedDate.toLocaleTimeString(languageName, {
|
||||
timeZone: timeZoneName,
|
||||
timeStyle: "short",
|
||||
hour12: hour12
|
||||
});
|
||||
},
|
||||
///////////////////////////////////////////
|
||||
// Turn a duration value into a display
|
||||
//
|
||||
durationLocalized(value, hideSeconds) {
|
||||
if (value == null || value == "00:00:00") {
|
||||
return "";
|
||||
}
|
||||
|
||||
let theDays = 0;
|
||||
let theHours = 0;
|
||||
let theMinutes = 0;
|
||||
let theSeconds = 0;
|
||||
let ret = "";
|
||||
|
||||
const work = value.split(":");
|
||||
//has days?
|
||||
if (work[0].includes(".")) {
|
||||
let dh = work[0].split(".");
|
||||
theDays = Number(dh[0]);
|
||||
theHours = Number(dh[1]);
|
||||
} else {
|
||||
theHours = Number(work[0]);
|
||||
}
|
||||
theMinutes = Number(work[1]);
|
||||
//has milliseconds? (ignore them)
|
||||
if (work[2].includes(".")) {
|
||||
let dh = work[2].split(".");
|
||||
theSeconds = Number(dh[0]);
|
||||
} else {
|
||||
theSeconds = Number(work[2]);
|
||||
}
|
||||
|
||||
if (theDays != 0) {
|
||||
ret += theDays + " " + window.$gz.translation.get("TimeSpanDays") + " ";
|
||||
}
|
||||
if (theHours != 0) {
|
||||
ret += theHours + " " + window.$gz.translation.get("TimeSpanHours") + " ";
|
||||
}
|
||||
if (theMinutes != 0) {
|
||||
ret +=
|
||||
theMinutes + " " + window.$gz.translation.get("TimeSpanMinutes") + " ";
|
||||
}
|
||||
if (!hideSeconds && theSeconds != 0) {
|
||||
ret +=
|
||||
theSeconds + " " + window.$gz.translation.get("TimeSpanSeconds") + " ";
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Convert a utc date to local time zone
|
||||
// and return time portion only in iso 8601
|
||||
// format (used by time and date picker components)
|
||||
//
|
||||
utcDateStringToLocal8601TimeOnlyString(value, timeZoneName) {
|
||||
if (!value) {
|
||||
//if no value, return the current time as expected by the time picker
|
||||
} else {
|
||||
//ok, the reason for sv-SE is that it's a locale that returns the time already in ISO format and 24hr by default
|
||||
//that can change over time so if this breaks that's why
|
||||
//also fr-CA does as well as possibly en-CA
|
||||
//https://stackoverflow.com/a/58633686/8939
|
||||
if (!timeZoneName) {
|
||||
timeZoneName = this.getResolvedTimeZoneName();
|
||||
}
|
||||
|
||||
return new Date(value).toLocaleTimeString("sv-SE", {
|
||||
timeZone: timeZoneName
|
||||
});
|
||||
}
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Convert a local time only string with date string
|
||||
// to UTC and output as ISO 8601
|
||||
// also converts to time zone specified if diff from browser
|
||||
// (used by time and date picker components)
|
||||
//
|
||||
localTimeDateStringToUTC8601String(value, timeZoneName) {
|
||||
//https://moment.github.io/luxon/docs/manual/zones.html#creating-datetimes-in-a-zone
|
||||
if (!timeZoneName) {
|
||||
timeZoneName = this.getResolvedTimeZoneName();
|
||||
}
|
||||
|
||||
//parse in the time in the currently used timezone
|
||||
let ret = window.$gz.DateTime.fromISO(value, {
|
||||
zone: timeZoneName
|
||||
});
|
||||
|
||||
ret = ret.setZone("utc"); //convert to UTC
|
||||
ret = ret.toISO(); //output as ISO 8601
|
||||
|
||||
return ret;
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// UTC Now in api format
|
||||
// to UTC and output as ISO 8601
|
||||
// (used to set defaults)
|
||||
//
|
||||
nowUTC8601String(timeZoneName) {
|
||||
if (!timeZoneName) {
|
||||
timeZoneName = this.getResolvedTimeZoneName();
|
||||
}
|
||||
const ret = window.$gz.DateTime.local()
|
||||
.setZone(timeZoneName)
|
||||
.toUTC()
|
||||
.toString();
|
||||
return ret;
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// UTC ISO 8601 string add minutes
|
||||
// and return as UTC ISO 8601 string
|
||||
// (used to set automatic / default adjusted times)
|
||||
//
|
||||
addMinutesToUTC8601String(val, minutes) {
|
||||
if (!val || val == "" || minutes == null || minutes == 0) {
|
||||
return val;
|
||||
}
|
||||
//instantiate a luxon date object from val which is assumed to be an iso string
|
||||
let dt = window.$gz.DateTime.fromISO(val);
|
||||
if (!dt.isValid) {
|
||||
console.error("locale::addMinutes, input not valid:", {
|
||||
val: val,
|
||||
dt: dt
|
||||
});
|
||||
return val;
|
||||
}
|
||||
//add minutes
|
||||
dt = dt.plus({ minutes: minutes });
|
||||
return dt.toUTC().toString();
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// UTC ISO 8601 string add arbitrary value based
|
||||
// on luxon duration format
|
||||
// and return as UTC ISO 8601 string
|
||||
//https://moment.github.io/luxon/api-docs/index.html#datetimeplus
|
||||
//
|
||||
addDurationToUTC8601String(val, duration) {
|
||||
if (
|
||||
!val ||
|
||||
val == "" ||
|
||||
duration == null ||
|
||||
!typeof duration === "object"
|
||||
) {
|
||||
return val;
|
||||
}
|
||||
//instantiate a luxon date object from val which is assumed to be an iso string
|
||||
let dt = window.$gz.DateTime.fromISO(val);
|
||||
if (!dt.isValid) {
|
||||
console.error("locale::addDurationToUTC8601String, input not valid:", {
|
||||
val: val,
|
||||
dt: dt
|
||||
});
|
||||
return val;
|
||||
}
|
||||
//add minutes
|
||||
dt = dt.plus(duration);
|
||||
return dt.toUTC().toString();
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// parse UTC ISO 8601 strings, diff, return hours
|
||||
//
|
||||
diffHoursFromUTC8601String(start, stop) {
|
||||
if (!start || start == "" || !stop == null || stop == "") {
|
||||
return 0;
|
||||
}
|
||||
//instantiate a luxon date object from val which is assumed to be an iso string
|
||||
const startDate = window.$gz.DateTime.fromISO(start);
|
||||
if (!startDate.isValid) {
|
||||
console.error("locale::diffHours, start not valid:", {
|
||||
start: start,
|
||||
startDate: startDate
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
const stopDate = window.$gz.DateTime.fromISO(stop);
|
||||
if (!stopDate.isValid) {
|
||||
console.error("locale::diffHours, start not valid:", {
|
||||
stop: stop,
|
||||
stopDate: stopDate
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
|
||||
// console.log(
|
||||
// "locale:diffhours...",
|
||||
// stopDate.diff(startDate, "hours").toObject().hours
|
||||
// );
|
||||
|
||||
// console.log(
|
||||
// "locale:diffhours.. ROUNDED.",
|
||||
// window.$gz.util.roundAccurately(
|
||||
// stopDate.diff(startDate, "hours").toObject().hours,
|
||||
// 2
|
||||
// )
|
||||
// );
|
||||
|
||||
return window.$gz.util.roundAccurately(
|
||||
stopDate.diff(startDate, "hours").toObject().hours,
|
||||
2
|
||||
);
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Local now timestamp converted to timeZoneName
|
||||
// and output as ISO 8601
|
||||
// (used to inform server of local client time)
|
||||
//
|
||||
clientLocalZoneTimeStamp(timeZoneName) {
|
||||
if (!timeZoneName) {
|
||||
timeZoneName = this.getResolvedTimeZoneName();
|
||||
}
|
||||
const ret = window.$gz.DateTime.local()
|
||||
.setZone(timeZoneName)
|
||||
.toString();
|
||||
return ret;
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Get default start date time in api format
|
||||
// (this is used to centralize and for future)
|
||||
defaultStartDateTime() {
|
||||
return {
|
||||
start: window.$gz.DateTime.local()
|
||||
.toUTC()
|
||||
.toString(),
|
||||
end: window.$gz.DateTime.local()
|
||||
.plus({ hours: 1 })
|
||||
.toUTC()
|
||||
.toString()
|
||||
};
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Convert a utc date to local time zone
|
||||
// and return date only portion only in iso 8601
|
||||
// format (used by time and date picker components)
|
||||
//
|
||||
utcDateStringToLocal8601DateOnlyString(value, timeZoneName) {
|
||||
if (!value) {
|
||||
//if no value, return the current time as expected by the time picker
|
||||
} else {
|
||||
//ok, the reason for sv-SE is that it's a locale that returns the time already in ISO format and 24hr by default
|
||||
//that can change over time so if this breaks that's why
|
||||
//also fr-CA does as well as possibly en-CA
|
||||
//https://stackoverflow.com/a/58633686/8939
|
||||
if (!timeZoneName) {
|
||||
timeZoneName = this.getResolvedTimeZoneName();
|
||||
}
|
||||
return new Date(value).toLocaleDateString("sv-SE", {
|
||||
timeZone: timeZoneName
|
||||
});
|
||||
}
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// Date/time past or future evaluation
|
||||
//
|
||||
dateIsPast(value) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Date(value) < new Date();
|
||||
},
|
||||
///////////////////////////////////////////
|
||||
// Turn a decimal number into a local
|
||||
// currency display
|
||||
//
|
||||
currencyLocalized(value, languageName, currencyName) {
|
||||
if (value == null) return "";
|
||||
if (!languageName) {
|
||||
languageName = this.getResolvedLanguage();
|
||||
}
|
||||
if (!currencyName) {
|
||||
currencyName = this.getCurrencyName();
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(languageName, {
|
||||
style: "currency",
|
||||
currency: currencyName
|
||||
}).format(value);
|
||||
},
|
||||
///////////////////////////////////////////
|
||||
// Turn a decimal number into a local
|
||||
// decimal format display
|
||||
//
|
||||
decimalLocalized(value, languageName) {
|
||||
if (value == null) return "";
|
||||
if (!languageName) {
|
||||
languageName = this.getResolvedLanguage();
|
||||
}
|
||||
//This forces 2 digits after the decimal
|
||||
// return new Intl.NumberFormat(languageName, {
|
||||
// minimumFractionDigits: 2
|
||||
// }).format(value);
|
||||
//this goes with whatever is the local format which for dev testing turned out to be perfect: 1.00 displays as 1 and 1.75 displays as 1.75
|
||||
//alignment goes out the window but it follows v7 format
|
||||
return new Intl.NumberFormat(languageName).format(value);
|
||||
},
|
||||
///////////////////////////////////////////
|
||||
// Turn a file / memory size number into a local
|
||||
// decimal format display and in reasonable human readable range
|
||||
//
|
||||
humanFileSize(bytes, languageName, si = false, dp = 1) {
|
||||
const thresh = si ? 1000 : 1024;
|
||||
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
return bytes + " B";
|
||||
}
|
||||
|
||||
const units = si
|
||||
? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
||||
: ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
|
||||
let u = -1;
|
||||
const r = 10 ** dp;
|
||||
|
||||
do {
|
||||
bytes /= thresh;
|
||||
++u;
|
||||
} while (
|
||||
Math.round(Math.abs(bytes) * r) / r >= thresh &&
|
||||
u < units.length - 1
|
||||
);
|
||||
|
||||
return (
|
||||
this.decimalLocalized(bytes.toFixed(dp), languageName) + " " + units[u]
|
||||
);
|
||||
}
|
||||
};
|
||||
46
client/src/api/notifypoll.js
Normal file
@@ -0,0 +1,46 @@
|
||||
let keepChecking = false;
|
||||
const DEFAULT_POLLING_INTERVAL = 60000;
|
||||
const MAX_POLLING_INTERVAL = 30 * 60 * 1000; //30 minutes maximum wait time
|
||||
export default {
|
||||
async startPolling() {
|
||||
if (keepChecking == true) {
|
||||
return;
|
||||
}
|
||||
keepChecking = true;
|
||||
//initial delay so it fetches "immediately"
|
||||
let pollingInterval = 3000;
|
||||
let status = null;
|
||||
while (keepChecking == true) {
|
||||
try {
|
||||
await window.$gz.util.sleepAsync(pollingInterval);
|
||||
if (keepChecking && window.$gz.store.state.authenticated) {
|
||||
if (window.$gz.erasingDatabase == false) {
|
||||
status = await window.$gz.api.get("notify/new-count");
|
||||
if (status.error) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(status));
|
||||
// throw new Error(status.error);
|
||||
} else {
|
||||
window.$gz.store.commit("setNewNotificationCount", status.data);
|
||||
//success so go to default in case it was changed by an error
|
||||
pollingInterval = DEFAULT_POLLING_INTERVAL;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
keepChecking = false;
|
||||
}
|
||||
} catch (error) {
|
||||
//fixup if fails on very first iteration with initial short polling interval
|
||||
if (pollingInterval < DEFAULT_POLLING_INTERVAL) {
|
||||
pollingInterval = DEFAULT_POLLING_INTERVAL;
|
||||
}
|
||||
pollingInterval *= 1.5;
|
||||
if (pollingInterval > MAX_POLLING_INTERVAL) {
|
||||
pollingInterval = MAX_POLLING_INTERVAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
stopPolling() {
|
||||
keepChecking = false;
|
||||
}
|
||||
};
|
||||
249
client/src/api/open-object-handler.js
Normal file
@@ -0,0 +1,249 @@
|
||||
import socktype from "./socktype";
|
||||
export default {
|
||||
///////////////////////////////
|
||||
// APP (GLOBAL) openobject CLICK HANDLER
|
||||
//
|
||||
// Deal with a request to open an object (from main datatables mainly)
|
||||
// if it's an open object url that triggered here the url would be in the format of {host/open/[socktype integer]/[id integer]}, i.e.
|
||||
// http://localhost:8080/open/2/105
|
||||
// called from App.vue
|
||||
handleOpenObjectClick(vm, tid) {
|
||||
//expects extra data (tid) to be one of { type: [AYATYPE], id: [RECORDID] }
|
||||
//NOTE: for new objects all edit pages assume record ID 0 (or null) means create rather than open
|
||||
|
||||
//for sake of ease of coding I'm going to assume null id also means make a new record intent
|
||||
//so I don't have to parse and decide constantly on forms for every control that has a open record link in it
|
||||
if (tid.id == null) {
|
||||
tid.id = 0;
|
||||
}
|
||||
|
||||
if (tid.type && tid.id != null) {
|
||||
const isCustomerTypeUser =
|
||||
window.$gz.store.state.userType == 3 ||
|
||||
window.$gz.store.state.userType == 4;
|
||||
//if these come from route parameters they may well be strings
|
||||
tid.type = Number.parseInt(tid.type, 10);
|
||||
tid.id = Number.parseInt(tid.id, 10);
|
||||
if (isCustomerTypeUser) {
|
||||
switch (tid.type) {
|
||||
case socktype.NotifySubscription:
|
||||
vm.$router.push({
|
||||
name: "home-notify-subscription",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-warning",
|
||||
`Customer user: open-object-handler unable to open link - [type:${tid.type}, id:${tid.id}]`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
switch (tid.type) {
|
||||
case socktype.Memo:
|
||||
vm.$router.push({
|
||||
name: "memo-edit",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
break;
|
||||
case socktype.Customer:
|
||||
vm.$router.push({
|
||||
name: "customer-edit",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
break;
|
||||
case socktype.CustomerNote:
|
||||
vm.$router.push({
|
||||
name: "customer-note-edit",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
break;
|
||||
case socktype.HeadOffice:
|
||||
vm.$router.push({
|
||||
name: "head-office-edit",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
break;
|
||||
|
||||
case socktype.User:
|
||||
//Is it an "Inside" user (staff or subcontractor)
|
||||
//or an "outside" user (customer or headoffice)
|
||||
//if key doesn't provide this then need to directly find out first before determining which form to redirect to
|
||||
if (tid.id != 0) {
|
||||
//lookup which one to open from server
|
||||
(async () => {
|
||||
try {
|
||||
//shortcut for superuser, always id 1
|
||||
if (tid.inside == undefined && tid.id == 1) {
|
||||
tid.inside = true;
|
||||
}
|
||||
if (tid.inside == undefined) {
|
||||
const res = await window.$gz.api.get(
|
||||
"user/inside-type/" + tid.id
|
||||
);
|
||||
if (res.error) {
|
||||
throw new Error(
|
||||
window.$gz.errorHandler.errorToString(res, vm)
|
||||
);
|
||||
}
|
||||
if (res.data) {
|
||||
tid.inside = res.data;
|
||||
}
|
||||
}
|
||||
if (tid.inside == true) {
|
||||
vm.$router.push({
|
||||
name: "adm-user",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
} else {
|
||||
vm.$router.push({
|
||||
name: "cust-user",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(e, vm));
|
||||
//throw new Error(e);
|
||||
}
|
||||
})();
|
||||
}
|
||||
break;
|
||||
case socktype.NotifySubscription:
|
||||
vm.$router.push({
|
||||
name: "home-notify-subscription",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
break;
|
||||
case socktype.FileAttachment:
|
||||
//lookup the actual type
|
||||
//then call this method again to do the actual open
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const res = await window.$gz.api.get(
|
||||
"attachment/parent/" + tid.id
|
||||
);
|
||||
|
||||
if (res.error) {
|
||||
throw new Error(
|
||||
window.$gz.errorHandler.errorToString(res, vm)
|
||||
);
|
||||
// throw new Error(res.error);
|
||||
}
|
||||
if (res.data.id && res.data.id != 0) {
|
||||
this.handleOpenObjectClick(vm, res.data);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
//throw new Error(e);
|
||||
throw new Error(window.$gz.errorHandler.errorToString(e, vm));
|
||||
}
|
||||
})();
|
||||
|
||||
break;
|
||||
|
||||
case socktype.Translation:
|
||||
vm.$router.push({
|
||||
name: "adm-translation",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
break;
|
||||
case socktype.Report:
|
||||
vm.$router.push({
|
||||
name: "sock-report-edit",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
break;
|
||||
case socktype.Backup:
|
||||
vm.$router.push({
|
||||
name: "ops-backup"
|
||||
});
|
||||
break;
|
||||
|
||||
case socktype.FormCustom:
|
||||
//all we have is the id, but need the formkey to open it
|
||||
(async () => {
|
||||
try {
|
||||
const res = await window.$gz.api.get(
|
||||
"form-custom/form-key/" + tid.id
|
||||
);
|
||||
|
||||
if (res.error) {
|
||||
throw new Error(
|
||||
window.$gz.errorHandler.errorToString(res, vm)
|
||||
);
|
||||
}
|
||||
if (res && res.data) {
|
||||
vm.$router.push({
|
||||
name: "sock-customize",
|
||||
params: {
|
||||
formCustomTemplateKey: res.data
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
//throw new Error(e);
|
||||
throw new Error(window.$gz.errorHandler.errorToString(e, vm));
|
||||
}
|
||||
})();
|
||||
break;
|
||||
|
||||
case socktype.Reminder:
|
||||
vm.$router.push({
|
||||
name: "reminder-edit",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
break;
|
||||
case socktype.Review:
|
||||
vm.$router.push({
|
||||
name: "review-edit",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
break;
|
||||
|
||||
case socktype.CustomerNotifySubscription:
|
||||
vm.$router.push({
|
||||
name: "cust-notify-subscription",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
break;
|
||||
case socktype.OpsNotificationSettings:
|
||||
vm.$router.push({
|
||||
name: "ops-notification-settings"
|
||||
});
|
||||
break;
|
||||
case socktype.Integration:
|
||||
vm.$router.push({
|
||||
name: "adm-integration",
|
||||
params: { recordid: tid.id }
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-warning",
|
||||
`open-object-handler: unknown [type:${tid.type}, id:${tid.id}]`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
///////////////////////////////////
|
||||
// WIRE UP MENU EVENTS
|
||||
//
|
||||
// called once from app.vue only
|
||||
//
|
||||
wireUpEventHandlers(vm) {
|
||||
const that = this;
|
||||
//expects extra data (tid) to be { type: [AYATYPE], id: [RECORDID] }
|
||||
window.$gz.eventBus.$on("openobject", function handleOpenObjectClickHandler(
|
||||
tid
|
||||
) {
|
||||
that.handleOpenObjectClick(vm, tid);
|
||||
});
|
||||
}
|
||||
//new functions above here
|
||||
};
|
||||
60
client/src/api/palette.js
Normal file
@@ -0,0 +1,60 @@
|
||||
//https://colorpalettes.net
|
||||
export default {
|
||||
color: {
|
||||
blue: "#1f77b4",
|
||||
red: "#d62728",
|
||||
orange: "#fe7f0e",
|
||||
green: "#2ca02c",
|
||||
purple: "#9c27b0",
|
||||
black: "#000000",
|
||||
cyan: "#00BCD4",
|
||||
teal: "#009688",
|
||||
primary: "#00205B", //APP Canucks dark blue
|
||||
secondary: "#00843D", //APP canucks green
|
||||
accent: "#db7022", //APP lighter orangey red, more friendly looking though not as much clarity it seems
|
||||
soft_sand: "#f1d3a1",
|
||||
soft_sand_taupe: "#e3dbd9",
|
||||
soft_pale_blue: "#e6eff6",
|
||||
soft_deep_blue: "#89b4c4",
|
||||
soft_green: "#ccdb86",
|
||||
soft_brown: "#c8bcb1",
|
||||
soft_brown_darker: "#8d7053",
|
||||
soft_gray: "#d2d7db"
|
||||
},
|
||||
getBoldPaletteArray(size) {
|
||||
const palette = [
|
||||
this.color.blue,
|
||||
this.color.red,
|
||||
this.color.green,
|
||||
this.color.orange,
|
||||
this.color.purple,
|
||||
this.color.cyan,
|
||||
this.color.teal,
|
||||
this.color.black
|
||||
];
|
||||
const paletteLength = palette.length;
|
||||
const ret = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
ret.push(palette[i % paletteLength]);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
getSoftPaletteArray(size) {
|
||||
const palette = [
|
||||
this.color.soft_sand,
|
||||
this.color.soft_pale_blue,
|
||||
this.color.soft_gray,
|
||||
this.color.soft_green,
|
||||
this.color.soft_brown,
|
||||
this.color.soft_deep_blue,
|
||||
this.color.soft_sand_taupe,
|
||||
this.color.soft_brown_darker
|
||||
];
|
||||
const paletteLength = palette.length;
|
||||
const ret = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
ret.push(palette[i % paletteLength]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
620
client/src/api/relative-date-filter-calculator.js
Normal file
@@ -0,0 +1,620 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/////////////////////////////////
|
||||
// Convert a date token to local
|
||||
// date range to UTC for server
|
||||
// dataListView consumption
|
||||
//
|
||||
export default {
|
||||
///////////////////////////////
|
||||
// token to date range
|
||||
//
|
||||
tokenToDates: function(token) {
|
||||
if (token == null || token.length == 0) {
|
||||
throw new Error(
|
||||
"relative-date-filter-calculator: date token is null or empty"
|
||||
);
|
||||
}
|
||||
|
||||
//return object contains the two dates that encompass the time period
|
||||
//the token represents to the local browser time zone but in UTC
|
||||
//and iso8601 format
|
||||
|
||||
//NOTE: it's valid for one of the two ret values might be undefined as it's valid to have a single date for
|
||||
//Past or Future
|
||||
const ret = { after: undefined, before: undefined };
|
||||
const dtNow = window.$gz.DateTime.local();
|
||||
const dtToday = window.$gz.DateTime.local(
|
||||
dtNow.year,
|
||||
dtNow.month,
|
||||
dtNow.day
|
||||
);
|
||||
let dtAfter = null;
|
||||
let dtBefore = null;
|
||||
|
||||
switch (token) {
|
||||
case "*yesterday*":
|
||||
//Between Day before yesterday at midnight and yesterday at midnight
|
||||
ret.after = dtToday
|
||||
.plus({ days: -1, seconds: -1 })
|
||||
.toUTC()
|
||||
.toString();
|
||||
ret.before = dtToday.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*today*":
|
||||
//Between yesterday at midnight and tommorow at midnight
|
||||
ret.after = dtToday
|
||||
.plus({ seconds: -1 })
|
||||
.toUTC()
|
||||
.toString();
|
||||
ret.before = dtToday
|
||||
.plus({ days: 1 })
|
||||
.toUTC()
|
||||
.toString();
|
||||
break;
|
||||
|
||||
case "*tomorrow*":
|
||||
//Between Tonight at midnight and day after tommorow at midnight
|
||||
ret.after = dtToday
|
||||
.plus({ days: 1, seconds: -1 })
|
||||
.toUTC()
|
||||
.toString();
|
||||
ret.before = dtToday
|
||||
.plus({ days: 2 })
|
||||
.toUTC()
|
||||
.toString();
|
||||
break;
|
||||
|
||||
case "*lastweek*":
|
||||
//Between two Sundays ago at midnight and last sunday at midnight
|
||||
|
||||
//go back a week
|
||||
dtAfter = dtToday.plus({ days: -7 });
|
||||
//go backwards to Sunday (In Luxon Monday is 1, Sunday is 7)
|
||||
while (dtAfter.weekday != 7) {
|
||||
dtAfter = dtAfter.plus({ days: -1 });
|
||||
}
|
||||
//go to very start of eighth dayahead
|
||||
dtBefore = dtAfter.plus({ days: 8 });
|
||||
//remove a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*thisweek*":
|
||||
//Between Sunday at midnight and Next sunday at midnight
|
||||
|
||||
//Start with today
|
||||
dtAfter = dtToday;
|
||||
//SET dtAfter to Monday start of this week
|
||||
//go backwards to monday (In Luxon Monday is 1, Sunday is 7)
|
||||
while (dtAfter.weekday != 1) {
|
||||
dtAfter = dtAfter.plus({ days: -1 });
|
||||
}
|
||||
//Now go back to sunday last second
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//Start with today
|
||||
dtBefore = dtToday;
|
||||
|
||||
//SET dtBefore to next monday
|
||||
//is it monday now?
|
||||
if (dtBefore.weekday == 1) {
|
||||
//Monday today? then go to next monday
|
||||
dtBefore = dtBefore.plus({ days: 7 });
|
||||
} else {
|
||||
//Find next monday...
|
||||
while (dtBefore.weekday != 1) {
|
||||
dtBefore = dtBefore.plus({ days: 1 });
|
||||
}
|
||||
}
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*nextweek*":
|
||||
//Between Next Sunday at midnight and Next Next sunday at midnight
|
||||
|
||||
//Start with today
|
||||
dtAfter = dtToday;
|
||||
//If today is monday skip over it first, we're looking for *next* monday, not this one
|
||||
if (dtAfter.weekday == 1) {
|
||||
dtAfter = dtAfter.plus({ days: 1 });
|
||||
}
|
||||
|
||||
//go forwards to next monday 12:00am (In Luxon Monday is 1, Sunday is 7)
|
||||
while (dtAfter.weekday != 1) {
|
||||
dtAfter = dtAfter.plus({ days: 1 });
|
||||
}
|
||||
//Now go back to sunday last second
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set dtBefore 7 days ahead of dtAfter
|
||||
//(sb BEFORE two mondays from now at zero hour so need to add a second due to prior removal of a second to make sunday)
|
||||
dtBefore = dtAfter.plus({ days: 7, seconds: 1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*lastmonth*":
|
||||
//start with the first day of this month
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, dtNow.month, 1);
|
||||
//subtract a Month
|
||||
dtAfter = dtAfter.plus({ months: -1 });
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*thismonth*":
|
||||
//start with the first day of this month
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, dtNow.month, 1);
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*nextmonth*":
|
||||
//start with the first day of this month
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, dtNow.month, 1);
|
||||
|
||||
//add a month
|
||||
dtAfter = dtAfter.plus({ months: 1 });
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*14daywindow*":
|
||||
//Start with today
|
||||
dtAfter = dtToday;
|
||||
|
||||
//subtract 7 days
|
||||
dtAfter = dtAfter.plus({ days: -7 });
|
||||
|
||||
//Add 15 days to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ days: 15 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*past*":
|
||||
//Any time before Now
|
||||
//set return values from calculated values
|
||||
ret.after = undefined;
|
||||
ret.before = dtNow.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*future*":
|
||||
//Any time after Now
|
||||
//set return values from calculated values
|
||||
ret.after = dtNow.toUTC().toString();
|
||||
ret.before = undefined;
|
||||
break;
|
||||
|
||||
case "*lastyear*":
|
||||
//"last year" means prior calendar year from start of january to end of december
|
||||
//start with the first day of this year
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year);
|
||||
|
||||
//subtract a year
|
||||
dtAfter = dtAfter.plus({ years: -1 });
|
||||
|
||||
//Before zero hour january 1st this year
|
||||
dtBefore = window.$gz.DateTime.local(dtNow.year);
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*thisyear*":
|
||||
//From zero hour january 1 this year (minus a second) to zero hour jan 1 next year
|
||||
//start with the first day of this year
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year);
|
||||
|
||||
//Before zero hour january 1st next year
|
||||
dtBefore = window.$gz.DateTime.local(dtNow.year);
|
||||
dtBefore = dtBefore.plus({ years: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*last3months*":
|
||||
//From Now minus 3 months
|
||||
dtAfter = dtToday.plus({ months: -3 });
|
||||
|
||||
//Before now
|
||||
dtBefore = dtNow;
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*last6months*":
|
||||
//From Now minus 6 months
|
||||
dtAfter = dtToday.plus({ months: -6 });
|
||||
|
||||
//Before now
|
||||
dtBefore = dtNow;
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*pastyear*": //within the prior 365 days before today
|
||||
//From Now minus 365 days
|
||||
dtAfter = dtToday.plus({ days: -365 });
|
||||
|
||||
//Before now
|
||||
dtBefore = dtNow;
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*past90days*":
|
||||
//From Now minus 90 days
|
||||
dtAfter = dtNow.plus({ days: -90 });
|
||||
|
||||
//Before now
|
||||
dtBefore = dtNow;
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*past30days*":
|
||||
//From Now minus 30 days
|
||||
dtAfter = dtNow.plus({ days: -30 });
|
||||
|
||||
//Before now
|
||||
dtBefore = dtNow;
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*past7days*":
|
||||
//From Now minus 7 days
|
||||
dtAfter = dtNow.plus({ days: -7 });
|
||||
|
||||
//Before now
|
||||
dtBefore = dtNow;
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*past24hours*":
|
||||
//From Now minus 24 hours
|
||||
dtAfter = dtNow.plus({ hours: -24 });
|
||||
|
||||
//Before now
|
||||
dtBefore = dtNow;
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*past6hours*":
|
||||
//From Now minus 6 hours
|
||||
dtAfter = dtNow.plus({ hours: -6 });
|
||||
|
||||
//Before now
|
||||
dtBefore = dtNow;
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
case "*january*":
|
||||
//This year specific month (month is 1 based)
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, 1, 1);
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*february*":
|
||||
//This year specific month (month is 1 based)
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, 2, 1);
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*march*":
|
||||
//This year specific month (month is 1 based)
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, 3, 1);
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*april*":
|
||||
//This year specific month (month is 1 based)
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, 4, 1);
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*may*":
|
||||
//This year specific month (month is 1 based)
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, 5, 1);
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*june*":
|
||||
//This year specific month (month is 1 based)
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, 6, 1);
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*july*":
|
||||
//This year specific month (month is 1 based)
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, 7, 1);
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*august*":
|
||||
//This year specific month (month is 1 based)
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, 8, 1);
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*september*":
|
||||
//This year specific month (month is 1 based)
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, 9, 1);
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*october*":
|
||||
//This year specific month (month is 1 based)
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, 10, 1);
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*november*":
|
||||
//This year specific month (month is 1 based)
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, 11, 1);
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*december*":
|
||||
//This year specific month (month is 1 based)
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, 12, 1);
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*lastyearlastmonth*":
|
||||
//start with the first day of this month
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, dtNow.month, 1);
|
||||
//subtract a Year and a Month
|
||||
dtAfter = dtAfter.plus({ years: -1, months: -1 });
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*lastyearthismonth*":
|
||||
//start with the first day of this month
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, dtNow.month, 1);
|
||||
//subtract a Year
|
||||
dtAfter = dtAfter.plus({ years: -1 });
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
case "*lastyearnextmonth*":
|
||||
//start with the first day of this month
|
||||
dtAfter = window.$gz.DateTime.local(dtNow.year, dtNow.month, 1);
|
||||
|
||||
//subtract a year, add a month
|
||||
dtAfter = dtAfter.plus({ years: -1, months: 1 });
|
||||
|
||||
//Add one month to dtAfter to get end date
|
||||
dtBefore = dtAfter.plus({ months: 1 });
|
||||
|
||||
//move after back a second for boundary
|
||||
dtAfter = dtAfter.plus({ seconds: -1 });
|
||||
|
||||
//set return values from calculated values
|
||||
ret.after = dtAfter.toUTC().toString();
|
||||
ret.before = dtBefore.toUTC().toString();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
"relative-date-time-filter-calculater: Date token [" +
|
||||
token +
|
||||
"] was not recognized"
|
||||
);
|
||||
//--------------------------
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
4
client/src/api/sockeye-version.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
version: "8.0.28",
|
||||
copyright: "© 1999-2022, Ground Zero Tech-Works Inc."
|
||||
};
|
||||
40
client/src/api/socktype.js
Normal file
@@ -0,0 +1,40 @@
|
||||
export default {
|
||||
NoType: 0,
|
||||
Global: 1,
|
||||
FormUserOptions: 2,
|
||||
User: 3,
|
||||
ServerState: 4,
|
||||
LogFile: 6,
|
||||
PickListTemplate: 7,
|
||||
Customer: 8,
|
||||
ServerJob: 9,
|
||||
ServerMetrics: 12,
|
||||
Translation: 13,
|
||||
UserOptions: 14,
|
||||
HeadOffice: 15,
|
||||
FileAttachment: 17,
|
||||
DataListSavedFilter: 18,
|
||||
FormCustom: 19,
|
||||
GlobalOps: 47, //really only used for rights, not an object type of any kind
|
||||
BizMetrics: 48, //deprecate? Not used for anything as of nov 2020
|
||||
Backup: 49,
|
||||
Notification: 50,
|
||||
NotifySubscription: 51,
|
||||
Reminder: 52,
|
||||
OpsNotificationSettings: 56,
|
||||
Report: 57,
|
||||
DashboardView: 58,
|
||||
CustomerNote: 59,
|
||||
Memo: 60,
|
||||
Review: 61,
|
||||
DataListColumnView: 68,
|
||||
CustomerNotifySubscription: 84, //proxy subs for customers
|
||||
Integration: 92 //3rd party or add-on integration data store
|
||||
};
|
||||
/**
|
||||
*
|
||||
* This is a mirror of SockType.cs in server project
|
||||
* To update just copy the contents of SockType.cs and replace " :" with ":" (without quotes obvsly)
|
||||
*
|
||||
*
|
||||
*/
|
||||
318
client/src/api/translation.js
Normal file
@@ -0,0 +1,318 @@
|
||||
export default {
|
||||
////////////////////////////////
|
||||
// Update the local cache
|
||||
//
|
||||
//
|
||||
async updateCache(editedTranslation) {
|
||||
//This function is only called if there is a requirement to refresh the local cache
|
||||
//either they just changed translations and saved it in user settings
|
||||
//or they just edited a translation and saved it in translation editor and it's also their own local translation
|
||||
|
||||
if (editedTranslation) {
|
||||
//iterate the keys that are cached and set them from whatever is in editedTranslation for that key
|
||||
for (const [key] of Object.entries(
|
||||
window.$gz.store.state.translationText
|
||||
)) {
|
||||
const display = editedTranslation.translationItems.find(
|
||||
z => z.key == key
|
||||
).display;
|
||||
|
||||
window.$gz.store.commit("setTranslationText", {
|
||||
key: key,
|
||||
value: display
|
||||
});
|
||||
}
|
||||
} else {
|
||||
//gather up the keys that are cached and fetch the latest and then replace them
|
||||
const needIt = [];
|
||||
Object.keys(window.$gz.store.state.translationText).forEach(z => {
|
||||
needIt.push(z);
|
||||
});
|
||||
//fetch these keys
|
||||
const transData = await window.$gz.api.upsert(
|
||||
"translation/subset",
|
||||
needIt
|
||||
);
|
||||
transData.data.forEach(function commitFetchedTranslationItemToStore(
|
||||
item
|
||||
) {
|
||||
window.$gz.store.commit("setTranslationText", item);
|
||||
});
|
||||
}
|
||||
},
|
||||
get(key) {
|
||||
if (!key) {
|
||||
console.trace("translation.js::get, no translation key was presented");
|
||||
return "";
|
||||
}
|
||||
//no translation for Wiki
|
||||
if (key == "Wiki") {
|
||||
return "Wiki";
|
||||
}
|
||||
if (!window.$gz.util.has(window.$gz.store.state.translationText, key)) {
|
||||
return "??" + key;
|
||||
}
|
||||
return window.$gz.store.state.translationText[key];
|
||||
},
|
||||
async cacheTranslations(keys, forceTranslationId) {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async function fetchTranslationKeysFromServer(resolve) {
|
||||
//
|
||||
//step 1: build an array of keys that we don't have already
|
||||
//Note: this will ensure only unique keys go into the store so it's safe to call this with dupes as can happen
|
||||
//for example datatables have dynamic column names so they need to fetch on demand
|
||||
const needIt = [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (
|
||||
!window.$gz.util.has(window.$gz.store.state.translationText, keys[i])
|
||||
) {
|
||||
needIt.push(keys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (needIt.length == 0) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
//step 2: get it
|
||||
let transData = null;
|
||||
|
||||
if (forceTranslationId) {
|
||||
transData = await window.$gz.api.upsert(
|
||||
`translation/subset/${forceTranslationId}`,
|
||||
needIt
|
||||
);
|
||||
} else {
|
||||
transData = await window.$gz.api.upsert("translation/subset", needIt);
|
||||
}
|
||||
|
||||
transData.data.forEach(function commitFetchedTranslationItemToStore(
|
||||
item
|
||||
) {
|
||||
window.$gz.store.commit("setTranslationText", item);
|
||||
});
|
||||
return resolve();
|
||||
});
|
||||
},
|
||||
//Keys that will always be required for any Sockeye work for any user
|
||||
coreKeys: [
|
||||
//main nav options
|
||||
"Home",
|
||||
"Dashboard",
|
||||
"Schedule",
|
||||
"MemoList",
|
||||
"ReviewList",
|
||||
"UserSettings",
|
||||
"SetLoginPassword",
|
||||
"NotifySubscriptionList",
|
||||
"UserPreferences",
|
||||
"Service",
|
||||
"CustomerList",
|
||||
"HeadOfficeList",
|
||||
"CustomerNotifySubscriptionList",
|
||||
"Contacts",
|
||||
"AdministrationGlobalSettings",
|
||||
"HelpLicense",
|
||||
"UserList",
|
||||
"Translation",
|
||||
"TranslationList",
|
||||
"ReportList",
|
||||
"ReminderList",
|
||||
"Accounting",
|
||||
"Administration",
|
||||
"Operations",
|
||||
"Attachments",
|
||||
"Review",
|
||||
"Extensions",
|
||||
"History",
|
||||
"Statistics",
|
||||
"Backup",
|
||||
"ServerState",
|
||||
"ServerJobs",
|
||||
"ServerLog",
|
||||
"ServerMetrics",
|
||||
"ServerProfiler",
|
||||
"OpsNotificationSettings",
|
||||
"ViewServerConfiguration",
|
||||
"NotificationCustomerDeliveryLog",
|
||||
"NotificationDeliveryLog",
|
||||
"HelpAboutSockeye",
|
||||
"MenuHelp",
|
||||
"More",
|
||||
"Logout",
|
||||
"Active",
|
||||
"Copy",
|
||||
"New",
|
||||
"Cancel",
|
||||
"Close",
|
||||
"Save",
|
||||
"SaveACopy",
|
||||
"Delete",
|
||||
"SoftDelete",
|
||||
"SoftDeleteAll",
|
||||
"Undelete",
|
||||
"Add",
|
||||
"Replace",
|
||||
"Remove",
|
||||
"OK",
|
||||
"Open",
|
||||
"Print",
|
||||
"Report",
|
||||
"Refresh",
|
||||
"Sort",
|
||||
"Duplicate",
|
||||
"RecordHistory",
|
||||
"Search",
|
||||
"TypeToSearchOrAdd",
|
||||
"SelectedItems",
|
||||
"AllItemsInList",
|
||||
"NoData",
|
||||
"Errors",
|
||||
"ErrorFieldLengthExceeded",
|
||||
"ErrorStartDateAfterEndDate",
|
||||
"ErrorRequiredFieldEmpty",
|
||||
"ErrorFieldValueNotInteger",
|
||||
"ErrorFieldValueNotDecimal",
|
||||
"ErrorAPI2000",
|
||||
"ErrorAPI2001",
|
||||
"ErrorAPI2002",
|
||||
"ErrorAPI2003",
|
||||
"ErrorAPI2004",
|
||||
"ErrorAPI2005",
|
||||
"ErrorAPI2006",
|
||||
"ErrorAPI2010",
|
||||
"ErrorAPI2020",
|
||||
"ErrorAPI2030",
|
||||
"ErrorAPI2040",
|
||||
"ErrorAPI2200",
|
||||
"ErrorAPI2201",
|
||||
"ErrorAPI2202",
|
||||
"ErrorAPI2203",
|
||||
"ErrorAPI2204",
|
||||
"ErrorAPI2205",
|
||||
"ErrorAPI2206",
|
||||
"ErrorAPI2207",
|
||||
"ErrorAPI2208",
|
||||
"ErrorAPI2209",
|
||||
"ErrorAPI2210",
|
||||
"ErrorAPI2212",
|
||||
"ErrorServerUnresponsive",
|
||||
"ErrorUserNotAuthenticated",
|
||||
"ErrorUserNotAuthorized",
|
||||
"ErrorNoMatch",
|
||||
"ErrorPickListQueryInvalid",
|
||||
"ErrorSecurityUserCapacity",
|
||||
"ErrorDBForeignKeyViolation",
|
||||
"DeletePrompt",
|
||||
"AreYouSureUnsavedChanges",
|
||||
"Leave",
|
||||
"Tags",
|
||||
"Tag",
|
||||
"Customize",
|
||||
"ObjectCustomFieldCustomGrid",
|
||||
"RowsPerPage",
|
||||
"PageOfPageText",
|
||||
"Loading",
|
||||
"Filter",
|
||||
"Heading",
|
||||
"Table",
|
||||
"InsertLink",
|
||||
"LinkUrl",
|
||||
"LinkText",
|
||||
"InsertImage",
|
||||
"ImageUrl",
|
||||
"ImageDescription",
|
||||
"AttachFile",
|
||||
"AttachmentNotes",
|
||||
"Upload",
|
||||
"AttachmentFileName",
|
||||
"FileAttachment",
|
||||
"MaintenanceExpired",
|
||||
"MaintenanceExpiredNote",
|
||||
"Import",
|
||||
"Export",
|
||||
"TimeSpanYears",
|
||||
"TimeSpanMonths",
|
||||
"TimeSpanDays",
|
||||
"TimeSpanHours",
|
||||
"TimeSpanMinutes",
|
||||
"TimeSpanSeconds",
|
||||
"DirectNotification",
|
||||
"UpdateAvailable",
|
||||
"DropFilesHere",
|
||||
"First",
|
||||
"Backward",
|
||||
"Forward",
|
||||
"Last",
|
||||
"GeoCapture",
|
||||
"GeoView",
|
||||
"CopyToClipboard",
|
||||
"SockType",
|
||||
"Now",
|
||||
"DateRangeToday",
|
||||
"ReportRenderTimeOut",
|
||||
"RenderingReport",
|
||||
"Settings",
|
||||
"IntegrationList"
|
||||
],
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// Take in a string that contains one or more
|
||||
//translation keys that start with LT:
|
||||
//translate each and replace and return the string translated
|
||||
// (fetch and cache any missing strings)
|
||||
async translateStringWithMultipleKeysAsync(s) {
|
||||
if (s == null) {
|
||||
return s;
|
||||
}
|
||||
let ret = s;
|
||||
|
||||
const found = s.match(/LT:[\w]*/gm);
|
||||
if (found == null) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
//clean up the keys for fetching
|
||||
const keysToCache = found.map(z => z.replace("LT:", ""));
|
||||
//cache / fetch any that are not already present
|
||||
await this.cacheTranslations(keysToCache);
|
||||
|
||||
//replace
|
||||
found.forEach(z => {
|
||||
const translated = this.get(z.replace("LT:", ""));
|
||||
//replace all
|
||||
ret = ret.split(z).join(translated);
|
||||
});
|
||||
|
||||
return ret;
|
||||
},
|
||||
////////////////////////////////////////////////////////
|
||||
// Take in a string that contains one or more
|
||||
//translation keys that start with LT:
|
||||
//translate each and replace and return the string translated
|
||||
// (DOES NOT fetch and cache any missing strings, they must exist)
|
||||
//this is the sync version to be used in non async capable code
|
||||
translateStringWithMultipleKeys(s) {
|
||||
let ret = s;
|
||||
const found = s.match(/LT:[\w]*/gm);
|
||||
if (found == null) {
|
||||
return ret;
|
||||
}
|
||||
//replace
|
||||
found.forEach(z => {
|
||||
const translated = this.get(z.replace("LT:", ""));
|
||||
//replace all
|
||||
ret = ret.split(z).join(translated);
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// dynamically set the vuetify language elements from
|
||||
// users translated text
|
||||
// Keeping vuetify using en locale and just adjusting on top of that
|
||||
//
|
||||
setVuetifyDefaultLanguageElements(vm) {
|
||||
vm.$vuetify.lang.locales.en.close = this.get("OK");
|
||||
}
|
||||
};
|
||||
23
client/src/assets/css/main.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.multi-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/*
|
||||
#nprogress .bar {
|
||||
height: 2px;
|
||||
background: rgb(255, 255, 0) !important;
|
||||
}
|
||||
|
||||
#nprogress .spinner .spinner-icon {
|
||||
border-top-color: #ffff00 !important;
|
||||
border-left-color: #ffff00 !important;
|
||||
}
|
||||
*/
|
||||
/* .aywiki > blockquote {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 50px;
|
||||
padding-left: 15px;
|
||||
border-left: 3px solid #ccc;
|
||||
background-color: rgb(245, 252, 255);
|
||||
} */
|
||||
BIN
client/src/assets/img/sockeye-128.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
client/src/assets/img/sockeye-64.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
client/src/assets/img/sockeye.ico
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
client/src/assets/img/sockeye.jpg
Normal file
|
After Width: | Height: | Size: 151 KiB |
1414
client/src/assets/img/sockeye.svg
Normal file
|
After Width: | Height: | Size: 96 KiB |
1414
client/src/assets/logo.svg
Normal file
|
After Width: | Height: | Size: 96 KiB |
42
client/src/components/_barebones_template.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
barebones template
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
default: null,
|
||||
type: Object
|
||||
},
|
||||
pvm: {
|
||||
default: null,
|
||||
type: Object
|
||||
},
|
||||
|
||||
formKey: { type: String, default: "" }, //used to grab template from store
|
||||
readonly: Boolean,
|
||||
disabled: Boolean
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {},
|
||||
|
||||
methods: {
|
||||
form() {
|
||||
return window.$gz.form;
|
||||
},
|
||||
fieldValueChanged(ref) {
|
||||
if (!this.pvm.formState.loading && !this.pvm.formState.readonly) {
|
||||
window.$gz.form.fieldValueChanged(this.pvm, ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
38
client/src/components/alert-control.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<v-col v-if="alertMessage" cols="12" mt-1 mb-2>
|
||||
<template v-if="popAlert">
|
||||
<v-alert
|
||||
v-show="alertMessage"
|
||||
ref="alertBox"
|
||||
data-cy="alertbox"
|
||||
color="accent"
|
||||
type="error"
|
||||
icon="$sockiExclamationTriangle"
|
||||
class="multi-line"
|
||||
outlined
|
||||
>{{ alertMessage }}</v-alert
|
||||
></template
|
||||
>
|
||||
<template v-else>
|
||||
<v-alert
|
||||
v-show="alertMessage"
|
||||
ref="alertBox"
|
||||
data-cy="alertbox"
|
||||
color="primary"
|
||||
icon="$sockiInfoCircle"
|
||||
class="multi-line"
|
||||
outlined
|
||||
>{{ alertMessage }}</v-alert
|
||||
></template
|
||||
>
|
||||
</v-col>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
alertMessage: { type: String, default: null },
|
||||
popAlert: { type: Boolean, default: false }
|
||||
},
|
||||
data: () => ({})
|
||||
};
|
||||
</script>
|
||||
387
client/src/components/attachment-control.vue
Normal file
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<div v-resize="onResize" class="mt-6">
|
||||
<div>
|
||||
<v-btn depressed tile @click="revealedClicked">
|
||||
{{ $sock.t("Attachments")
|
||||
}}<v-icon
|
||||
right
|
||||
v-text="reveal ? '$sockiEyeSlash' : '$sockiEye'"
|
||||
></v-icon
|
||||
></v-btn>
|
||||
</div>
|
||||
|
||||
<template v-if="reveal">
|
||||
<div>
|
||||
<template v-if="readonly">
|
||||
<!-- Note: this is just a copy of the inner part of read/write version below
|
||||
with the action taken out -->
|
||||
<div class="mt-4" :style="cardTextStyle()">
|
||||
<span v-if="!hasFiles()" class="text-h4">{{
|
||||
$sock.t("NoData")
|
||||
}}</span>
|
||||
|
||||
<v-list three-line>
|
||||
<v-list-item
|
||||
v-for="item in displayList"
|
||||
:key="item.id"
|
||||
:href="item.url"
|
||||
target="_blank"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-icon v-text="item.icon"></v-icon>
|
||||
</v-list-item-avatar>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle
|
||||
v-text="item.info"
|
||||
></v-list-item-subtitle>
|
||||
|
||||
<v-list-item-subtitle
|
||||
v-text="item.notes"
|
||||
></v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-tabs v-model="tab" color="primary">
|
||||
<v-tabs-slider></v-tabs-slider>
|
||||
<v-tab key="list"><v-icon>$sockiFolder</v-icon></v-tab>
|
||||
<v-tab key="attach"><v-icon>$sockiPaperclip</v-icon></v-tab>
|
||||
<v-tabs-items v-model="tab">
|
||||
<v-tab-item key="list">
|
||||
<div
|
||||
v-cloak
|
||||
id="dropDiv"
|
||||
class="mt-4"
|
||||
:style="cardTextStyle()"
|
||||
@drop.prevent="onDrop"
|
||||
@dragover.prevent="onDragOver"
|
||||
@dragleave="onDragEnd"
|
||||
>
|
||||
<span v-if="!hasFiles()">{{ $sock.t("DropFilesHere") }}</span>
|
||||
<v-list three-line>
|
||||
<v-list-item
|
||||
v-for="item in displayList"
|
||||
:key="item.id"
|
||||
:href="item.url"
|
||||
target="_blank"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-icon v-text="item.icon"></v-icon>
|
||||
</v-list-item-avatar>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title
|
||||
v-text="item.name"
|
||||
></v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle
|
||||
v-text="item.info"
|
||||
></v-list-item-subtitle>
|
||||
|
||||
<v-list-item-subtitle
|
||||
v-text="item.notes"
|
||||
></v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
|
||||
<v-list-item-action>
|
||||
<v-btn large icon @click="openEditMenu(item, $event)">
|
||||
<v-icon>$sockiEdit</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
<v-btn depressed tile class="mt-8" @click="revealedClicked">
|
||||
{{ $sock.t("Attachments")
|
||||
}}<v-icon
|
||||
right
|
||||
v-text="reveal ? '$sockiEyeSlash' : '$sockiEye'"
|
||||
></v-icon
|
||||
></v-btn>
|
||||
</v-tab-item>
|
||||
<v-tab-item key="attach">
|
||||
<div class="mt-8">
|
||||
<v-file-input
|
||||
v-model="uploadFiles"
|
||||
:label="$sock.t('AttachFile')"
|
||||
prepend-icon="$sockiPaperclip"
|
||||
multiple
|
||||
chips
|
||||
></v-file-input>
|
||||
|
||||
<v-text-field
|
||||
v-model="notes"
|
||||
:label="$sock.t('AttachmentNotes')"
|
||||
></v-text-field>
|
||||
<v-btn color="primary" text @click="upload">{{
|
||||
$sock.t("Upload")
|
||||
}}</v-btn>
|
||||
</div>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-tabs>
|
||||
<v-menu
|
||||
v-model="editMenu"
|
||||
min-width="360"
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
:position-x="menuX"
|
||||
:position-y="menuY"
|
||||
absolute
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ $sock.t("FileAttachment") }}</v-card-title>
|
||||
<div class="ma-6">
|
||||
<v-text-field
|
||||
v-model="editName"
|
||||
:label="$sock.t('AttachmentFileName')"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="editNotes"
|
||||
:label="$sock.t('AttachmentNotes')"
|
||||
></v-text-field>
|
||||
</div>
|
||||
<v-card-actions>
|
||||
<v-btn text @click="remove()">
|
||||
{{ $sock.t("Delete") }}
|
||||
</v-btn>
|
||||
<v-spacer v-if="!$vuetify.breakpoint.xs"></v-spacer>
|
||||
<v-btn text @click="editMenu = false">{{
|
||||
$sock.t("Cancel")
|
||||
}}</v-btn>
|
||||
<v-btn color="primary" text @click="saveEdit">{{
|
||||
$sock.t("OK")
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
sockType: { type: Number, default: null },
|
||||
sockId: { type: Number, default: null },
|
||||
readonly: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reveal: false,
|
||||
height: 300,
|
||||
displayList: [],
|
||||
notes: null,
|
||||
tab: null,
|
||||
uploadFiles: [],
|
||||
editMenu: false,
|
||||
menuX: 10,
|
||||
menuY: 10,
|
||||
editNotes: null,
|
||||
editName: null,
|
||||
editId: null
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
revealedClicked() {
|
||||
this.reveal = !this.reveal;
|
||||
if (this.reveal) {
|
||||
this.getList();
|
||||
}
|
||||
},
|
||||
onResize() {
|
||||
this.height = window.innerHeight * 0.8;
|
||||
},
|
||||
cardTextStyle() {
|
||||
return "height: " + this.height + "px;overflow-y:auto;";
|
||||
},
|
||||
hasFiles() {
|
||||
if (!this.displayList || this.displayList.length == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
async upload() {
|
||||
//similar code in wiki-control
|
||||
const vm = this;
|
||||
const fileData = [];
|
||||
for (let i = 0; i < vm.uploadFiles.length; i++) {
|
||||
let f = vm.uploadFiles[i];
|
||||
fileData.push({ name: f.name, lastModified: f.lastModified });
|
||||
}
|
||||
const at = {
|
||||
sockId: vm.sockId,
|
||||
sockType: vm.sockType,
|
||||
files: vm.uploadFiles,
|
||||
fileData: JSON.stringify(fileData), //note this is required for an array or it will come to the server as a string [object,object]
|
||||
notes: vm.notes ? vm.notes : ""
|
||||
};
|
||||
try {
|
||||
const res = await window.$gz.api.uploadAttachment(at);
|
||||
if (res.error) {
|
||||
window.$gz.errorHandler.handleFormError(res.error);
|
||||
} else {
|
||||
vm.uploadFiles = [];
|
||||
vm.updateDisplayList(res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error);
|
||||
}
|
||||
},
|
||||
async remove() {
|
||||
try {
|
||||
if ((await window.$gz.dialog.confirmDelete()) !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await window.$gz.api.remove("attachment/" + this.editId);
|
||||
if (res.error) {
|
||||
window.$gz.errorHandler.handleFormError(res.error);
|
||||
} else {
|
||||
this.editMenu = false;
|
||||
this.editName = null;
|
||||
this.editNotes = null;
|
||||
this.editId = null;
|
||||
this.getList();
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error);
|
||||
}
|
||||
},
|
||||
async getList() {
|
||||
const vm = this;
|
||||
try {
|
||||
const res = await window.$gz.api.get(
|
||||
"attachment/list?socktype=" + vm.sockType + "&ayaid=" + vm.sockId
|
||||
);
|
||||
if (res.error) {
|
||||
window.$gz.errorHandler.handleFormError(res.error);
|
||||
} else {
|
||||
vm.updateDisplayList(res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error);
|
||||
}
|
||||
},
|
||||
updateDisplayList(data) {
|
||||
//{"data":[{"id":1,"concurrency":7733332,"contentType":"image/png","displayFileName":"Screen Shot 2020-01-09 at 10.50.24.png","lastModified":"0001-01-01T00:00:00Z","notes":"Here are notes"},{"id":4,"concurrency":7733354,"contentType":"text/plain","displayFileName":"TNT log file sockeye.txt","lastModified":"0001-01-01T00:00:00Z","notes":"Here are notes"},{"id":2,"concurrency":7733342,"contentType":"text/plain","displayFileName":"stack.txt","lastModified":"0001-01-01T00:00:00Z","notes":"Here are notes"},{"id":3,"concurrency":7733348,"contentType":"image/jpeg","displayFileName":"t2cx6sloffk41.jpg","lastModified":"0001-01-01T00:00:00Z","notes":"Here are notes"}]}
|
||||
if (!data) {
|
||||
data = [];
|
||||
}
|
||||
|
||||
const timeZoneName = window.$gz.locale.getResolvedTimeZoneName();
|
||||
const languageName = window.$gz.locale.getResolvedLanguage();
|
||||
const hour12 = window.$gz.store.state.userOptions.hour12;
|
||||
const ret = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const o = data[i];
|
||||
ret.push({
|
||||
id: o.id,
|
||||
concurrency: o.concurrency,
|
||||
url: window.$gz.api.attachmentDownloadUrl(o.id, o.contentType),
|
||||
name: o.displayFileName,
|
||||
info: `${window.$gz.locale.utcDateToShortDateAndTimeLocalized(
|
||||
o.lastModified,
|
||||
timeZoneName,
|
||||
languageName,
|
||||
hour12
|
||||
)} ${o.attachedByUser} ${window.$gz.locale.humanFileSize(
|
||||
o.size,
|
||||
languageName,
|
||||
false,
|
||||
2
|
||||
)}`,
|
||||
|
||||
notes: o.notes ? o.notes : "",
|
||||
icon: window.$gz.util.iconForFile(o.displayFileName, o.contentType)
|
||||
});
|
||||
}
|
||||
this.displayList = ret;
|
||||
},
|
||||
openEditMenu(item, e) {
|
||||
e.preventDefault();
|
||||
this.editMenu = false;
|
||||
this.editName = item.name;
|
||||
this.editNotes = item.notes;
|
||||
this.editId = item.id;
|
||||
|
||||
this.menuX = e.clientX;
|
||||
this.menuY = e.clientY;
|
||||
this.$nextTick(() => {
|
||||
this.editMenu = true;
|
||||
});
|
||||
},
|
||||
async saveEdit() {
|
||||
const vm = this;
|
||||
if (!vm.editName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let o = null;
|
||||
let i = 0;
|
||||
for (i = 0; i < vm.displayList.length; i++) {
|
||||
if (vm.displayList[i].id == vm.editId) {
|
||||
o = vm.displayList[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (o.name == vm.editName && o.notes == vm.editNotes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = {
|
||||
concurrency: o.concurrency,
|
||||
displayFileName: vm.editName,
|
||||
notes: vm.editNotes
|
||||
};
|
||||
try {
|
||||
const res = await window.$gz.api.upsert("attachment/" + vm.editId, p);
|
||||
|
||||
if (res.error) {
|
||||
window.$gz.errorHandler.handleFormError(res.error);
|
||||
} else {
|
||||
vm.editMenu = false;
|
||||
vm.editName = null;
|
||||
vm.editNotes = null;
|
||||
vm.editId = null;
|
||||
//due to reactivity issues
|
||||
vm.updateDisplayList(res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error);
|
||||
}
|
||||
},
|
||||
onDrop(ev) {
|
||||
dropDiv.style.border = "none";
|
||||
dropDiv = null;
|
||||
//handle file drop
|
||||
var files = Array.from(ev.dataTransfer.files);
|
||||
if (files.length > 0) {
|
||||
this.uploadFiles = files;
|
||||
this.upload();
|
||||
}
|
||||
},
|
||||
onDragOver() {
|
||||
if (!dropDiv) {
|
||||
dropDiv = document.getElementById("dropDiv");
|
||||
}
|
||||
|
||||
dropDiv.style.border = "4px dashed #00ff00";
|
||||
},
|
||||
onDragEnd() {
|
||||
dropDiv.style.border = "none";
|
||||
dropDiv = null;
|
||||
}
|
||||
//-----
|
||||
}
|
||||
};
|
||||
let dropDiv = null;
|
||||
</script>
|
||||
43
client/src/components/chart-bar-control.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script>
|
||||
import { Bar } from "vue-chartjs";
|
||||
/*
|
||||
Bar,
|
||||
HorizontalBar,
|
||||
Doughnut,
|
||||
Line,
|
||||
Pie,
|
||||
PolarArea,
|
||||
Radar,
|
||||
Bubble,
|
||||
Scatter
|
||||
*/
|
||||
//https://vue-chartjs.org/guide/
|
||||
//https://www.chartjs.org/docs/latest/
|
||||
//https://dyclassroom.com/chartjs/how-to-create-a-pie-chart-using-chartjs
|
||||
export default {
|
||||
extends: Bar,
|
||||
props: {
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
chartData() {
|
||||
//these do nothing
|
||||
//this.$data._chart.update();
|
||||
//this.$data._chart.renderChart();
|
||||
|
||||
//this redraws the chart
|
||||
this.renderChart(this.chartData, this.options);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.renderChart(this.chartData, this.options);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
43
client/src/components/chart-bar-horizontal-control.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script>
|
||||
import { HorizontalBar } from "vue-chartjs";
|
||||
/*
|
||||
Bar,
|
||||
HorizontalBar,
|
||||
Doughnut,
|
||||
Line,
|
||||
Pie,
|
||||
PolarArea,
|
||||
Radar,
|
||||
Bubble,
|
||||
Scatter
|
||||
*/
|
||||
//https://vue-chartjs.org/guide/
|
||||
//https://www.chartjs.org/docs/latest/
|
||||
//https://dyclassroom.com/chartjs/how-to-create-a-pie-chart-using-chartjs
|
||||
export default {
|
||||
extends: HorizontalBar,
|
||||
props: {
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
chartData() {
|
||||
//these do nothing
|
||||
//this.$data._chart.update();
|
||||
//this.$data._chart.renderChart();
|
||||
|
||||
//this redraws the chart
|
||||
this.renderChart(this.chartData, this.options);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.renderChart(this.chartData, this.options);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
42
client/src/components/chart-line-control.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script>
|
||||
import { Line } from "vue-chartjs";
|
||||
/*
|
||||
Bar,
|
||||
HorizontalBar,
|
||||
Doughnut,
|
||||
Line,
|
||||
Pie,
|
||||
PolarArea,
|
||||
Radar,
|
||||
Bubble,
|
||||
Scatter
|
||||
*/
|
||||
//https://vue-chartjs.org/guide/
|
||||
//https://www.chartjs.org/docs/latest/
|
||||
export default {
|
||||
extends: Line,
|
||||
props: {
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
chartData() {
|
||||
//these do nothing
|
||||
//this.$data._chart.update();
|
||||
//this.$data._chart.renderChart();
|
||||
|
||||
//this redraws the chart
|
||||
this.renderChart(this.chartData, this.options);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.renderChart(this.chartData, this.options);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
78
client/src/components/currency-control.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-text-field
|
||||
ref="textField"
|
||||
v-currency="{
|
||||
currency: currencyName,
|
||||
locale: languageName
|
||||
}"
|
||||
dense
|
||||
:value="currencyValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="label"
|
||||
:rules="rules"
|
||||
:error-messages="errorMessages"
|
||||
:append-outer-icon="appendOuterIcon"
|
||||
@input="updateValue"
|
||||
@click:append-outer="$emit('gz-append-outer')"
|
||||
></v-text-field>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
//https://dm4t2.github.io/vue-currency-input/guide/#introduction :value="formattedValue"
|
||||
//https://codesandbox.io/s/vue-template-kd7d1?fontsize=14&module=%2Fsrc%2FApp.vue
|
||||
//https://github.com/dm4t2/vue-currency-input
|
||||
//https://github.com/dm4t2/vue-currency-input/releases
|
||||
|
||||
//NOTE: when get sick of this not working, can look into this: https://github.com/phiny1/v-currency-field
|
||||
//which is purported to be exactly what I'm trying to do here with a v-text-field but better I guess??
|
||||
//or look at the source for ideas?
|
||||
|
||||
import { parse } from "vue-currency-input";
|
||||
export default {
|
||||
props: {
|
||||
label: { type: String, default: null },
|
||||
rules: { type: Array, default: undefined },
|
||||
value: { type: Number, default: null },
|
||||
readonly: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
errorMessages: { type: Array, default: null },
|
||||
appendOuterIcon: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currencyName: window.$gz.locale.getCurrencyName(),
|
||||
languageName: window.$gz.locale.getResolvedLanguage(),
|
||||
initializing: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currencyValue() {
|
||||
return this.value;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateValue() {
|
||||
//this is required because the initial setting triggers an input event
|
||||
//however the two values differ because it comes from the server in much higher precision
|
||||
//and this control rounds it down
|
||||
//so the first trigger must be ignored until it "settles"
|
||||
if (this.initializing) {
|
||||
this.initializing = false;
|
||||
return;
|
||||
}
|
||||
const val = this.$refs.textField.$refs.input.value;
|
||||
|
||||
const parsedValue = parse(val, {
|
||||
currency: this.currencyName,
|
||||
locale: this.languageName
|
||||
});
|
||||
|
||||
if (parsedValue != this.value) {
|
||||
this.$emit("input", parsedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
479
client/src/components/custom-fields-control.vue
Normal file
@@ -0,0 +1,479 @@
|
||||
<template>
|
||||
<div v-if="availableCustomFields.length !== 0" class="mt-2">
|
||||
<span class="text-subtitle-1">
|
||||
{{ $sock.t("ObjectCustomFieldCustomGrid") }}
|
||||
</span>
|
||||
|
||||
<div class="mt-3">
|
||||
<v-row align-center justify-left row wrap>
|
||||
<template v-for="item in availableCustomFields">
|
||||
<v-col :key="item.fld" cols="12" sm="6" lg="4" xl="3" px-2>
|
||||
<!-- DATETIME -->
|
||||
<div v-if="item.type === 1">
|
||||
<gz-date-time-picker
|
||||
:ref="item.fld"
|
||||
v-model="_self[item.dataKey]"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="$sock.t(item.tKey)"
|
||||
:data-cy="item.fld"
|
||||
:error-messages="form().serverErrors(parentVM, item.fld)"
|
||||
:rules="[
|
||||
form().customFieldsCheck(
|
||||
parentVM,
|
||||
item,
|
||||
_self,
|
||||
$sock.t(item.tKey)
|
||||
)
|
||||
]"
|
||||
></gz-date-time-picker>
|
||||
</div>
|
||||
|
||||
<!-- DATE -->
|
||||
<div v-else-if="item.type === 2">
|
||||
<gz-date-picker
|
||||
:ref="item.fld"
|
||||
v-model="_self[item.dataKey]"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="$sock.t(item.tKey)"
|
||||
:data-cy="item.fld"
|
||||
:error-messages="form().serverErrors(parentVM, item.fld)"
|
||||
:rules="[
|
||||
form().customFieldsCheck(
|
||||
parentVM,
|
||||
item,
|
||||
_self,
|
||||
$sock.t(item.tKey)
|
||||
)
|
||||
]"
|
||||
></gz-date-picker>
|
||||
</div>
|
||||
<!-- TIME -->
|
||||
<div v-else-if="item.type === 3">
|
||||
<gz-time-picker
|
||||
:ref="item.fld"
|
||||
v-model="_self[item.dataKey]"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="$sock.t(item.tKey)"
|
||||
:data-cy="item.fld"
|
||||
:error-messages="form().serverErrors(parentVM, item.fld)"
|
||||
:rules="[
|
||||
form().customFieldsCheck(
|
||||
parentVM,
|
||||
item,
|
||||
_self,
|
||||
$sock.t(item.tKey)
|
||||
)
|
||||
]"
|
||||
></gz-time-picker>
|
||||
</div>
|
||||
<!-- TEXT -->
|
||||
<div v-else-if="item.type === 4">
|
||||
<v-textarea
|
||||
:ref="item.fld"
|
||||
v-model="_self[item.dataKey]"
|
||||
dense
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="$sock.t(item.tKey)"
|
||||
:data-cy="item.fld"
|
||||
:error-messages="form().serverErrors(parentVM, item.fld)"
|
||||
:rules="[
|
||||
form().customFieldsCheck(
|
||||
parentVM,
|
||||
item,
|
||||
_self,
|
||||
$sock.t(item.tKey)
|
||||
)
|
||||
]"
|
||||
auto-grow
|
||||
:clearable="!readonly"
|
||||
></v-textarea>
|
||||
</div>
|
||||
<!-- INTEGER -->
|
||||
<div v-else-if="item.type === 5">
|
||||
<v-text-field
|
||||
:ref="item.fld"
|
||||
v-model="_self[item.dataKey]"
|
||||
dense
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="$sock.t(item.tKey)"
|
||||
:data-cy="item.fld"
|
||||
:error-messages="form().serverErrors(parentVM, item.fld)"
|
||||
:rules="[
|
||||
form().customFieldsCheck(
|
||||
parentVM,
|
||||
item,
|
||||
_self,
|
||||
$sock.t(item.tKey)
|
||||
)
|
||||
]"
|
||||
:clearable="!readonly"
|
||||
:counter="10"
|
||||
type="number"
|
||||
step="none"
|
||||
></v-text-field>
|
||||
</div>
|
||||
<!-- BOOL -->
|
||||
<div v-else-if="item.type === 6">
|
||||
<v-checkbox
|
||||
:ref="item.fld"
|
||||
v-model="_self[item.dataKey]"
|
||||
dense
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="$sock.t(item.tKey)"
|
||||
:data-cy="item.fld"
|
||||
:error-messages="form().serverErrors(parentVM, item.fld)"
|
||||
:rules="[
|
||||
form().customFieldsCheck(
|
||||
parentVM,
|
||||
item,
|
||||
_self,
|
||||
$sock.t(item.tKey)
|
||||
)
|
||||
]"
|
||||
></v-checkbox>
|
||||
</div>
|
||||
<!-- DECIMAL -->
|
||||
<div v-else-if="item.type === 7">
|
||||
<gz-decimal
|
||||
:ref="item.fld"
|
||||
v-model="_self[item.dataKey]"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="$sock.t(item.tKey)"
|
||||
:data-cy="item.fld"
|
||||
:error-messages="form().serverErrors(parentVM, item.fld)"
|
||||
:rules="[
|
||||
form().customFieldsCheck(
|
||||
parentVM,
|
||||
item,
|
||||
_self,
|
||||
$sock.t(item.tKey)
|
||||
)
|
||||
]"
|
||||
></gz-decimal>
|
||||
</div>
|
||||
<!-- CURRENCY -->
|
||||
<div v-else-if="item.type === 8">
|
||||
<gz-currency
|
||||
:ref="item.fld"
|
||||
v-model="_self[item.dataKey]"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="$sock.t(item.tKey)"
|
||||
:data-cy="item.fld"
|
||||
:error-messages="form().serverErrors(parentVM, item.fld)"
|
||||
:rules="[
|
||||
form().customFieldsCheck(
|
||||
parentVM,
|
||||
item,
|
||||
_self,
|
||||
$sock.t(item.tKey)
|
||||
)
|
||||
]"
|
||||
></gz-currency>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="error"
|
||||
>UNKNOWN CUSTOM CONTROL TYPE: {{ item.type }}</span
|
||||
>
|
||||
</div>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
default: "{}",
|
||||
type: String
|
||||
},
|
||||
formKey: { type: String, default: "" }, //used to grab template from store
|
||||
keyStartWith: { type: String, default: "" }, //prefix of key names used to differentiate when more than one custom fields collection on same form (i.e. workorder, workorderitem, workoritemunit etc)
|
||||
readonly: Boolean,
|
||||
disabled: Boolean,
|
||||
parentVM: {
|
||||
default: null,
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
availableCustomFields() {
|
||||
//item.type only exists for custom fields so they are the ones to return
|
||||
//In addition if there is a keyStartWith then there are multiple custom field controls on same form so that's a different route to take
|
||||
const template = this.$store.state.formCustomTemplate[this.formKey];
|
||||
|
||||
if (template != undefined) {
|
||||
if (this.keyStartWith != "") {
|
||||
return template.filter(
|
||||
z => z.type != undefined && z.fld.includes(this.keyStartWith)
|
||||
);
|
||||
} else {
|
||||
//single custom control form, just return the fields
|
||||
return template.filter(z => z.type != undefined);
|
||||
}
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
c1: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c1");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c1", newValue);
|
||||
}
|
||||
},
|
||||
c2: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c2");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c2", newValue);
|
||||
}
|
||||
},
|
||||
c3: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c3");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c3", newValue);
|
||||
}
|
||||
},
|
||||
c4: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c4");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c4", newValue);
|
||||
}
|
||||
},
|
||||
c5: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c5");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c5", newValue);
|
||||
}
|
||||
},
|
||||
c6: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c6");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c6", newValue);
|
||||
}
|
||||
},
|
||||
c7: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c7");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c7", newValue);
|
||||
}
|
||||
},
|
||||
c8: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c8");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c8", newValue);
|
||||
}
|
||||
},
|
||||
c9: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c9");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c9", newValue);
|
||||
}
|
||||
},
|
||||
c10: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c10");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c10", newValue);
|
||||
}
|
||||
},
|
||||
c11: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c11");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c11", newValue);
|
||||
}
|
||||
},
|
||||
c12: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c12");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c12", newValue);
|
||||
}
|
||||
},
|
||||
c13: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c13");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c13", newValue);
|
||||
}
|
||||
},
|
||||
c14: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c14");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c14", newValue);
|
||||
}
|
||||
},
|
||||
c15: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c15");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c15", newValue);
|
||||
}
|
||||
},
|
||||
c16: {
|
||||
get: function() {
|
||||
return this.GetValueForField("c16");
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.SetValueForField("c16", newValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
form() {
|
||||
//nothing
|
||||
return window.$gz.form;
|
||||
},
|
||||
fieldValueChanged(ref) {
|
||||
if (
|
||||
!this.parentVM.formState.loading &&
|
||||
!this.parentVM.formState.readonly
|
||||
) {
|
||||
window.$gz.form.fieldValueChanged(this.parentVM, ref);
|
||||
}
|
||||
},
|
||||
GetValueForField: function(dataKey) {
|
||||
let cData = {};
|
||||
//get the data out of the JSON string value
|
||||
if (this.value != null) {
|
||||
cData = JSON.parse(this.value);
|
||||
}
|
||||
|
||||
//Custom field types can be changed by the user and cause old entered data to be invalid for that field type
|
||||
//Here we need to take action if the data is of an incompatible type for the control field type and attempt to coerce or simply nullify if not co-ercable the data
|
||||
// - CURRENT TEXT fields could handle any data so they don't need to be changed
|
||||
// - CURRENT BOOL fields can only handle empty or true false so they would need to be set null
|
||||
// - CURRENT TIME, DATE, DATETIME are pretty specific but all use a datetime string so any value not datetime like should be nulled
|
||||
// - CURRENT NUMBER, CURRENCY are also pretty specific but easy to identify if not fully numeric and then sb nulled or attempt to convert then null if not
|
||||
|
||||
const ctrlType = this.$store.state.formCustomTemplate[this.formKey].find(
|
||||
z => z.dataKey == dataKey
|
||||
).type;
|
||||
|
||||
//First get current value for the data that came from the server
|
||||
let ret = cData[dataKey];
|
||||
//Only process if value is non-null since all control types can handle null
|
||||
if (ret != null) {
|
||||
//check types that matter
|
||||
/*thes are all types, not necessarily all custom field types
|
||||
NoType = 0,
|
||||
DateTime = 1,
|
||||
Date = 2,
|
||||
Time = 3,
|
||||
Text = 4,
|
||||
Integer = 5,
|
||||
Bool = 6,
|
||||
Decimal = 7,
|
||||
Currency = 8,
|
||||
Tags = 9,
|
||||
Enum = 10,
|
||||
EmailAddress = 11,
|
||||
HTTP = 12,
|
||||
InternalId = 13,
|
||||
MemorySize=14
|
||||
*/
|
||||
switch (ctrlType) {
|
||||
//DateLike?
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
//can it be parsed into a date using the same library as the components use?
|
||||
if (!window.$gz.DateTime.fromISO(ret).isValid) {
|
||||
ret = null;
|
||||
}
|
||||
break;
|
||||
case 6:
|
||||
//if it's not already a boolean
|
||||
if (!window.$gz.util.isBoolean(ret)) {
|
||||
//it's not a bool and it's not null, it came from some other data type,
|
||||
//perhaps though, it's a truthy string so check for that before giving up and nulling
|
||||
if (window.$gz.util.isString(ret)) {
|
||||
ret = window.$gz.util.stringToBoolean(ret);
|
||||
break;
|
||||
}
|
||||
if (ret === 1) {
|
||||
ret = true;
|
||||
break;
|
||||
}
|
||||
if (ret === 0) {
|
||||
ret = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
case 7:
|
||||
if (!window.$gz.util.isNumeric(ret)) {
|
||||
ret = window.$gz.util.stringToFloat(ret);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
SetValueForField: function(dataKey, newValue) {
|
||||
//Get the current data out of the json string value
|
||||
//
|
||||
let cData = {};
|
||||
if (this.value != null) {
|
||||
cData = JSON.parse(this.value);
|
||||
}
|
||||
|
||||
if (!window.$gz.util.has(cData, dataKey)) {
|
||||
cData[dataKey] = null;
|
||||
}
|
||||
|
||||
//handle null or undefined
|
||||
if (newValue === null || newValue === undefined) {
|
||||
cData[dataKey] = null;
|
||||
} else {
|
||||
//then set item in the cData
|
||||
cData[dataKey] = newValue.toString();
|
||||
}
|
||||
|
||||
this.$emit("input", JSON.stringify(cData));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
229
client/src/components/dash-base.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<!-- <v-sheet color="white" height="420px" style="overflow: auto;" elevation="4"> -->
|
||||
<v-sheet style="overflow: auto;" elevation="4">
|
||||
<slot name="dash-title">
|
||||
<v-toolbar color="grey lighten-5" flat dense>
|
||||
<template v-if="showMoreButton">
|
||||
<v-btn
|
||||
text
|
||||
icon
|
||||
color="primary"
|
||||
@click="$emit('dash-more-click', id)"
|
||||
>
|
||||
<template v-if="count > 0">
|
||||
<v-badge inline>
|
||||
<template v-slot:badge>
|
||||
{{ count }} <span v-if="hasMoreItems">+</span>
|
||||
</template>
|
||||
<v-icon>{{ icon }}</v-icon>
|
||||
</v-badge>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-icon>{{ icon }}</v-icon>
|
||||
</template>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="count > 0">
|
||||
<v-badge inline class="mr-4">
|
||||
<template v-slot:badge>
|
||||
{{ count }} <span v-if="hasMoreItems">+</span>
|
||||
</template>
|
||||
<v-icon>{{ icon }}</v-icon>
|
||||
</v-badge>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-icon class="mr-4">{{ icon }}</v-icon>
|
||||
</template>
|
||||
</template>
|
||||
<v-toolbar-title> {{ displayTitle }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu bottom left>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on">
|
||||
<v-icon>$sockiEllipsisV</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-if="showMoreButton"
|
||||
@click="$emit('dash-more-click', id)"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $sock.t("More") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="showContextButton"
|
||||
@click="$emit('dash-context', id)"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>$sockiCog</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $sock.t("Settings") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="hasAddUrl" :to="addUrl">
|
||||
<v-list-item-icon>
|
||||
<v-icon>$sockiPlus</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $sock.t("New") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="$emit('dash-refresh')">
|
||||
<v-list-item-icon>
|
||||
<v-icon>$sockiSync</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $sock.t("Refresh") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="$emit('dash-move-start', id)">
|
||||
<v-list-item-icon>
|
||||
<v-icon>$sockiStepBackward</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $sock.t("First") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="$emit('dash-move-back', id)">
|
||||
<v-list-item-icon>
|
||||
<v-icon>$sockiBackward</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $sock.t("Backward") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="$emit('dash-move-forward', id)">
|
||||
<v-list-item-icon>
|
||||
<v-icon>$sockiForward</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $sock.t("Forward") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="$emit('dash-move-end', id)">
|
||||
<v-list-item-icon>
|
||||
<v-icon>$sockiStepForward</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $sock.t("Last") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="$emit('dash-remove', id)">
|
||||
<v-list-item-icon>
|
||||
<v-icon>$sockiTrashAlt</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $sock.t("Remove") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-toolbar>
|
||||
</slot>
|
||||
<slot name="settings">
|
||||
<div></div>
|
||||
</slot>
|
||||
<div v-if="hasError" class=" d-flex align-center">
|
||||
<v-alert
|
||||
data-cy="dash-error"
|
||||
color="error"
|
||||
icon="$sockiExclamationTriangle"
|
||||
class="multi-line"
|
||||
outlined
|
||||
>{{ errorMessage }}</v-alert
|
||||
>
|
||||
</div>
|
||||
<slot v-if="!hasError" name="main"
|
||||
><div class="ml-4 mt-1 d-flex align-center">
|
||||
<div>
|
||||
<span class="grey--text"
|
||||
>CONTENT CONTENT CONTENT CONTENT CONTENT CONTENT<br />CONTENT
|
||||
CONTENT CONTENT CONTENT CONTENT CONTENT<br />CONTENT CONTENT CONTENT
|
||||
CONTENT CONTENT CONTENT<br />CONTENT CONTENT CONTENT CONTENT CONTENT
|
||||
CONTENT<br />CONTENT CONTENT CONTENT CONTENT CONTENT CONTENT<br />CONTENT
|
||||
CONTENT CONTENT CONTENT CONTENT CONTENT<br />CONTENT CONTENT CONTENT
|
||||
CONTENT CONTENT CONTENT<br />CONTENT CONTENT CONTENT CONTENT CONTENT
|
||||
CONTENT<br />CONTENT CONTENT CONTENT CONTENT CONTENT CONTENT<br />CONTENT
|
||||
CONTENT CONTENT CONTENT CONTENT CONTENT</span
|
||||
>
|
||||
</div>
|
||||
</div></slot
|
||||
>
|
||||
</v-sheet>
|
||||
</template>
|
||||
<script>
|
||||
const KPI_LIST_MAX_ITEMS_TO_RETURN = 100;
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
title: { type: String, default: null },
|
||||
showMoreButton: { type: Boolean, default: false },
|
||||
showContextButton: { type: Boolean, default: false },
|
||||
addUrl: { type: String, default: null },
|
||||
count: { type: Number, default: 0 },
|
||||
updateFrequency: { type: Number, default: 60000 },
|
||||
maxListItems: { type: Number, default: 10 },
|
||||
icon: { type: String, default: "$sockiTachometer" },
|
||||
errorMessage: { type: String, default: null },
|
||||
settings: { type: Object, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timer: ""
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasAddUrl: function() {
|
||||
return this.addUrl && this.addUrl != "";
|
||||
},
|
||||
displayTitle() {
|
||||
if (this.settings && this.settings.customTitle) {
|
||||
return this.settings.customTitle;
|
||||
} else {
|
||||
return this.$sock.t(this.title);
|
||||
}
|
||||
},
|
||||
hasError() {
|
||||
return this.errorMessage != null && this.errorMessage.length > 0;
|
||||
},
|
||||
hasMoreItems() {
|
||||
//case 4200
|
||||
return this.count == KPI_LIST_MAX_ITEMS_TO_RETURN;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.updateFrequency > 0) {
|
||||
this.timer = setInterval(() => {
|
||||
this.refresh();
|
||||
}, this.updateFrequency + window.$gz.util.getRandomInt(60000)); //add up to 60 seconds so they don't all fire at once
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer);
|
||||
},
|
||||
methods: {
|
||||
refresh: function() {
|
||||
this.$emit("dash-refresh");
|
||||
},
|
||||
showContext: function() {
|
||||
this.$emit("dash-context");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
164
client/src/components/dash-today-reminders.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<gz-dash
|
||||
icon="$sockiStickyNote"
|
||||
:add-url="'home-reminders/0'"
|
||||
:show-more-button="false"
|
||||
:update-frequency="300000"
|
||||
:error-message="errorMessage"
|
||||
v-bind="$attrs"
|
||||
@dash-refresh="getDataFromApi()"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<template slot="main">
|
||||
<v-sheet height="400">
|
||||
<v-calendar
|
||||
ref="calendar"
|
||||
color="primary"
|
||||
type="day"
|
||||
hide-header
|
||||
:now="now"
|
||||
:interval-count="intervalCount"
|
||||
:first-time="startAt"
|
||||
:events="events"
|
||||
:event-color="getEventColor"
|
||||
:locale="languageName"
|
||||
@click:event="showEvent"
|
||||
>
|
||||
<template v-slot:event="{ event, eventSummary }">
|
||||
<div>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
:class="event.textColor + '--text'"
|
||||
v-html="eventSummary()"
|
||||
/><v-icon v-if="!event.editable" x-small :color="event.textColor">
|
||||
$sockiLock</v-icon
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</v-calendar>
|
||||
</v-sheet>
|
||||
</template>
|
||||
</gz-dash>
|
||||
</template>
|
||||
<script>
|
||||
import GzDash from "./dash-base.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GzDash
|
||||
},
|
||||
props: {
|
||||
maxListItems: { type: Number, default: 10 }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
events: [],
|
||||
errorMessage: null,
|
||||
timeZoneName: window.$gz.locale.getResolvedTimeZoneName(),
|
||||
languageName: window.$gz.locale.getResolvedLanguage(),
|
||||
hour12: window.$gz.locale.getHour12(),
|
||||
startAt: "00:00",
|
||||
intervalCount: 24,
|
||||
now: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {},
|
||||
created() {
|
||||
//console.log("reminders-created");
|
||||
},
|
||||
updated() {
|
||||
//console.log("reminders-updated");
|
||||
},
|
||||
beforeUpdate() {
|
||||
//console.log("reminders-beforeUpdate");
|
||||
},
|
||||
async mounted() {
|
||||
await this.getDataFromApi();
|
||||
},
|
||||
methods: {
|
||||
getEventColor(event) {
|
||||
return event.color;
|
||||
},
|
||||
showEvent({ nativeEvent, event }) {
|
||||
nativeEvent.stopPropagation();
|
||||
|
||||
window.$gz.eventBus.$emit("openobject", {
|
||||
type: event.type,
|
||||
id: event.id
|
||||
});
|
||||
},
|
||||
|
||||
async getDataFromApi() {
|
||||
//console.log("reminders-getdata");
|
||||
let now = new Date();
|
||||
|
||||
//set now for the calendar to trigger a refresh
|
||||
//if this doesn't work then need to trigger the change event: https://vuetifyjs.com/en/api/v-calendar/#events
|
||||
this.now = now.toLocaleString("sv-SE", {
|
||||
timeZone: this.timeZoneName
|
||||
});
|
||||
|
||||
//case 4198
|
||||
if (this.$refs && this.$refs.calendar) {
|
||||
this.$refs.calendar.scrollToTime({
|
||||
hour: now.getHours(),
|
||||
minute: 0
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
this.errorMessage = null;
|
||||
const now = window.$gz.locale.nowUTC8601String(this.timeZoneName);
|
||||
const res = await window.$gz.api.post("schedule/personal", {
|
||||
view: 1,
|
||||
dark: this.$store.state.darkMode,
|
||||
start: window.$gz.locale.addDurationToUTC8601String(now, {
|
||||
hours: -24
|
||||
}),
|
||||
end: window.$gz.locale.addDurationToUTC8601String(now, { hours: 24 }),
|
||||
wisu: false,
|
||||
reviews: false,
|
||||
reminders: true
|
||||
});
|
||||
if (res.error) {
|
||||
this.errorMessage = res.error;
|
||||
} else {
|
||||
this.events.splice(0);
|
||||
const timeZoneName = this.timeZoneName;
|
||||
let i = res.data.length;
|
||||
while (i--) {
|
||||
const x = res.data[i];
|
||||
this.events.push({
|
||||
start: new Date(
|
||||
new Date(x.start)
|
||||
.toLocaleString("sv-SE", {
|
||||
timeZone: timeZoneName
|
||||
})
|
||||
.replace(" ", "T")
|
||||
).getTime(),
|
||||
end: new Date(
|
||||
new Date(x.end)
|
||||
.toLocaleString("sv-SE", {
|
||||
timeZone: timeZoneName
|
||||
})
|
||||
.replace(" ", "T")
|
||||
).getTime(),
|
||||
timed: true,
|
||||
name: x.name,
|
||||
color: x.color,
|
||||
textColor: x.textColor,
|
||||
type: x.type,
|
||||
id: x.id,
|
||||
editable: x.editable,
|
||||
userId: x.userId
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorMessage = error.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
154
client/src/components/dash-today-reviews.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<gz-dash
|
||||
icon="$sockiCalendarCheck"
|
||||
:show-more-button="false"
|
||||
:update-frequency="300000"
|
||||
:error-message="errorMessage"
|
||||
v-bind="$attrs"
|
||||
@dash-refresh="getDataFromApi()"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<template slot="main">
|
||||
<v-sheet height="400">
|
||||
<v-calendar
|
||||
ref="rvwcalendar"
|
||||
color="primary"
|
||||
type="day"
|
||||
hide-header
|
||||
:now="now"
|
||||
:interval-count="intervalCount"
|
||||
:first-time="startAt"
|
||||
:events="events"
|
||||
:event-color="getEventColor"
|
||||
:locale="languageName"
|
||||
@click:event="showEvent"
|
||||
>
|
||||
<template v-slot:event="{ event, eventSummary }">
|
||||
<div>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
:class="event.textColor + '--text'"
|
||||
v-html="eventSummary()"
|
||||
/><v-icon v-if="!event.editable" x-small :color="event.textColor">
|
||||
$sockiLock</v-icon
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</v-calendar>
|
||||
</v-sheet>
|
||||
</template>
|
||||
</gz-dash>
|
||||
</template>
|
||||
<script>
|
||||
import GzDash from "./dash-base.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GzDash
|
||||
},
|
||||
props: {
|
||||
maxListItems: { type: Number, default: 10 }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
events: [],
|
||||
errorMessage: null,
|
||||
timeZoneName: window.$gz.locale.getResolvedTimeZoneName(),
|
||||
languageName: window.$gz.locale.getResolvedLanguage(),
|
||||
hour12: window.$gz.locale.getHour12(),
|
||||
startAt: "00:00",
|
||||
intervalCount: 24,
|
||||
now: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {},
|
||||
async mounted() {
|
||||
//must be called from mounted to have refs available
|
||||
await this.getDataFromApi();
|
||||
},
|
||||
methods: {
|
||||
getEventColor(event) {
|
||||
return event.color;
|
||||
},
|
||||
showEvent({ nativeEvent, event }) {
|
||||
nativeEvent.stopPropagation();
|
||||
|
||||
window.$gz.eventBus.$emit("openobject", {
|
||||
type: event.type,
|
||||
id: event.id
|
||||
});
|
||||
},
|
||||
|
||||
async getDataFromApi() {
|
||||
let now = new Date();
|
||||
|
||||
//set now for the calendar to trigger a refresh
|
||||
//if this doesn't work then need to trigger the change event: https://vuetifyjs.com/en/api/v-calendar/#events
|
||||
this.now = now.toLocaleString("sv-SE", {
|
||||
timeZone: this.timeZoneName
|
||||
});
|
||||
|
||||
//case 4198
|
||||
if (this.$refs && this.$refs.calendar) {
|
||||
this.$refs.rvwcalendar.scrollToTime({
|
||||
hour: now.getHours(),
|
||||
minute: 0
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
this.errorMessage = null;
|
||||
const now = window.$gz.locale.nowUTC8601String(this.timeZoneName);
|
||||
const res = await window.$gz.api.post("schedule/personal", {
|
||||
view: 1,
|
||||
dark: this.$store.state.darkMode,
|
||||
start: window.$gz.locale.addDurationToUTC8601String(now, {
|
||||
hours: -24
|
||||
}),
|
||||
end: window.$gz.locale.addDurationToUTC8601String(now, { hours: 24 }),
|
||||
wisu: false,
|
||||
reviews: true,
|
||||
reminders: false
|
||||
});
|
||||
if (res.error) {
|
||||
this.errorMessage = res.error;
|
||||
} else {
|
||||
this.events.splice(0);
|
||||
const timeZoneName = this.timeZoneName;
|
||||
let i = res.data.length;
|
||||
while (i--) {
|
||||
const x = res.data[i];
|
||||
this.events.push({
|
||||
start: new Date(
|
||||
new Date(x.start)
|
||||
.toLocaleString("sv-SE", {
|
||||
timeZone: timeZoneName
|
||||
})
|
||||
.replace(" ", "T")
|
||||
).getTime(),
|
||||
end: new Date(
|
||||
new Date(x.end)
|
||||
.toLocaleString("sv-SE", {
|
||||
timeZone: timeZoneName
|
||||
})
|
||||
.replace(" ", "T")
|
||||
).getTime(),
|
||||
timed: true,
|
||||
name: x.name,
|
||||
color: x.color,
|
||||
textColor: x.textColor,
|
||||
type: x.type,
|
||||
id: x.id,
|
||||
editable: x.editable,
|
||||
userId: x.userId
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorMessage = error.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
153
client/src/components/dash-today-scheduled-wo.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<gz-dash
|
||||
icon="$sockiUserClock"
|
||||
:add-url="'svc-workorders/0'"
|
||||
:show-more-button="false"
|
||||
:update-frequency="300000"
|
||||
:error-message="errorMessage"
|
||||
v-bind="$attrs"
|
||||
@dash-refresh="getDataFromApi()"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<template slot="main">
|
||||
<v-sheet height="400">
|
||||
<v-calendar
|
||||
ref="calendar"
|
||||
color="primary"
|
||||
type="day"
|
||||
hide-header
|
||||
:now="now"
|
||||
:interval-count="intervalCount"
|
||||
:first-time="startAt"
|
||||
:events="events"
|
||||
:event-color="getEventColor"
|
||||
:locale="languageName"
|
||||
@click:event="showEvent"
|
||||
>
|
||||
<template v-slot:event="{ event, eventSummary }">
|
||||
<div>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
:class="event.textColor + '--text'"
|
||||
v-html="eventSummary()"
|
||||
/><v-icon v-if="!event.editable" x-small :color="event.textColor">
|
||||
$sockiLock</v-icon
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</v-calendar>
|
||||
</v-sheet>
|
||||
</template>
|
||||
</gz-dash>
|
||||
</template>
|
||||
<script>
|
||||
import GzDash from "./dash-base.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GzDash
|
||||
},
|
||||
props: {
|
||||
maxListItems: { type: Number, default: 10 }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
events: [],
|
||||
errorMessage: null,
|
||||
timeZoneName: window.$gz.locale.getResolvedTimeZoneName(),
|
||||
languageName: window.$gz.locale.getResolvedLanguage(),
|
||||
hour12: window.$gz.locale.getHour12(),
|
||||
formUserOptions: {},
|
||||
startAt: "00:00",
|
||||
intervalCount: 24,
|
||||
now: null
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
async mounted() {
|
||||
await this.getDataFromApi();
|
||||
},
|
||||
methods: {
|
||||
getEventColor(event) {
|
||||
return event.color;
|
||||
},
|
||||
showEvent({ nativeEvent, event }) {
|
||||
nativeEvent.stopPropagation();
|
||||
|
||||
window.$gz.eventBus.$emit("openobject", {
|
||||
type: event.type,
|
||||
id: event.id
|
||||
});
|
||||
},
|
||||
|
||||
async getDataFromApi() {
|
||||
let now = new Date();
|
||||
//set now for the calendar to trigger a refresh
|
||||
//if this doesn't work then need to trigger the change event: https://vuetifyjs.com/en/api/v-calendar/#events
|
||||
this.now = now.toLocaleString("sv-SE", {
|
||||
timeZone: this.timeZoneName
|
||||
});
|
||||
|
||||
//case 4198
|
||||
if (this.$refs && this.$refs.calendar) {
|
||||
this.$refs.calendar.scrollToTime({
|
||||
hour: now.getHours(),
|
||||
minute: 0
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
this.errorMessage = null;
|
||||
const now = window.$gz.locale.nowUTC8601String(this.timeZoneName);
|
||||
const res = await window.$gz.api.post("schedule/personal", {
|
||||
view: 1,
|
||||
dark: this.$store.state.darkMode,
|
||||
start: window.$gz.locale.addDurationToUTC8601String(now, {
|
||||
hours: -24
|
||||
}),
|
||||
end: window.$gz.locale.addDurationToUTC8601String(now, { hours: 24 }),
|
||||
wisu: true, //workorder item scheduled user records
|
||||
reviews: false,
|
||||
reminders: false
|
||||
});
|
||||
if (res.error) {
|
||||
this.errorMessage = res.error;
|
||||
} else {
|
||||
this.events.splice(0);
|
||||
const timeZoneName = this.timeZoneName;
|
||||
let i = res.data.length;
|
||||
while (i--) {
|
||||
const x = res.data[i];
|
||||
this.events.push({
|
||||
start: new Date(
|
||||
new Date(x.start)
|
||||
.toLocaleString("sv-SE", {
|
||||
timeZone: timeZoneName
|
||||
})
|
||||
.replace(" ", "T")
|
||||
).getTime(),
|
||||
end: new Date(
|
||||
new Date(x.end)
|
||||
.toLocaleString("sv-SE", {
|
||||
timeZone: timeZoneName
|
||||
})
|
||||
.replace(" ", "T")
|
||||
).getTime(),
|
||||
timed: true,
|
||||
name: x.name,
|
||||
color: x.color,
|
||||
textColor: x.textColor,
|
||||
type: x.type,
|
||||
id: x.id,
|
||||
editable: x.editable,
|
||||
userId: x.userId
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorMessage = error.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
1076
client/src/components/data-table-filter-control.vue
Normal file
212
client/src/components/data-table-filter-manager-control.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="isVisible"
|
||||
max-width="600px"
|
||||
data-cy="dataTableFilterManagerControl"
|
||||
@keydown.esc="close()"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ $sock.t("Filter") }} </v-card-title>
|
||||
<v-card-subtitle class="mt-1"
|
||||
>{{ activeFilterNameAtOpen }} {{ activeFilterCreator }}</v-card-subtitle
|
||||
>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="activeFilter.name"
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('GridFilterName')"
|
||||
required
|
||||
></v-text-field>
|
||||
<v-checkbox
|
||||
ref="public"
|
||||
v-model="activeFilter.public"
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AnyUser')"
|
||||
data-cy="public"
|
||||
></v-checkbox>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn text color="primary" @click="close()">{{
|
||||
$sock.t("Cancel")
|
||||
}}</v-btn>
|
||||
<v-spacer />
|
||||
<template v-if="isSelfOwned">
|
||||
<v-btn text color="primary" @click="deleteFilter()">{{
|
||||
$sock.t("Delete")
|
||||
}}</v-btn>
|
||||
<v-spacer />
|
||||
</template>
|
||||
|
||||
<v-btn text color="primary" @click="saveAndExit(true)">{{
|
||||
$sock.t("SaveACopy")
|
||||
}}</v-btn>
|
||||
|
||||
<template v-if="activeFilter.defaultFilter == false && isSelfOwned">
|
||||
<v-spacer />
|
||||
<v-btn text color="primary" @click="saveAndExit()">{{
|
||||
$sock.t("Save")
|
||||
}}</v-btn>
|
||||
</template>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
dataListKey: { type: String, default: null },
|
||||
activeFilterId: { type: Number, default: null }
|
||||
},
|
||||
data: () => ({
|
||||
isVisible: false,
|
||||
resolve: null,
|
||||
reject: null,
|
||||
activeFilter: {
|
||||
id: 0,
|
||||
userId: 0,
|
||||
name: null,
|
||||
public: false,
|
||||
defaultFilter: true,
|
||||
listKey: null,
|
||||
filter: "[]"
|
||||
},
|
||||
activeFilterNameAtOpen: null,
|
||||
activeFilterCreator: "",
|
||||
isSelfOwned: true,
|
||||
formState: {
|
||||
ready: false,
|
||||
dirty: false,
|
||||
valid: true,
|
||||
readOnly: false,
|
||||
loading: true,
|
||||
errorBoxMessage: undefined,
|
||||
appError: undefined,
|
||||
serverError: {}
|
||||
}
|
||||
}),
|
||||
async created() {
|
||||
await initForm(this);
|
||||
},
|
||||
methods: {
|
||||
async deleteFilter() {
|
||||
//prompt if a true delete and not a default filter "reset"
|
||||
if (!this.activeFilter.defaultFilter) {
|
||||
const dialogResult = await window.$gz.dialog.confirmDelete();
|
||||
if (dialogResult != true) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await window.$gz.api.remove(
|
||||
`data-list-filter/${this.activeFilter.id}`
|
||||
);
|
||||
if (res.error) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(res, this));
|
||||
} else {
|
||||
this.close({ refresh: true });
|
||||
}
|
||||
},
|
||||
async saveAndExit(saveAs) {
|
||||
if (saveAs) {
|
||||
//SAVE AS
|
||||
//strip ID
|
||||
delete this.activeFilter.id;
|
||||
delete this.activeFilter.concurrency;
|
||||
|
||||
//save as can never save as default
|
||||
this.activeFilter.defaultFilter = false;
|
||||
//save as can never be same name as default -
|
||||
if (
|
||||
this.activeFilter.name == "-" ||
|
||||
this.activeFilter.name == this.activeFilterNameAtOpen
|
||||
) {
|
||||
this.activeFilter.name += " [" + this.$sock.t("Copy") + "]";
|
||||
}
|
||||
this.activeFilter.userId = window.$gz.store.state.userId;
|
||||
const res = await window.$gz.api.post(
|
||||
"data-list-filter",
|
||||
this.activeFilter
|
||||
);
|
||||
if (res.error) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(res, this));
|
||||
} else {
|
||||
this.close({ refresh: true, newFilterId: res.data.id });
|
||||
}
|
||||
} else {
|
||||
//SAVE
|
||||
const res = await window.$gz.api.put(
|
||||
"data-list-filter",
|
||||
this.activeFilter
|
||||
);
|
||||
if (res.error) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(res, this));
|
||||
} else {
|
||||
this.close({ refresh: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
async open(tableColumnData) {
|
||||
this.tableColumnData = tableColumnData;
|
||||
|
||||
await fetchActiveFilter(this);
|
||||
this.activeFilterNameAtOpen = this.activeFilter.name;
|
||||
this.isSelfOwned =
|
||||
this.activeFilter.userId == window.$gz.store.state.userId;
|
||||
|
||||
//Get owner name
|
||||
if (!this.isSelfOwned) {
|
||||
const res = await window.$gz.api.post("pick-list/list", {
|
||||
sockType: window.$gz.type.User,
|
||||
inactive: true,
|
||||
preselectedIds: [this.activeFilter.userId]
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
this.activeFilterCreator = " (creator: UNKNOWN / ERROR)";
|
||||
throw new Error(window.$gz.errorHandler.errorToString(res, this));
|
||||
} else {
|
||||
this.activeFilterCreator = `(${res.data[0].name})`;
|
||||
}
|
||||
}
|
||||
|
||||
this.isVisible = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
},
|
||||
close(ret) {
|
||||
this.isVisible = false;
|
||||
|
||||
this.resolve(ret);
|
||||
}
|
||||
}
|
||||
};
|
||||
/////////////////////////////////
|
||||
//
|
||||
//
|
||||
async function initForm() {
|
||||
await fetchTranslatedText();
|
||||
}
|
||||
|
||||
////////////////////
|
||||
//
|
||||
async function fetchActiveFilter(vm) {
|
||||
///api/v8/data-list-filter/{id}
|
||||
const res = await window.$gz.api.get(`data-list-filter/${vm.activeFilterId}`);
|
||||
if (res.error) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(res, vm));
|
||||
} else {
|
||||
vm.activeFilter = res.data;
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// Ensures UI translated text is available
|
||||
//
|
||||
async function fetchTranslatedText() {
|
||||
await window.$gz.translation.cacheTranslations(["GridFilterName", "AnyUser"]);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="isVisible"
|
||||
max-width="600px"
|
||||
data-cy="dataTableMobileFilterColumnSelectorControl"
|
||||
@keydown.esc="close()"
|
||||
>
|
||||
<v-list>
|
||||
<v-subheader>{{ $sock.t("Filter") }}</v-subheader>
|
||||
<v-list-item-group v-model="mobileSelectedFilterColumn" color="primary">
|
||||
<template v-for="(item, i) in headers">
|
||||
<v-list-item v-if="item.flt" :key="i" @click="close(item)">
|
||||
<v-list-item-icon>
|
||||
<v-icon :color="filterColor(item)">$sockiFilter</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.text"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
isVisible: false,
|
||||
resolve: null,
|
||||
reject: null,
|
||||
filterColor: null,
|
||||
mobileSelectedFilterColumn: null,
|
||||
headers: []
|
||||
}),
|
||||
|
||||
methods: {
|
||||
open(headers, filterColor) {
|
||||
this.headers = headers;
|
||||
this.filterColor = filterColor;
|
||||
this.isVisible = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
},
|
||||
close(ret) {
|
||||
this.isVisible = false;
|
||||
this.resolve(ret);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
1399
client/src/components/data-table.vue
Normal file
170
client/src/components/date-control.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row>
|
||||
<template v-if="!readonly">
|
||||
<template v-if="!$store.state.nativeDateTimeInput">
|
||||
<v-col cols="12">
|
||||
<v-dialog v-model="dlgdate" width="300px">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-text-field
|
||||
dense
|
||||
prepend-icon="$sockiCalendarAlt"
|
||||
:value="dateValue"
|
||||
:label="label"
|
||||
:rules="rules"
|
||||
readonly
|
||||
:error="!!hasErrors"
|
||||
v-on="on"
|
||||
@click:prepend="dlgdate = true"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<v-date-picker
|
||||
dense
|
||||
:value="dateValue"
|
||||
:locale="languageName"
|
||||
@input="updateDateValue"
|
||||
>
|
||||
<v-btn text color="primary" @click="$emit('input', null)">{{
|
||||
$sock.t("Delete")
|
||||
}}</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn text color="primary" @click="setToday()">{{
|
||||
$sock.t("DateRangeToday")
|
||||
}}</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="primary" @click="dlgdate = false">{{
|
||||
$sock.t("OK")
|
||||
}}</v-btn>
|
||||
</v-date-picker>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
</template>
|
||||
<template v-if="$store.state.nativeDateTimeInput">
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
ref="dateField"
|
||||
dense
|
||||
:value="dateValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="label"
|
||||
:rules="rules"
|
||||
type="date"
|
||||
:error-messages="errorMessages"
|
||||
:data-cy="`${dataCy}:time`"
|
||||
@change="updateDateValue"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
dense
|
||||
:value="readonlyFormat()"
|
||||
:label="label"
|
||||
readonly
|
||||
prepend-icon="$sockiCalendarAlt"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
<div class="v-messages theme--light error--text mt-n5" role="alert">
|
||||
<div class="v-messages__wrapper">
|
||||
<div class="v-messages__message">{{ allErrors() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
//******************************** NOTE: this control also captures the TIME even though it's DATE only, this is an intentional design decision to support field change to date or date AND time and is considered a display issue */
|
||||
export default {
|
||||
props: {
|
||||
label: { type: String, default: null },
|
||||
rules: { type: Array, default: undefined },
|
||||
errorMessages: { type: Array, default: null },
|
||||
value: { type: String, default: null },
|
||||
readonly: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
dataCy: { type: String, default: null }
|
||||
},
|
||||
data: () => ({
|
||||
dlgdate: false,
|
||||
timeZoneName: window.$gz.locale.getResolvedTimeZoneName(),
|
||||
languageName: window.$gz.locale.getResolvedLanguage()
|
||||
}),
|
||||
computed: {
|
||||
hasErrors() {
|
||||
return this.errorMessages != null && this.errorMessages.length > 0;
|
||||
},
|
||||
timeValue() {
|
||||
return window.$gz.locale.utcDateStringToLocal8601TimeOnlyString(
|
||||
this.value,
|
||||
this.timeZoneName
|
||||
);
|
||||
},
|
||||
dateValue() {
|
||||
return window.$gz.locale.utcDateStringToLocal8601DateOnlyString(
|
||||
this.value,
|
||||
this.timeZoneName
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setToday() {
|
||||
const v = window.$gz.locale
|
||||
.nowUTC8601String(this.timeZoneName)
|
||||
.split("T")[0];
|
||||
this.updateDateValue(v);
|
||||
this.dlgdate = false;
|
||||
},
|
||||
allErrors() {
|
||||
let ret = "";
|
||||
|
||||
if (this.errorMessages != null && this.errorMessages.length > 0) {
|
||||
ret += this.errorMessages.toString();
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
readonlyFormat() {
|
||||
return window.$gz.locale.utcDateToShortDateLocalized(
|
||||
this.value,
|
||||
this.timeZoneName,
|
||||
this.languageName
|
||||
);
|
||||
},
|
||||
updateDateValue(v) {
|
||||
this.updateValue(v, this.timeValue);
|
||||
this.dlgdate = false;
|
||||
},
|
||||
updateValue(theDate, theTime) {
|
||||
const vm = this;
|
||||
|
||||
if (!theDate) {
|
||||
const v = new Date();
|
||||
const fullYear = v.getFullYear();
|
||||
let fullMonth = v.getMonth() + 1;
|
||||
if (fullMonth < 10) {
|
||||
fullMonth = "0" + fullMonth.toString();
|
||||
}
|
||||
let fullDay = v.getDate();
|
||||
if (fullDay < 10) {
|
||||
fullDay = "0" + fullDay.toString();
|
||||
}
|
||||
theDate = fullYear + "-" + fullMonth + "-" + fullDay;
|
||||
}
|
||||
|
||||
if (!theTime) {
|
||||
theTime = "00:00:00";
|
||||
}
|
||||
|
||||
const ret = window.$gz.locale.localTimeDateStringToUTC8601String(
|
||||
theDate + "T" + theTime,
|
||||
vm.timeZoneName
|
||||
);
|
||||
vm.$emit("input", ret);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
249
client/src/components/date-time-control.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<v-row>
|
||||
<template v-if="!readonly">
|
||||
<template v-if="!$store.state.nativeDateTimeInput">
|
||||
<v-col xs6>
|
||||
<v-dialog v-model="dlgdate" width="300px">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-text-field
|
||||
dense
|
||||
prepend-icon="$sockiCalendarAlt"
|
||||
:value="dateValue"
|
||||
:label="label"
|
||||
:rules="rules"
|
||||
readonly
|
||||
:error="!!hasErrors"
|
||||
v-on="on"
|
||||
@click:prepend="dlgdate = true"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<v-date-picker
|
||||
dense
|
||||
:value="dateValue"
|
||||
:locale="languageName"
|
||||
@input="updateDateValue"
|
||||
>
|
||||
<v-btn text color="primary" @click="$emit('input', null)">{{
|
||||
$sock.t("Delete")
|
||||
}}</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="primary" @click="setToday()">{{
|
||||
$sock.t("DateRangeToday")
|
||||
}}</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="primary" @click="dlgdate = false">{{
|
||||
$sock.t("OK")
|
||||
}}</v-btn>
|
||||
</v-date-picker>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
<v-col xs6>
|
||||
<v-dialog v-model="dlgtime" width="300px">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-text-field
|
||||
dense
|
||||
:value="readonlyTimeFormat()"
|
||||
label
|
||||
prepend-icon="$sockiClock"
|
||||
readonly
|
||||
:error="!!hasErrors"
|
||||
v-on="on"
|
||||
@click:prepend="dlgtime = true"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<v-time-picker
|
||||
dense
|
||||
scrollable
|
||||
ampm-in-title
|
||||
:format="hour12 ? 'ampm' : '24hr'"
|
||||
:value="timeValue"
|
||||
@input="updateTimeValue"
|
||||
>
|
||||
<v-btn text color="primary" @click="$emit('input', null)">{{
|
||||
$sock.t("Delete")
|
||||
}}</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="primary" @click="setNow()">{{
|
||||
$sock.t("Now")
|
||||
}}</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="primary" @click="dlgtime = false">{{
|
||||
$sock.t("OK")
|
||||
}}</v-btn>
|
||||
</v-time-picker>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
</template>
|
||||
<template v-if="$store.state.nativeDateTimeInput">
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
ref="dateField"
|
||||
dense
|
||||
:value="dateValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="label"
|
||||
:rules="rules"
|
||||
type="date"
|
||||
:error-messages="errorMessages"
|
||||
:data-cy="`${dataCy}:date`"
|
||||
@change="updateDateValue"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
ref="timeField"
|
||||
dense
|
||||
:value="timeValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
type="time"
|
||||
:data-cy="`${dataCy}:time`"
|
||||
@change="updateTimeValue"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
dense
|
||||
:value="readonlyFormat()"
|
||||
:label="label"
|
||||
readonly
|
||||
prepend-icon="$sockiCalendarAlt"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</div>
|
||||
<div class="v-messages theme--light error--text mt-n5" role="alert">
|
||||
<div class="v-messages__wrapper">
|
||||
<div class="v-messages__message">{{ allErrors() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: { type: String, default: null },
|
||||
rules: { type: Array, default: undefined },
|
||||
errorMessages: { type: Array, default: null },
|
||||
value: { type: String, default: null },
|
||||
readonly: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
dataCy: { type: String, default: null }
|
||||
},
|
||||
data: () => ({
|
||||
nativeInput: true,
|
||||
dlgdate: false,
|
||||
dlgtime: false,
|
||||
timeZoneName: window.$gz.locale.getResolvedTimeZoneName(),
|
||||
languageName: window.$gz.locale.getResolvedLanguage(),
|
||||
hour12: window.$gz.locale.getHour12()
|
||||
}),
|
||||
computed: {
|
||||
hasErrors() {
|
||||
return this.errorMessages != null && this.errorMessages.length > 0;
|
||||
},
|
||||
timeValue() {
|
||||
return window.$gz.locale.utcDateStringToLocal8601TimeOnlyString(
|
||||
this.value,
|
||||
this.timeZoneName
|
||||
);
|
||||
},
|
||||
dateValue() {
|
||||
return window.$gz.locale.utcDateStringToLocal8601DateOnlyString(
|
||||
this.value,
|
||||
this.timeZoneName
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setToday() {
|
||||
//Was this but in afternoon shows the next day ?!?
|
||||
// const v = window.$gz.locale
|
||||
// .nowUTC8601String(this.timeZoneName)
|
||||
// .split("T")[0];
|
||||
|
||||
const v = window.$gz.DateTime.local()
|
||||
.toString()
|
||||
.split("T")[0];
|
||||
|
||||
this.updateDateValue(v);
|
||||
this.dlgdate = false;
|
||||
},
|
||||
setNow() {
|
||||
//was this but works the more simpler way copied from set today
|
||||
//const v = window.$gz.locale.nowUTC8601String().split("T")[1];
|
||||
|
||||
//now without the milliseconds
|
||||
var nowNoMs = window.$gz.DateTime.local().set({ milliseconds: 0 });
|
||||
const v = nowNoMs.toString().split("T")[1];
|
||||
|
||||
this.updateTimeValue(v);
|
||||
this.dlgtime = false;
|
||||
},
|
||||
allErrors() {
|
||||
let ret = "";
|
||||
|
||||
if (this.errorMessages != null && this.errorMessages.length > 0) {
|
||||
ret += this.errorMessages.toString();
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
readonlyFormat() {
|
||||
return window.$gz.locale.utcDateToShortDateAndTimeLocalized(
|
||||
this.value,
|
||||
this.timeZoneName,
|
||||
this.languageName,
|
||||
this.hour12
|
||||
);
|
||||
},
|
||||
readonlyTimeFormat() {
|
||||
return window.$gz.locale.utcDateToShortTimeLocalized(
|
||||
this.value,
|
||||
this.timeZoneName,
|
||||
this.languageName,
|
||||
this.hour12
|
||||
);
|
||||
},
|
||||
updateTimeValue(v) {
|
||||
this.updateValue(this.dateValue, v);
|
||||
},
|
||||
updateDateValue(v) {
|
||||
this.updateValue(v, this.timeValue);
|
||||
this.dlgdate = false;
|
||||
},
|
||||
updateValue(theDate, theTime) {
|
||||
const vm = this;
|
||||
if (!theDate) {
|
||||
const v = new Date();
|
||||
const fullYear = v.getFullYear();
|
||||
let fullMonth = v.getMonth() + 1;
|
||||
if (fullMonth < 10) {
|
||||
fullMonth = "0" + fullMonth.toString();
|
||||
}
|
||||
let fullDay = v.getDate();
|
||||
if (fullDay < 10) {
|
||||
fullDay = "0" + fullDay.toString();
|
||||
}
|
||||
theDate = fullYear + "-" + fullMonth + "-" + fullDay;
|
||||
}
|
||||
|
||||
if (!theTime) {
|
||||
theTime = "00:00:00";
|
||||
}
|
||||
|
||||
const ret = window.$gz.locale.localTimeDateStringToUTC8601String(
|
||||
theDate + "T" + theTime,
|
||||
vm.timeZoneName
|
||||
);
|
||||
vm.$emit("input", ret);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
71
client/src/components/days-of-week-control.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<v-select
|
||||
dense
|
||||
:items="daysOfWeek"
|
||||
item-text="name"
|
||||
item-value="id"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
:value="selectedValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="label"
|
||||
:rules="rules"
|
||||
:error-messages="errorMessages"
|
||||
:data-cy="'dayinput:' + testId"
|
||||
@input="handleInput"
|
||||
></v-select>
|
||||
</template>
|
||||
<script>
|
||||
//bitwise selection of days of week
|
||||
//https://stackoverflow.com/a/24174625/8939
|
||||
|
||||
export default {
|
||||
props: {
|
||||
label: { type: String, default: null },
|
||||
rules: { type: Array, default: undefined },
|
||||
errorMessages: { type: Array, default: null },
|
||||
value: { type: Number, default: 0 },
|
||||
readonly: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
testId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
internalValue: null,
|
||||
daysOfWeek: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedValue() {
|
||||
const ret = [];
|
||||
if (this.value != null && this.value != 0) {
|
||||
for (let i = 0; i < this.daysOfWeek.length; i++) {
|
||||
const day = this.daysOfWeek[i];
|
||||
if (this.value & day.id) {
|
||||
ret.push(day.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await window.$gz.enums.fetchEnumList("AyaDaysOfWeek");
|
||||
this.daysOfWeek = window.$gz.enums.getSelectionList("AyaDaysOfWeek", true);
|
||||
},
|
||||
methods: {
|
||||
handleInput(value) {
|
||||
let newValue = 0;
|
||||
if (value != null && value != [] && value.length > 0) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const day = value[i];
|
||||
newValue = newValue | day;
|
||||
}
|
||||
}
|
||||
this.$emit("input", newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
64
client/src/components/decimal-control.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-text-field
|
||||
ref="textField"
|
||||
v-currency="{
|
||||
currency: null,
|
||||
locale: languageName,
|
||||
precision: precision,
|
||||
allowNegative: true
|
||||
}"
|
||||
dense
|
||||
:value="currencyValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="label"
|
||||
:rules="rules"
|
||||
:error-messages="errorMessages"
|
||||
@input="updateValue"
|
||||
></v-text-field>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
//### NOTE: THIS IS A DUPLICATE OF CURRENCYCONTROL AND THE ONLY DIFFERENCE IS THE "currency:" VALUE IS SET TO NULL IN THE TEMPLATE AND IN THE updateValue METHOD
|
||||
//https://dm4t2.github.io/vue-currency-input/guide/#introduction :value="formattedValue"
|
||||
//https://codesandbox.io/s/vue-template-kd7d1?fontsize=14&module=%2Fsrc%2FApp.vue
|
||||
//https://github.com/dm4t2/vue-currency-input
|
||||
//https://github.com/dm4t2/vue-currency-input/releases
|
||||
import { parse } from "vue-currency-input";
|
||||
export default {
|
||||
props: {
|
||||
label: { type: String, default: null },
|
||||
rules: { type: Array, default: undefined },
|
||||
value: { type: Number, default: null },
|
||||
readonly: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
errorMessages: { type: Array, default: null },
|
||||
precision: { type: Number, default: undefined }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
languageName: window.$gz.locale.getResolvedLanguage()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currencyValue() {
|
||||
return this.value;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateValue() {
|
||||
const val = this.$refs.textField.$refs.input.value;
|
||||
const parsedValue = parse(val, {
|
||||
currency: null,
|
||||
locale: this.languageName
|
||||
});
|
||||
if (parsedValue == this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("input", parsedValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
162
client/src/components/duration-control.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div>
|
||||
<template>
|
||||
<v-row dense>
|
||||
<v-col
|
||||
><span class="text-caption">
|
||||
{{ label }}
|
||||
</span></v-col
|
||||
>
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
v-show="showDays"
|
||||
ref="daysPicker"
|
||||
dense
|
||||
:value="splitSpan.days"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="$sock.t('TimeSpanDays')"
|
||||
type="number"
|
||||
:data-cy="`${dataCy}:days`"
|
||||
:error="!!hasErrors"
|
||||
@input="updateSpan()"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
ref="hoursPicker"
|
||||
dense
|
||||
:value="splitSpan.hours"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="$sock.t('TimeSpanHours')"
|
||||
type="number"
|
||||
:data-cy="`${dataCy}:hours`"
|
||||
:error="!!hasErrors"
|
||||
@input="updateSpan()"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
ref="minutesPicker"
|
||||
dense
|
||||
:value="splitSpan.minutes"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="$sock.t('TimeSpanMinutes')"
|
||||
type="number"
|
||||
:data-cy="`${dataCy}:minutes`"
|
||||
:error="!!hasErrors"
|
||||
@input="updateSpan()"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
v-show="showSeconds"
|
||||
ref="secondsPicker"
|
||||
dense
|
||||
:value="splitSpan.seconds"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="$sock.t('TimeSpanSeconds')"
|
||||
type="number"
|
||||
:data-cy="`${dataCy}:seconds`"
|
||||
:error="!!hasErrors"
|
||||
@input="updateSpan()"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<div class="v-messages theme--light error--text mt-n5" role="alert">
|
||||
<div class="v-messages__wrapper">
|
||||
<div class="v-messages__message">{{ allErrors() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: { type: String, default: null },
|
||||
rules: { type: Array, default: undefined },
|
||||
errorMessages: { type: Array, default: null },
|
||||
value: { type: String, default: null },
|
||||
readonly: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
showSeconds: { type: Boolean, default: true },
|
||||
showDays: { type: Boolean, default: true },
|
||||
dataCy: { type: String, default: null }
|
||||
},
|
||||
computed: {
|
||||
hasErrors() {
|
||||
return this.errorMessages != null && this.errorMessages.length > 0;
|
||||
},
|
||||
splitSpan() {
|
||||
const vm = this;
|
||||
let theDays = 0;
|
||||
let theHours = 0;
|
||||
let theMinutes = 0;
|
||||
let theSeconds = 0;
|
||||
|
||||
if (vm.value == null) {
|
||||
theDays = 0;
|
||||
theHours = 0;
|
||||
theMinutes = 0;
|
||||
theSeconds = 0;
|
||||
} else {
|
||||
const work = vm.value.split(":");
|
||||
//has days?
|
||||
if (work[0].includes(".")) {
|
||||
const dh = work[0].split(".");
|
||||
theDays = Number(dh[0]);
|
||||
theHours = Number(dh[1]);
|
||||
} else {
|
||||
theHours = Number(work[0]);
|
||||
}
|
||||
theMinutes = Number(work[1]);
|
||||
//has milliseconds? (ignore them)
|
||||
if (work[2].includes(".")) {
|
||||
const dh = work[2].split(".");
|
||||
theSeconds = Number(dh[0]);
|
||||
} else {
|
||||
theSeconds = Number(work[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
days: theDays,
|
||||
hours: theHours,
|
||||
minutes: theMinutes,
|
||||
seconds: theSeconds
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
allErrors() {
|
||||
let ret = "";
|
||||
|
||||
if (this.errorMessages != null && this.errorMessages.length > 0) {
|
||||
ret += this.errorMessages.toString();
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
updateSpan() {
|
||||
let ret = "";
|
||||
//NOTE: even though a user may type a text value into the input, because it's set to "Number"
|
||||
//it always has a value of zero if it's not a digit even though the user typed Q for example (firefox at least)
|
||||
//so no parsing here to handle weird entries is required AFAICT
|
||||
const daysValue = this.$refs.daysPicker.$refs.input.value || 0;
|
||||
const hoursValue = this.$refs.hoursPicker.$refs.input.value || 0;
|
||||
const minutesValue = this.$refs.minutesPicker.$refs.input.value || 0;
|
||||
const secondsValue = this.$refs.secondsPicker.$refs.input.value || 0;
|
||||
|
||||
if (daysValue > 0) {
|
||||
ret = `${daysValue}.`;
|
||||
}
|
||||
|
||||
ret += `${hoursValue}:${minutesValue}:${secondsValue}`;
|
||||
this.$emit("input", ret);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
26
client/src/components/email-control.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<v-text-field
|
||||
dense
|
||||
v-bind="$attrs"
|
||||
type="email"
|
||||
prepend-icon="$sockiAt"
|
||||
v-on="$listeners"
|
||||
@click:prepend="openUrl"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
openUrl() {
|
||||
if (
|
||||
this.$el &&
|
||||
this.$el.attributes &&
|
||||
this.$el.attributes.value &&
|
||||
this.$el.attributes.value.value
|
||||
) {
|
||||
window.open(`mailto:${this.$el.attributes.value.value}`, "email");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
23
client/src/components/error-control.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<v-col v-if="errorBoxMessage" cols="12" mt-1 mb-2>
|
||||
<v-alert
|
||||
v-show="errorBoxMessage"
|
||||
ref="generalerror"
|
||||
dense
|
||||
data-cy="generalerror"
|
||||
color="error"
|
||||
icon="$sockiExclamationTriangle"
|
||||
class="multi-line"
|
||||
outlined
|
||||
>{{ errorBoxMessage }}</v-alert
|
||||
>
|
||||
</v-col>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
errorBoxMessage: { type: String, default: null }
|
||||
},
|
||||
data: () => ({})
|
||||
};
|
||||
</script>
|
||||
148
client/src/components/extension-delete-control.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<v-expansion-panel v-if="available == true">
|
||||
<v-expansion-panel-header
|
||||
disable-icon-rotate
|
||||
expand-icon="$sockiTrashAlt"
|
||||
>{{ $sock.t("Delete") }}</v-expansion-panel-header
|
||||
>
|
||||
<v-expansion-panel-content>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-icon x-large color="accent">$sockiSkullCrossbones</v-icon>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-btn icon @click="goHelp()"
|
||||
><v-icon>$sockiQuestionCircle</v-icon></v-btn
|
||||
>
|
||||
<v-btn
|
||||
:disabled="!canDoAction()"
|
||||
color="blue darken-1"
|
||||
text
|
||||
:loading="jobActive"
|
||||
@click="doAction()"
|
||||
>{{ $sock.t("StartJob") }}</v-btn
|
||||
>
|
||||
|
||||
<v-btn
|
||||
v-if="jobActive"
|
||||
color="red darken-1"
|
||||
text
|
||||
@click="requestCancel()"
|
||||
>
|
||||
{{ $sock.t("Cancel") }}</v-btn
|
||||
><span v-if="jobActive">{{ progress }}</span>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
dataListSelection: { type: Object, default: null }
|
||||
},
|
||||
data: () => ({
|
||||
jobActive: false,
|
||||
currentJobId: null,
|
||||
progress: "",
|
||||
rights: window.$gz.role.defaultRightsObject(),
|
||||
available: false
|
||||
}),
|
||||
async created() {
|
||||
const vm = this;
|
||||
await fetchTranslatedText();
|
||||
//NOTE: if extension doesn't support a particular object add it here to the NoType default
|
||||
if (
|
||||
vm.dataListSelection.SockType != 0 &&
|
||||
vm.dataListSelection.SockType != window.$gz.type.PartInventoryRestock
|
||||
) {
|
||||
vm.rights = window.$gz.role.getRights(vm.dataListSelection.SockType);
|
||||
}
|
||||
vm.available = vm.rights.change;
|
||||
},
|
||||
methods: {
|
||||
goHelp() {
|
||||
window.open(window.$gz.api.helpUrl() + "sock-ex-delete", "_blank");
|
||||
},
|
||||
canDoAction() {
|
||||
return true;
|
||||
},
|
||||
async requestCancel() {
|
||||
await window.$gz.api.upsert(
|
||||
"job-operations/request-cancel",
|
||||
this.currentJobId
|
||||
);
|
||||
},
|
||||
async doAction() {
|
||||
const vm = this;
|
||||
const dialogResult = await window.$gz.dialog.confirmGeneric(
|
||||
"EraseMultipleObjectsWarning",
|
||||
"error"
|
||||
);
|
||||
if (dialogResult == false) {
|
||||
return;
|
||||
}
|
||||
//Clear any possible prior errors
|
||||
vm.$emit("ext-show-job-log", "clear");
|
||||
|
||||
//do the batch action
|
||||
const url = "job-operations/batch-delete";
|
||||
const body = this.dataListSelection;
|
||||
|
||||
try {
|
||||
this.progress = "";
|
||||
//call api route
|
||||
let jobId = await window.$gz.api.upsert(url, body);
|
||||
if (jobId.error) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(jobId, vm));
|
||||
}
|
||||
|
||||
this.currentJobId = jobId.jobId;
|
||||
vm.jobActive = true;
|
||||
|
||||
let jobProgress = {};
|
||||
|
||||
while (vm.jobActive == true) {
|
||||
await window.$gz.util.sleepAsync(2000);
|
||||
jobProgress = await window.$gz.api.get(
|
||||
`job-operations/progress/${this.currentJobId}`
|
||||
);
|
||||
if (jobProgress.error) {
|
||||
throw new Error(
|
||||
window.$gz.errorHandler.errorToString(jobProgress, vm)
|
||||
);
|
||||
}
|
||||
jobProgress = jobProgress.data;
|
||||
this.progress = jobProgress.progress;
|
||||
|
||||
if (jobProgress.jobStatus == 4 || jobProgress.jobStatus == 0) {
|
||||
if (jobProgress.jobStatus == 4) {
|
||||
//emit job id and event to parent for log viewing
|
||||
vm.$emit("ext-show-job-log", jobId);
|
||||
}
|
||||
throw new Error("Job failed");
|
||||
}
|
||||
if (jobProgress.jobStatus == 3) {
|
||||
vm.jobActive = false;
|
||||
}
|
||||
}
|
||||
//Here if it's completed successfully
|
||||
window.$gz.eventBus.$emit("notify-success", vm.$sock.t("JobCompleted"));
|
||||
vm.$emit("ext-close-refresh");
|
||||
} catch (error) {
|
||||
vm.jobActive = false;
|
||||
window.$gz.eventBus.$emit("notify-error", vm.$sock.t("JobFailed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// Ensures UI translated text is available
|
||||
//
|
||||
async function fetchTranslatedText() {
|
||||
await window.$gz.translation.cacheTranslations([
|
||||
"EraseMultipleObjectsWarning"
|
||||
]);
|
||||
}
|
||||
</script>
|
||||
63
client/src/components/extension-export-control.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<v-expansion-panel v-if="available()">
|
||||
<v-expansion-panel-header
|
||||
disable-icon-rotate
|
||||
expand-icon="$sockiFileDownload"
|
||||
>{{ $sock.t("Export") }}</v-expansion-panel-header
|
||||
>
|
||||
<v-expansion-panel-content>
|
||||
<v-btn icon @click="goHelp()"
|
||||
><v-icon>$sockiQuestionCircle</v-icon></v-btn
|
||||
>
|
||||
<v-btn color="blue darken-1" text @click="doAction()">{{
|
||||
$sock.t("Export")
|
||||
}}</v-btn>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
dataListSelection: { type: Object, default: null }
|
||||
},
|
||||
data: () => ({
|
||||
jobActive: false
|
||||
}),
|
||||
methods: {
|
||||
available() {
|
||||
return (
|
||||
this.dataListSelection.SockType != 0 &&
|
||||
this.dataListSelection.SockType != window.$gz.type.PartInventoryRestock
|
||||
);
|
||||
},
|
||||
goHelp() {
|
||||
window.open(window.$gz.api.helpUrl() + "sock-ex-export", "_blank");
|
||||
},
|
||||
|
||||
async doAction() {
|
||||
try {
|
||||
const res = await window.$gz.api.upsert(
|
||||
`export/render`,
|
||||
this.dataListSelection
|
||||
);
|
||||
if (res.error) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(res, this));
|
||||
}
|
||||
|
||||
const href = window.$gz.api.genericDownloadUrl(
|
||||
"export/download/" + res.data
|
||||
);
|
||||
|
||||
if (window.open(href, "DownloadExport") == null) {
|
||||
throw new Error(
|
||||
"Unable to download, your browser rejected navigating to download url."
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error, this);
|
||||
window.$gz.eventBus.$emit("notify-error", this.$sock.t("JobFailed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
186
client/src/components/extension-tags-control.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<v-expansion-panel v-if="available == true">
|
||||
<v-expansion-panel-header disable-icon-rotate expand-icon="$sockiTags">{{
|
||||
$sock.t("Tags")
|
||||
}}</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-radio-group v-model="action">
|
||||
<v-radio :label="$sock.t('Add')" value="Add"></v-radio>
|
||||
<v-radio :label="$sock.t('Remove')" value="Remove"></v-radio>
|
||||
<v-radio :label="$sock.t('Replace')" value="Replace"></v-radio>
|
||||
</v-radio-group>
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
dense
|
||||
:value="tag"
|
||||
:label="$sock.t('Tag')"
|
||||
required
|
||||
@input="normalizeTag"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="action == 'Replace'" cols="12">
|
||||
<v-text-field
|
||||
dense
|
||||
:value="replace"
|
||||
:label="$sock.t('Replace')"
|
||||
required
|
||||
@input="normalizeReplace"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn icon @click="goHelp()"
|
||||
><v-icon>$sockiQuestionCircle</v-icon></v-btn
|
||||
>
|
||||
<v-btn
|
||||
:disabled="!canDoAction()"
|
||||
color="blue darken-1"
|
||||
text
|
||||
:loading="jobActive"
|
||||
@click="doAction()"
|
||||
>{{ $sock.t("StartJob") }}</v-btn
|
||||
>
|
||||
|
||||
<v-btn
|
||||
v-if="jobActive"
|
||||
color="red darken-1"
|
||||
text
|
||||
@click="requestCancel()"
|
||||
>
|
||||
{{ $sock.t("Cancel") }}</v-btn
|
||||
><span v-if="jobActive">{{ progress }}</span>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
dataListSelection: { type: Object, default: null }
|
||||
},
|
||||
data: () => ({
|
||||
action: "Add",
|
||||
tag: null,
|
||||
replace: null,
|
||||
jobActive: false,
|
||||
currentJobId: null,
|
||||
progress: "",
|
||||
rights: window.$gz.role.defaultRightsObject(),
|
||||
available: false
|
||||
}),
|
||||
created() {
|
||||
const vm = this;
|
||||
|
||||
if (
|
||||
vm.dataListSelection.SockType != 0 &&
|
||||
vm.dataListSelection.SockType != window.$gz.type.PartInventoryRestock
|
||||
) {
|
||||
vm.rights = window.$gz.role.getRights(vm.dataListSelection.SockType);
|
||||
}
|
||||
vm.available = vm.rights.change;
|
||||
},
|
||||
methods: {
|
||||
goHelp() {
|
||||
window.open(window.$gz.api.helpUrl() + "sock-ex-tags", "_blank");
|
||||
},
|
||||
canDoAction() {
|
||||
const vm = this;
|
||||
if (vm.action == "Replace" && !vm.replace) {
|
||||
return false;
|
||||
}
|
||||
if (vm.tag) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
async requestCancel() {
|
||||
await window.$gz.api.upsert(
|
||||
"job-operations/request-cancel",
|
||||
this.currentJobId
|
||||
);
|
||||
},
|
||||
async doAction() {
|
||||
let url = "tag-list/";
|
||||
switch (this.action) {
|
||||
case "Add":
|
||||
url += `batch-add/${this.tag}`;
|
||||
break;
|
||||
case "Remove":
|
||||
url += `batch-remove/${this.tag}`;
|
||||
break;
|
||||
case "Replace":
|
||||
url += `batch-replace/${this.tag}?toTag=${this.replace}`;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
this.progress = "";
|
||||
let jobId = await window.$gz.api.upsert(url, this.dataListSelection);
|
||||
if (jobId.error) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(jobId, this));
|
||||
}
|
||||
|
||||
this.currentJobId = jobId.jobId;
|
||||
this.jobActive = true;
|
||||
|
||||
let jobProgress = {};
|
||||
while (this.jobActive == true) {
|
||||
await window.$gz.util.sleepAsync(2000);
|
||||
jobProgress = await window.$gz.api.get(
|
||||
`job-operations/progress/${this.currentJobId}`
|
||||
);
|
||||
|
||||
if (jobProgress.error) {
|
||||
throw new Error(
|
||||
window.$gz.errorHandler.errorToString(jobProgress, this)
|
||||
);
|
||||
}
|
||||
jobProgress = jobProgress.data;
|
||||
this.progress = jobProgress.progress;
|
||||
|
||||
if (jobProgress.jobStatus == 4 || jobProgress.jobStatus == 0) {
|
||||
throw new Error("Job failed");
|
||||
}
|
||||
if (jobProgress.jobStatus == 3) {
|
||||
this.jobActive = false;
|
||||
}
|
||||
}
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-success",
|
||||
this.$sock.t("JobCompleted")
|
||||
);
|
||||
} catch (error) {
|
||||
this.jobActive = false;
|
||||
window.$gz.errorHandler.handleFormError(error, this);
|
||||
window.$gz.eventBus.$emit("notify-error", this.$sock.t("JobFailed"));
|
||||
}
|
||||
},
|
||||
normalize(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
//Must be lowercase per rules
|
||||
//This may be naive when we get international customers but for now supporting utf-8 and it appears it's safe to do this with unicode
|
||||
value = value.toLowerCase();
|
||||
//No spaces in tags, replace with dashes
|
||||
value = value.split(" ").join("-");
|
||||
//Remove multiple dash sequences
|
||||
value = value.replace(/-+/g, "-");
|
||||
|
||||
//Ensure doesn't start or end with a dash
|
||||
//linter says this is an unnecessary escape character so going with it but if issues with tag normalization here's maybe the culprit
|
||||
//value = value.replace(/^\-+-\-+$/g, "");
|
||||
value = value.replace(/^-+--+$/g, "");
|
||||
return value;
|
||||
},
|
||||
normalizeTag(value) {
|
||||
value = this.normalize(value);
|
||||
this.tag = value;
|
||||
},
|
||||
normalizeReplace(value) {
|
||||
value = this.normalize(value);
|
||||
this.replace = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
158
client/src/components/extensions-control.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<v-dialog v-model="isVisible" persistent data-cy="extensions">
|
||||
<v-card>
|
||||
<v-card-title>{{ $sock.t("Extensions") }}</v-card-title>
|
||||
<v-card-subtitle class="mt-1">{{ titleText() }}</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<template v-if="errorObj.length > 0">
|
||||
<div class="mt-4 mb-8">
|
||||
<v-icon large color="error">$sockiExclamationTriangle</v-icon>
|
||||
<v-data-table
|
||||
dense
|
||||
:headers="headers"
|
||||
:items="errorObj"
|
||||
class="elevation-4"
|
||||
:disable-pagination="true"
|
||||
:disable-filtering="true"
|
||||
hide-default-footer
|
||||
hide-default-header
|
||||
:no-data-text="$sock.t('NoData')"
|
||||
>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</template>
|
||||
<v-expansion-panels focusable>
|
||||
<ExtensionTags :data-list-selection="dataListSelection" />
|
||||
<ExtensionExport :data-list-selection="dataListSelection" />
|
||||
<ExtensionDelete
|
||||
:data-list-selection="dataListSelection"
|
||||
@ext-close-refresh="close({ refresh: true })"
|
||||
@ext-show-job-log="handleError($event)"
|
||||
/>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn text color="primary" @click="close()">{{
|
||||
$sock.t("Close")
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
import ExtensionTags from "./extension-tags-control.vue";
|
||||
import ExtensionExport from "./extension-export-control.vue";
|
||||
import ExtensionDelete from "./extension-delete-control.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ExtensionTags,
|
||||
ExtensionExport,
|
||||
ExtensionDelete
|
||||
},
|
||||
data: () => ({
|
||||
isVisible: false,
|
||||
resolve: null,
|
||||
reject: null,
|
||||
dataListSelection: {
|
||||
SockType: 0,
|
||||
selectedRowIds: [],
|
||||
dataListKey: null
|
||||
},
|
||||
headers: [],
|
||||
errorObj: [],
|
||||
timeZoneName: window.$gz.locale.getResolvedTimeZoneName(),
|
||||
languageName: window.$gz.locale.getResolvedLanguage(),
|
||||
hour12: window.$gz.locale.getHour12()
|
||||
}),
|
||||
async created() {
|
||||
await initForm(this);
|
||||
},
|
||||
methods: {
|
||||
titleText() {
|
||||
if (this.dataListSelection.selectedRowIds.length < 1) {
|
||||
return this.$sock.t("AllItemsInList");
|
||||
}
|
||||
return `${this.$sock.t("SelectedItems")} ${
|
||||
this.dataListSelection.selectedRowIds.length
|
||||
}`;
|
||||
},
|
||||
async handleError(jobId) {
|
||||
if (!jobId || jobId == "00000000-0000-0000-0000-000000000000") {
|
||||
throw "Error: extension triggered handleError with empty jobId";
|
||||
}
|
||||
|
||||
if (jobId == "clear") {
|
||||
this.errorObj = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await window.$gz.api.get(`job-operations/logs/${jobId}`);
|
||||
if (res.data) {
|
||||
const ret = [];
|
||||
for (let i = 0; i < res.data.length; i++) {
|
||||
const o = res.data[i];
|
||||
ret.push({
|
||||
id: i,
|
||||
created: window.$gz.locale.utcDateToShortDateAndTimeLocalized(
|
||||
o.created,
|
||||
this.timeZoneName,
|
||||
this.languageName,
|
||||
this.hour12
|
||||
),
|
||||
status: await window.$gz.translation.translateStringWithMultipleKeysAsync(
|
||||
o.statusText
|
||||
),
|
||||
jobId:
|
||||
o.jobId == "00000000-0000-0000-0000-000000000000" ? "" : o.jobId
|
||||
});
|
||||
}
|
||||
|
||||
this.errorObj = ret;
|
||||
} else {
|
||||
this.errorObj = [];
|
||||
}
|
||||
},
|
||||
open(dls) {
|
||||
this.errorObj = [];
|
||||
this.dataListSelection = dls;
|
||||
this.isVisible = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
},
|
||||
close(ret) {
|
||||
this.isVisible = false;
|
||||
this.errorObj = [];
|
||||
this.resolve(ret);
|
||||
}
|
||||
}
|
||||
};
|
||||
/////////////////////////////////
|
||||
//
|
||||
//
|
||||
async function initForm(vm) {
|
||||
await fetchTranslatedText(vm);
|
||||
await createTableHeaders(vm);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// Ensures UI translated text is available
|
||||
//
|
||||
async function fetchTranslatedText() {
|
||||
await window.$gz.translation.cacheTranslations(["TimeStamp", "ID", "Status"]);
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
//
|
||||
//
|
||||
async function createTableHeaders(vm) {
|
||||
vm.headers = [
|
||||
{ text: vm.$sock.t("TimeStamp"), value: "created" },
|
||||
{ text: vm.$sock.t("Status"), value: "status" },
|
||||
{ text: vm.$sock.t("ID"), value: "jobId" }
|
||||
];
|
||||
}
|
||||
</script>
|
||||
135
client/src/components/gzconfirm.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog
|
||||
v-model="isVisible"
|
||||
persistent
|
||||
:max-width="maxWidth"
|
||||
:width="width"
|
||||
data-cy="gzconfirm"
|
||||
@keydown.esc="cancel"
|
||||
>
|
||||
<v-card elevation="24">
|
||||
<v-card-title class="text-h6 text-sm-h5 grey lighten-4">
|
||||
<template v-if="options.type == 'success'">
|
||||
<v-icon large color="success">$sockiCheckCircle</v-icon>
|
||||
</template>
|
||||
<template v-if="options.type == 'info'">
|
||||
<v-icon large color="info">$sockiInfoCircle</v-icon>
|
||||
</template>
|
||||
<template v-if="options.type == 'question'">
|
||||
<v-icon large color="info">$sockiQuestionCircle</v-icon>
|
||||
</template>
|
||||
<template v-if="options.type == 'warning'">
|
||||
<v-icon large color="warning">$sockiExclamationCircle</v-icon>
|
||||
</template>
|
||||
<template v-if="options.type == 'error'">
|
||||
<v-icon large color="error">$sockiExclamationTriangle</v-icon>
|
||||
</template>
|
||||
<template v-if="options.type == 'dire'">
|
||||
<v-icon large color="error">$sockiSkullCrossbones</v-icon>
|
||||
</template>
|
||||
<span v-if="options.title" class="ml-5"> {{ options.title }} </span>
|
||||
</v-card-title>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<v-card-text
|
||||
class="text-body-1 text-sm-h6 my-5"
|
||||
v-html="options.message"
|
||||
>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
v-if="options.helpUrl"
|
||||
data-cy="gzconfirm:morebutton"
|
||||
text
|
||||
@click="helpClick()"
|
||||
>
|
||||
{{ this.$root.$gz.translation.get("More") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="options.noButtonText"
|
||||
color="primary"
|
||||
text
|
||||
data-cy="gzconfirm:nobutton"
|
||||
@click.native="cancel"
|
||||
>{{ options.noButtonText }}</v-btn
|
||||
>
|
||||
|
||||
<v-btn
|
||||
v-if="options.type == 'dire'"
|
||||
class="ml-4"
|
||||
color="error"
|
||||
data-cy="gzconfirm:yesbutton"
|
||||
@click.native="agree"
|
||||
>
|
||||
<v-icon left>$sockiSkullCrossbones</v-icon
|
||||
>{{ options.yesButtonText }}
|
||||
<v-icon right>$sockiSkullCrossbones</v-icon></v-btn
|
||||
>
|
||||
|
||||
<v-btn
|
||||
v-else
|
||||
color="primary"
|
||||
text
|
||||
data-cy="gzconfirm:yesbutton"
|
||||
@click.native="agree"
|
||||
>{{ options.yesButtonText }}</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
width: 290,
|
||||
maxWidth: 290,
|
||||
isVisible: false,
|
||||
resolve: null,
|
||||
reject: null,
|
||||
options: {
|
||||
title: null,
|
||||
message: null,
|
||||
yesButtonText: null,
|
||||
noButtonText: null,
|
||||
type: "info" //one of success, info, question, warning, and error, see v-alert docs for more info
|
||||
}
|
||||
}),
|
||||
methods: {
|
||||
open(options) {
|
||||
if (options.message.includes("\n")) {
|
||||
options.message = options.message.replace(/\n/g, "<br />");
|
||||
}
|
||||
this.options = Object.assign(this.options, options);
|
||||
this.maxWidth = Math.floor(window.innerWidth * 0.9);
|
||||
const calculatedWidth = Math.floor(window.innerWidth * 0.5);
|
||||
if (calculatedWidth < 290) {
|
||||
this.width = 290;
|
||||
} else if (calculatedWidth > 800) {
|
||||
this.width = 800;
|
||||
} else {
|
||||
this.width = calculatedWidth;
|
||||
}
|
||||
|
||||
this.isVisible = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
},
|
||||
agree() {
|
||||
this.resolve(true);
|
||||
this.isVisible = false;
|
||||
},
|
||||
cancel() {
|
||||
this.resolve(false);
|
||||
this.isVisible = false;
|
||||
},
|
||||
helpClick() {
|
||||
window.open(this.options.helpUrl, "_blank");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
79
client/src/components/gznotify.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<v-snackbar
|
||||
data-cy="gznotify"
|
||||
:value="isVisible"
|
||||
:color="currentNotification.type"
|
||||
multi-line
|
||||
>
|
||||
<v-alert :type="currentNotification.type" mode="out-in">
|
||||
{{ currentNotification.message }}
|
||||
</v-alert>
|
||||
|
||||
<v-btn
|
||||
v-if="currentNotification.helpUrl"
|
||||
data-cy="gznotify:morebutton"
|
||||
text
|
||||
@click="helpClick()"
|
||||
>
|
||||
{{ this.$root.$gz.translation.get("More") }}
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
<script>
|
||||
const DEFAULT_NOTIFY_OPTIONS = { type: "info", timeout: 3000 };
|
||||
export default {
|
||||
data: () => ({
|
||||
isVisible: false,
|
||||
processing: false,
|
||||
notificationQueue: [],
|
||||
currentNotification: {
|
||||
type: "info", //one of success, info, warning, and error, see v-alert docs for more info
|
||||
timeout: 3000,
|
||||
message: null,
|
||||
helpUrl: null
|
||||
}
|
||||
}),
|
||||
methods: {
|
||||
addNotification(options) {
|
||||
if (!options.message) {
|
||||
return;
|
||||
}
|
||||
if (!options.type) {
|
||||
options.type = DEFAULT_NOTIFY_OPTIONS.type;
|
||||
}
|
||||
if (!options.timeout) {
|
||||
options.timeout = DEFAULT_NOTIFY_OPTIONS.timeout;
|
||||
}
|
||||
this.notificationQueue.push(options);
|
||||
//trigger the notification queue handler if it isn't already in action
|
||||
if (!this.processing) {
|
||||
this.handleNotifications();
|
||||
}
|
||||
},
|
||||
handleNotifications() {
|
||||
this.processing = true;
|
||||
//Process the queue
|
||||
if (this.notificationQueue.length > 0) {
|
||||
//Move the next item into the current slot
|
||||
this.currentNotification = this.notificationQueue.shift();
|
||||
//If don't use nextTick then don't get all visible when multiple in sequence
|
||||
this.$nextTick(() => {
|
||||
this.isVisible = true;
|
||||
});
|
||||
//Show it for the designated time before moving on to the next
|
||||
setTimeout(() => {
|
||||
this.isVisible = false;
|
||||
//recurse
|
||||
this.handleNotifications();
|
||||
}, this.currentNotification.timeout);
|
||||
} else {
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
},
|
||||
helpClick() {
|
||||
window.open(this.currentNotification.helpUrl, "_blank");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
62
client/src/components/percent-control.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<v-text-field
|
||||
ref="textField"
|
||||
v-currency="{
|
||||
currency: null,
|
||||
locale: languageName,
|
||||
precision: precision
|
||||
}"
|
||||
dense
|
||||
:value="currencyValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="label"
|
||||
:rules="rules"
|
||||
:error-messages="errorMessages"
|
||||
append-icon="$sockiPercent"
|
||||
@input="updateValue"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<script>
|
||||
//### NOTE: THIS IS A DUPLICATE OF CURRENCYCONTROL AND THE ONLY DIFFERENCE IS THE "currency:" VALUE IS SET TO NULL IN THE TEMPLATE AND IN THE updateValue METHOD
|
||||
//https://dm4t2.github.io/vue-currency-input/guide/#introduction :value="formattedValue"
|
||||
//https://codesandbox.io/s/vue-template-kd7d1?fontsize=14&module=%2Fsrc%2FApp.vue
|
||||
//https://github.com/dm4t2/vue-currency-input
|
||||
//https://github.com/dm4t2/vue-currency-input/releases
|
||||
import { parse } from "vue-currency-input";
|
||||
export default {
|
||||
props: {
|
||||
label: { type: String, default: null },
|
||||
rules: { type: Array, default: undefined },
|
||||
value: { type: Number, default: null },
|
||||
readonly: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
errorMessages: { type: Array, default: null },
|
||||
precision: { type: Number, default: 3 }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
languageName: window.$gz.locale.getResolvedLanguage()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currencyValue() {
|
||||
return this.value;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateValue() {
|
||||
const val = this.$refs.textField.$refs.input.value;
|
||||
const parsedValue = parse(val, {
|
||||
currency: null,
|
||||
locale: this.languageName
|
||||
});
|
||||
|
||||
if (parsedValue == this.value) {
|
||||
return;
|
||||
}
|
||||
this.$emit("input", parsedValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
26
client/src/components/phone-control.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<v-text-field
|
||||
dense
|
||||
v-bind="$attrs"
|
||||
type="tel"
|
||||
prepend-icon="$sockiPhoneAlt"
|
||||
v-on="$listeners"
|
||||
@click:prepend="openUrl"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
openUrl() {
|
||||
if (
|
||||
this.$el &&
|
||||
this.$el.attributes &&
|
||||
this.$el.attributes.value &&
|
||||
this.$el.attributes.value.value
|
||||
) {
|
||||
window.open(`tel:${this.$el.attributes.value.value}`, "phone");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
411
client/src/components/pick-list.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-autocomplete
|
||||
dense
|
||||
:value="value"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
return-object
|
||||
:items="searchResults"
|
||||
:label="label"
|
||||
:hint="hint"
|
||||
persistent-hint
|
||||
item-text="name"
|
||||
item-value="id"
|
||||
item-disabled="!active"
|
||||
:rules="rules"
|
||||
:error-messages="errorMessages"
|
||||
:loading="fetching"
|
||||
:placeholder="$sock.t('Search')"
|
||||
:search-input.sync="searchEntry"
|
||||
:filter="customFilter"
|
||||
hide-no-data
|
||||
:clearable="!readonly && canClear"
|
||||
:no-filter="isTagFilter"
|
||||
:append-icon="errorIcon"
|
||||
@input="selectionMade($event)"
|
||||
@click:append="handleErrorClick"
|
||||
@mousedown="dropdown"
|
||||
>
|
||||
<template v-if="hasError()" v-slot:prepend-item>
|
||||
<div class="pl-2">
|
||||
<span class="error--text"> {{ entryError }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:prepend>
|
||||
<v-icon @click="handleEditClick">{{ editIcon() }}</v-icon>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
rules: { type: Array, default: undefined },
|
||||
errorMessages: { type: Array, default: null },
|
||||
readonly: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
sockType: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
includeInactive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showEditIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
allowNoSelection: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canClear: { type: Boolean, default: true },
|
||||
label: { type: String, default: "" },
|
||||
variant: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchResults: [],
|
||||
entryError: null,
|
||||
searchEntry: null,
|
||||
lastSelection: null,
|
||||
fetching: false,
|
||||
isTagFilter: false,
|
||||
errorIcon: null,
|
||||
initialized: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
sockType(val, oldVal) {
|
||||
if (val != oldVal && oldVal != null) {
|
||||
//change of type so clear out the list
|
||||
this.searchResults = [];
|
||||
this.searchEntry = null;
|
||||
this.lastSelection = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
},
|
||||
value() {
|
||||
this.fetchValueIfNotPresent();
|
||||
},
|
||||
searchEntry(val) {
|
||||
this.clearErrors();
|
||||
//if the search entry is in the results list then it's a drop down selection not a typed search so bail
|
||||
for (let i = 0; i < this.searchResults.length; i++) {
|
||||
if (this.searchResults[i].name == val) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!val || this.fetching || !this.initialized) {
|
||||
if (!this.initialized) {
|
||||
this.$nextTick(() => {
|
||||
this.initialized = true;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.doSearch(val);
|
||||
},
|
||||
entryError() {
|
||||
if (this.hasError()) {
|
||||
this.errorIcon = "$sockiQuestionCircle";
|
||||
} else {
|
||||
this.errorIcon = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchValueIfNotPresent();
|
||||
},
|
||||
methods: {
|
||||
//Get full selection for use on forms where we need the
|
||||
//full selection including name, active, id
|
||||
getFullSelectionValue() {
|
||||
return this.lastSelection;
|
||||
},
|
||||
hasError: function() {
|
||||
return this.entryError != null;
|
||||
},
|
||||
clearErrors: function() {
|
||||
this.entryError = null;
|
||||
},
|
||||
handleErrorClick: function() {
|
||||
//open help nav for picklist
|
||||
window.$gz.eventBus.$emit("menu-click", {
|
||||
key: "app:help",
|
||||
data: "sock-start-form-autocomplete/#searching"
|
||||
});
|
||||
},
|
||||
editIcon: function() {
|
||||
if (!this.showEditIcon) {
|
||||
return null;
|
||||
}
|
||||
return "$sockiEdit";
|
||||
},
|
||||
handleEditClick: function() {
|
||||
let idToOpen = 0;
|
||||
if (this.lastSelection != null && this.lastSelection.id) {
|
||||
idToOpen = this.lastSelection.id;
|
||||
}
|
||||
window.$gz.eventBus.$emit("openobject", {
|
||||
type: this.sockType,
|
||||
id: idToOpen
|
||||
});
|
||||
},
|
||||
selectionMade(e) {
|
||||
this.clearErrors();
|
||||
if (e == undefined) {
|
||||
//this will happen when clear clicked
|
||||
//simulate empty selection:
|
||||
e = window.$gz.form.getNoSelectionItem(true);
|
||||
}
|
||||
this.lastSelection = e;
|
||||
//this is required for the control to update and parent form to detect it
|
||||
this.$emit("input", e.id);
|
||||
//this is sometimes required for forms that need more than the ID (contract etc)
|
||||
this.$emit("update:name", e.name);
|
||||
this.$emit("update:active", e.active);
|
||||
},
|
||||
fetchValueIfNotPresent() {
|
||||
//is there a value that might require fetching?
|
||||
const val = this.value;
|
||||
if (val == null) {
|
||||
return;
|
||||
}
|
||||
//check if it's in the list of items we have here
|
||||
for (let i = 0; i < this.searchResults.length; i++) {
|
||||
if (this.searchResults[i].id == val) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
//is it the no selection item?
|
||||
if (val == null) {
|
||||
window.$gz.form.addNoSelectionItem(this.searchResults, true);
|
||||
} else {
|
||||
const pickListParams = {
|
||||
sockType: this.sockType,
|
||||
preselectedIds: [this.value]
|
||||
};
|
||||
if (this.variant != null) {
|
||||
pickListParams["listVariant"] = this.variant;
|
||||
}
|
||||
if (this.template != null) {
|
||||
pickListParams["template"] = this.template;
|
||||
}
|
||||
this.getList(pickListParams);
|
||||
}
|
||||
},
|
||||
replaceLastSelection() {
|
||||
//check if searchResults has last selection, if not then add it back in again
|
||||
if (this.lastSelection == null) {
|
||||
//it might be initializing
|
||||
for (let i = 0; i < this.searchResults.length; i++) {
|
||||
if (this.searchResults[i].id == this.value) {
|
||||
this.lastSelection = this.searchResults[i];
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this.searchResults.length; i++) {
|
||||
if (this.searchResults[i].id == this.lastSelection.id) {
|
||||
return; //already there
|
||||
}
|
||||
}
|
||||
//Not there so insert it
|
||||
this.searchResults.push(this.lastSelection);
|
||||
},
|
||||
dropdown() {
|
||||
const vm = this;
|
||||
//check if we have only the initial loaded item and no selection item
|
||||
if (vm.searchResults.length < 3) {
|
||||
//get the default list
|
||||
vm.getList();
|
||||
}
|
||||
},
|
||||
customFilter(item, queryText) {
|
||||
//NOTE: I wanted this to work with tags but all it does is highlight all of each row if tag query is present
|
||||
//I guess because it later on attempts to do the highlighting and can't find all the entered query
|
||||
//it's not clean so I'm just going to make it only highlight if it's a non tag query for now
|
||||
//and do no filtering (highlighting) at all if it's a tag query
|
||||
if (queryText.includes(" ") || queryText.startsWith("..")) {
|
||||
this.isTagFilter = true;
|
||||
return false;
|
||||
}
|
||||
if (this.$store.state.globalSettings.filterCaseSensitive == true) {
|
||||
return item.name.indexOf(queryText) > -1;
|
||||
} else {
|
||||
//need to do a case insensitive filter and hopefully it mirrors postgres at the backend
|
||||
return item.name.toLowerCase().indexOf(queryText.toLowerCase()) > -1;
|
||||
}
|
||||
},
|
||||
getList: async function(pickListParams) {
|
||||
if (this.fetching) {
|
||||
return;
|
||||
}
|
||||
this.fetching = true;
|
||||
//default params for when called on init
|
||||
if (!pickListParams) {
|
||||
pickListParams = {
|
||||
sockType: this.sockType
|
||||
};
|
||||
if (this.value != null) {
|
||||
pickListParams["preselectedIds"] = [this.value];
|
||||
}
|
||||
if (this.includeInactive) {
|
||||
pickListParams["inactive"] = true;
|
||||
}
|
||||
if (this.variant != null) {
|
||||
pickListParams["listVariant"] = this.variant;
|
||||
}
|
||||
if (this.template != null) {
|
||||
pickListParams["template"] = this.template;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await window.$gz.api.upsert(
|
||||
"pick-list/list",
|
||||
pickListParams
|
||||
);
|
||||
|
||||
this.fetching = false;
|
||||
if (!Object.prototype.hasOwnProperty.call(res, "data")) {
|
||||
return Promise.reject(res);
|
||||
}
|
||||
this.searchResults = res.data;
|
||||
if (this.allowNoSelection) {
|
||||
window.$gz.form.addNoSelectionItem(this.searchResults, true);
|
||||
}
|
||||
this.replaceLastSelection();
|
||||
} catch (err) {
|
||||
window.$gz.errorHandler.handleFormError(err);
|
||||
this.fetching = false;
|
||||
}
|
||||
},
|
||||
doSearch: debounce(function(searchFor) {
|
||||
//NOTE debounce with a watcher is a bit different, currently it has to be done exactly this way, nothing else will work properly
|
||||
//https://vuejs.org/v2/guide/migration.html#debounce-Param-Attribute-for-v-model-removed
|
||||
//-----------------
|
||||
|
||||
const vm = this;
|
||||
let isATwoTermQuery = false;
|
||||
let queryTerms = [];
|
||||
//NOTE: empty query is valid; it means get the top 100 ordered by template order
|
||||
let emptyQuery = false;
|
||||
if (searchFor == null || searchFor == "") {
|
||||
emptyQuery = true;
|
||||
} else {
|
||||
//Pre-process the query to validate and send conditionally
|
||||
//get the discrete search terms and verify there are max two
|
||||
if (searchFor.includes(" ")) {
|
||||
queryTerms = searchFor.split(" ");
|
||||
if (queryTerms.length > 2) {
|
||||
vm.entryError = vm.$sock.t("ErrorPickListQueryInvalid");
|
||||
return;
|
||||
}
|
||||
isATwoTermQuery = true;
|
||||
} else {
|
||||
//one term only so push it into array
|
||||
queryTerms.push(searchFor);
|
||||
//Marker term, will be weeded back out later
|
||||
queryTerms.push("[?]");
|
||||
}
|
||||
|
||||
//Now vet the terms
|
||||
//Is user in mid entry of a second tag (space only?)
|
||||
//will appear as an empty string post split
|
||||
if (queryTerms[1] == "") {
|
||||
//mid entry of a second term, just return
|
||||
return;
|
||||
}
|
||||
|
||||
//Is user in mid entry of tag query, just bounce back
|
||||
if (
|
||||
queryTerms[0] == "." ||
|
||||
queryTerms[0] == ".." ||
|
||||
queryTerms[1] == "." ||
|
||||
queryTerms[1] == ".."
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isATwoTermQuery) {
|
||||
//check that both terms aren't tags
|
||||
if (
|
||||
//note: de-lodashed here not sure if I need to add a null check or not
|
||||
queryTerms[0].startsWith("..") &&
|
||||
queryTerms[1].startsWith("..")
|
||||
) {
|
||||
vm.entryError = vm.$sock.t("ErrorPickListQueryInvalid");
|
||||
return;
|
||||
}
|
||||
|
||||
//check that both aren't non-tags
|
||||
if (
|
||||
!queryTerms[0].startsWith("..") &&
|
||||
!queryTerms[1].startsWith("..")
|
||||
) {
|
||||
vm.entryError = vm.$sock.t("ErrorPickListQueryInvalid");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//build parameter
|
||||
const pickListParams = { sockType: vm.sockType };
|
||||
if (!emptyQuery) {
|
||||
let query = queryTerms[0];
|
||||
if (queryTerms[1] != "[?]") {
|
||||
query += " " + queryTerms[1];
|
||||
}
|
||||
pickListParams["query"] = query;
|
||||
}
|
||||
|
||||
if (vm.includeInactive) {
|
||||
pickListParams["inactive"] = true;
|
||||
}
|
||||
if (vm.variant != null) {
|
||||
pickListParams["listVariant"] = vm.variant;
|
||||
}
|
||||
if (vm.template != null) {
|
||||
pickListParams["template"] = vm.template;
|
||||
}
|
||||
this.getList(pickListParams);
|
||||
//------------
|
||||
}, 300) //did some checking, 200-300ms seems to be the most common debounce time for ajax search queries
|
||||
}
|
||||
};
|
||||
|
||||
//https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_debounce
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this,
|
||||
args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
}, wait);
|
||||
if (immediate && !timeout) func.apply(context, args);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
299
client/src/components/report-control.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense justify="center">
|
||||
<v-dialog
|
||||
v-model="isVisible"
|
||||
scrollable
|
||||
max-width="600px"
|
||||
data-cy="reportselector"
|
||||
@keydown.esc="cancel"
|
||||
>
|
||||
<v-card elevation="24">
|
||||
<v-card-title class="text-h5 lighten-2" primary-title>
|
||||
<span> {{ $sock.t("Report") }} </span>
|
||||
</v-card-title>
|
||||
<v-card-text style="height: 500px;">
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="item in reportList"
|
||||
:key="item.id"
|
||||
class="my-n3"
|
||||
@click="renderReport(item.id, item.name)"
|
||||
>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-action class="d-none d-sm-flex">
|
||||
<v-btn x-small icon @click.stop="editReport(item.id)">
|
||||
<v-icon color="primary">$sockiEdit</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
v-if="rights.change"
|
||||
color="primary"
|
||||
text
|
||||
data-cy="reportselector:ok"
|
||||
class="d-none d-sm-flex"
|
||||
@click.native="newReport"
|
||||
>{{ $sock.t("New") }}</v-btn
|
||||
>
|
||||
<v-spacer v-if="!$vuetify.breakpoint.xs"></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
text
|
||||
data-cy="reportselector:cancel"
|
||||
@click.native="cancel"
|
||||
>{{ $sock.t("Cancel") }}</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-row>
|
||||
<!-- ################################################################################-->
|
||||
<!-- ########################## JOB FORM ####################################-->
|
||||
<!-- ################################################################################-->
|
||||
<template>
|
||||
<v-row dense justify="center">
|
||||
<v-dialog v-model="jobActive" persistent max-width="360px">
|
||||
<v-card>
|
||||
<v-card-title>{{ $sock.t("RenderingReport") }}</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="text-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
width="10"
|
||||
size="50"
|
||||
></v-progress-circular>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn text color="primary" @click="cancelJob">{{
|
||||
$sock.t("Cancel")
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-row>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
rights: window.$gz.role.getRights(window.$gz.type.Report),
|
||||
reportDataOptions: {},
|
||||
isVisible: false,
|
||||
resolve: null,
|
||||
reject: null,
|
||||
options: {
|
||||
width: 290,
|
||||
zIndex: 200
|
||||
},
|
||||
reportList: [],
|
||||
selectedReport: null,
|
||||
jobActive: false,
|
||||
preSelectReportId: null,
|
||||
jobId: null
|
||||
}),
|
||||
methods: {
|
||||
editReport(reportid) {
|
||||
this.isVisible = false;
|
||||
this.resolve(null);
|
||||
this.$router.push({
|
||||
name: "sock-report-edit",
|
||||
params: {
|
||||
recordid: reportid,
|
||||
reportDataOptions: this.reportDataOptions
|
||||
}
|
||||
});
|
||||
},
|
||||
async cancelJob() {
|
||||
if (this.jobId && this.jobId != -1) {
|
||||
await window.$gz.api.post(`report/request-cancel/${this.jobId}`);
|
||||
}
|
||||
this.jobActive = false;
|
||||
this.resolve(null);
|
||||
},
|
||||
async renderReport(reportId, reportName) {
|
||||
if (this.$route.params.recordid == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reportDataOptions = this.reportDataOptions;
|
||||
if (!reportDataOptions) {
|
||||
this.reject("Missing report data unable to render report");
|
||||
}
|
||||
reportDataOptions.ReportId = reportId;
|
||||
//Meta data from client for use by report script
|
||||
reportDataOptions.ClientMeta = window.$gz.api.reportClientMetaData();
|
||||
this.jobActive = true;
|
||||
|
||||
try {
|
||||
let jobId = await window.$gz.api.upsert(
|
||||
"report/render-job",
|
||||
reportDataOptions
|
||||
);
|
||||
if (jobId.error) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(jobId, this));
|
||||
}
|
||||
jobId = jobId.jobId;
|
||||
this.jobActive = true;
|
||||
this.jobId = jobId;
|
||||
|
||||
let jobStatus = 1;
|
||||
while (this.jobActive == true) {
|
||||
await window.$gz.util.sleepAsync(1000);
|
||||
jobStatus = await window.$gz.api.get(
|
||||
`job-operations/status/${jobId}`
|
||||
);
|
||||
if (jobStatus.error) {
|
||||
throw new Error(
|
||||
window.$gz.errorHandler.errorToString(jobStatus, this)
|
||||
);
|
||||
}
|
||||
jobStatus = jobStatus.data;
|
||||
|
||||
/*
|
||||
Absent = 0,
|
||||
Sleeping = 1,
|
||||
Running = 2,
|
||||
Completed = 3,
|
||||
Failed = 4
|
||||
*/
|
||||
//check for any terminal status
|
||||
if (jobStatus != 1 && jobStatus != 2) {
|
||||
this.jobActive = false;
|
||||
const jobLogRes = await window.$gz.api.get(
|
||||
`job-operations/logs/${jobId}`
|
||||
);
|
||||
|
||||
//get final entry is error or success
|
||||
var finalJobLogMessage = jobLogRes.data[jobLogRes.data.length - 1];
|
||||
const finalJobLogObject = JSON.parse(finalJobLogMessage.statusText);
|
||||
|
||||
if (jobStatus == 4 || jobStatus == 0) {
|
||||
var e = null;
|
||||
//any error should be in a rendererror keyed object
|
||||
if (!finalJobLogObject.rendererror) {
|
||||
//unusual unknown error, shouldn't happen but just in case
|
||||
e = this.$sock.t("JobFailed");
|
||||
} else {
|
||||
//failure of some kind, either timeout, exception or exception plus pagelog
|
||||
|
||||
if (finalJobLogObject.rendererror.timeout) {
|
||||
//timeout
|
||||
await window.$gz.dialog.displayNoTranslationModalNotificationMessage(
|
||||
window.$gz.translation
|
||||
.get("ReportRenderTimeOut")
|
||||
.replace(
|
||||
"{0}",
|
||||
finalJobLogObject.rendererror.timeoutsetting
|
||||
),
|
||||
null,
|
||||
"error",
|
||||
`${window.$gz.api.helpUrl()}/sock-report-timeout`
|
||||
);
|
||||
//we're done here
|
||||
return this.reject(this.$sock.t("JobFailed"));
|
||||
} else {
|
||||
//exception
|
||||
e = `${this.$sock.t("JobFailed")}: ${
|
||||
finalJobLogObject.rendererror.exception
|
||||
}`;
|
||||
if (finalJobLogObject.rendererror.pagelog) {
|
||||
e +=
|
||||
"\n---------\n" + finalJobLogObject.rendererror.pagelog;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(e);
|
||||
}
|
||||
if (jobStatus == 3) {
|
||||
//success or cancelled
|
||||
|
||||
//todo handle cancelled type of completed
|
||||
//var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { rendererror = new { cancelled = true} }, Newtonsoft.Json.Formatting.None);
|
||||
if (!finalJobLogObject.reportfilename) {
|
||||
throw new Error(
|
||||
`${this.$sock.t(
|
||||
"JobCompleted"
|
||||
)}: But no file name was returned, cannot open report URL`
|
||||
);
|
||||
}
|
||||
var reportUrl = window.$gz.api.reportDownloadUrl(
|
||||
finalJobLogObject.reportfilename
|
||||
);
|
||||
if (window.open(reportUrl, "Report") == null) {
|
||||
this.reject(
|
||||
"Problem displaying report in new window. Browser must allow pop-ups to view reports; check your browser setting"
|
||||
);
|
||||
}
|
||||
|
||||
this.isVisible = false;
|
||||
if (reportName != null) {
|
||||
this.resolve({ name: reportName, id: reportId });
|
||||
} else {
|
||||
this.resolve(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.jobActive = false;
|
||||
this.reject(error);
|
||||
}
|
||||
},
|
||||
async open(reportDataOptions, preSelectReportId) {
|
||||
const vm = this;
|
||||
if (reportDataOptions == null) {
|
||||
throw new Error("report-selector:Open missing reportDataOptions");
|
||||
}
|
||||
this.reportDataOptions = reportDataOptions;
|
||||
this.preSelectReportId = preSelectReportId;
|
||||
if (!preSelectReportId) {
|
||||
if (reportDataOptions.SockType == null) {
|
||||
throw new Error(
|
||||
"report-selector:Open - SockType is missing or empty"
|
||||
);
|
||||
}
|
||||
//get report list from server
|
||||
const res = await window.$gz.api.get(
|
||||
`report/list/${reportDataOptions.SockType}`
|
||||
);
|
||||
if (res.error) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(res, vm));
|
||||
} else {
|
||||
this.reportList = res.data;
|
||||
}
|
||||
this.isVisible = true;
|
||||
} else {
|
||||
this.renderReport(this.preSelectReportId);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.isVisible = false;
|
||||
this.resolve(null);
|
||||
},
|
||||
newReport() {
|
||||
this.isVisible = false;
|
||||
this.resolve(null);
|
||||
this.$router.push({
|
||||
name: "sock-report-edit",
|
||||
params: {
|
||||
recordid: 0,
|
||||
reportDataOptions: this.reportDataOptions
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
84
client/src/components/role-control.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<v-select
|
||||
dense
|
||||
:items="availableRoles"
|
||||
item-text="name"
|
||||
item-value="id"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
small-chips
|
||||
:value="selectedValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="label"
|
||||
:rules="rules"
|
||||
:error-messages="errorMessages"
|
||||
:data-cy="'roleinput:' + testId"
|
||||
@input="handleInput"
|
||||
></v-select>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: { type: String, default: null },
|
||||
rules: { type: Array, default: undefined },
|
||||
errorMessages: { type: Array, default: null },
|
||||
value: { type: Number, default: 0 },
|
||||
readonly: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
limitSelectionTo: { type: String, default: null }, //"inside" - no customer roles, "outside" - no non-customer roles
|
||||
testId: { type: String, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
internalValue: null,
|
||||
availableRoles: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedValue() {
|
||||
const ret = [];
|
||||
if (this.value != null && this.value != 0) {
|
||||
for (let i = 0; i < this.availableRoles.length; i++) {
|
||||
const role = this.availableRoles[i];
|
||||
if (this.value & role.id) {
|
||||
ret.push(role.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await window.$gz.enums.fetchEnumList("AuthorizationRoles");
|
||||
const rawRoles = window.$gz.enums.getSelectionList("AuthorizationRoles");
|
||||
if (this.limitSelectionTo == null) {
|
||||
this.availableRoles = rawRoles;
|
||||
} else {
|
||||
if (this.limitSelectionTo == "inside") {
|
||||
//CustomerRestricted=2048, Customer=4096
|
||||
this.availableRoles = rawRoles.filter(
|
||||
z => z.id != 2048 && z.id != 4096
|
||||
);
|
||||
} else {
|
||||
this.availableRoles = rawRoles.filter(
|
||||
z => z.id == 2048 || z.id == 4096
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleInput(value) {
|
||||
let newValue = 0;
|
||||
if (value != null && value != [] && value.length > 0) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const role = value[i];
|
||||
newValue = newValue | role;
|
||||
}
|
||||
}
|
||||
this.$emit("input", newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
154
client/src/components/tag-picker.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<v-autocomplete
|
||||
dense
|
||||
:value="value"
|
||||
:readonly="readonly"
|
||||
:items="sourcetags"
|
||||
:loading="tagSearchUnderway"
|
||||
:placeholder="placeHolderText"
|
||||
:persistent-placeholder="persistentPlaceHolder"
|
||||
:hint="hint"
|
||||
:label="noLabel ? '' : title"
|
||||
:prepend-inner-icon="prependInnerIcon"
|
||||
:no-data-text="$sock.t('NoData')"
|
||||
:search-input.sync="tagSearchEntry"
|
||||
:clearable="clearable"
|
||||
hide-selected
|
||||
multiple
|
||||
chips
|
||||
small-chips
|
||||
deletable-chips
|
||||
cache-items
|
||||
:rules="rules"
|
||||
:error-messages="errorMessages"
|
||||
@input="input($event)"
|
||||
>
|
||||
<template v-if="offerAdd()" slot="no-data">
|
||||
<v-chip color="primary" text-color="white" class="text-h4">
|
||||
{{ normalizeTag(tagSearchEntry) }}</v-chip
|
||||
>
|
||||
<v-btn large icon @click="addTag()">
|
||||
<v-icon large color="success">$sockiPlusCircle</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
type: Array
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default() {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
readonly: { type: Boolean, default: false },
|
||||
rules: { type: Array, default: undefined },
|
||||
errorMessages: { type: Array, default: null },
|
||||
selectOnly: { type: Boolean, default: false },
|
||||
noLabel: { type: Boolean, default: false },
|
||||
clearable: { type: Boolean, default: false },
|
||||
placeHolder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
persistentPlaceHolder: { type: Boolean, default: false },
|
||||
hint: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
prependInnerIcon: {
|
||||
type: String,
|
||||
default() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
sourcetags: [],
|
||||
tagSearchEntry: null,
|
||||
tagSearchUnderway: false,
|
||||
initialized: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
if (this.label) {
|
||||
return this.label;
|
||||
}
|
||||
return this.$sock.t("Tags");
|
||||
},
|
||||
placeHolderText() {
|
||||
if (this.placeHolder) {
|
||||
return this.placeHolder;
|
||||
}
|
||||
return this.$sock.t("TypeToSearchOrAdd");
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async tagSearchEntry(val) {
|
||||
if (!val || this.tagSearchUnderway) {
|
||||
return;
|
||||
}
|
||||
this.tagSearchUnderway = true;
|
||||
try {
|
||||
const res = await window.$gz.api.get("tag-list/list?query=" + val);
|
||||
//We never expect there to be no data here
|
||||
if (!Object.prototype.hasOwnProperty.call(res, "data")) {
|
||||
throw new Error(res);
|
||||
}
|
||||
//adding this to the property will automatically have it cached by the autocomplete component
|
||||
//as cache-items has been set so this just needs to be set here once and all is well in future
|
||||
//Any search will be kept for later so this is very efficient
|
||||
this.sourcetags = res.data;
|
||||
this.tagSearchUnderway = false;
|
||||
} catch (err) {
|
||||
window.$gz.errorHandler.handleFormError(err);
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
//Set the initial list items based on the record items, this only needs to be called once at init
|
||||
if (!this.initialized && this.value != null && this.value.length > 0) {
|
||||
this.sourcetags = this.value;
|
||||
this.initialized = true;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
offerAdd() {
|
||||
if (
|
||||
this.selectOnly ||
|
||||
this.tagSearchEntry == null ||
|
||||
this.tagSearchEntry == ""
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const searchTag = this.normalizeTag(this.tagSearchEntry);
|
||||
if (this.value.some(z => z == searchTag)) return false;
|
||||
return true;
|
||||
},
|
||||
input(e) {
|
||||
this.tagSearchEntry = "";
|
||||
this.$emit("input", e);
|
||||
},
|
||||
addTag() {
|
||||
let theTag = this.tagSearchEntry;
|
||||
theTag = this.normalizeTag(theTag);
|
||||
this.sourcetags.push(theTag);
|
||||
this.value.push(theTag);
|
||||
this.tagSearchEntry = "";
|
||||
this.$emit("input", this.value);
|
||||
},
|
||||
normalizeTag(tagName) {
|
||||
return window.$gz.util.normalizeTag(tagName);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
173
client/src/components/time-control.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense>
|
||||
<template v-if="!readonly">
|
||||
<template v-if="!$store.state.nativeDateTimeInput">
|
||||
<v-col cols="12">
|
||||
<v-dialog v-model="dlgtime" width="300px">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-text-field
|
||||
dense
|
||||
:value="readonlyFormat()"
|
||||
:label="label"
|
||||
:rules="rules"
|
||||
prepend-icon="$sockiClock"
|
||||
readonly
|
||||
:error="!!hasErrors"
|
||||
v-on="on"
|
||||
@click:prepend="dlgtime = true"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<v-time-picker
|
||||
dense
|
||||
scrollable
|
||||
ampm-in-title
|
||||
:format="hour12 ? 'ampm' : '24hr'"
|
||||
:value="timeValue"
|
||||
@input="updateTimeValue"
|
||||
><v-btn text color="primary" @click="$emit('input', null)">{{
|
||||
$sock.t("Delete")
|
||||
}}</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="primary" @click="setNow()">{{
|
||||
$sock.t("Now")
|
||||
}}</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="primary" @click="dlgtime = false">{{
|
||||
$sock.t("OK")
|
||||
}}</v-btn>
|
||||
</v-time-picker>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
</template>
|
||||
<template v-if="$store.state.nativeDateTimeInput">
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
ref="timeField"
|
||||
dense
|
||||
:value="timeValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="label"
|
||||
type="time"
|
||||
:data-cy="`${dataCy}:time`"
|
||||
@change="updateTimeValue"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
dense
|
||||
:value="readonlyFormat()"
|
||||
:label="label"
|
||||
prepend-icon="$sockiClock"
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
<div class="v-messages theme--light error--text mt-n5" role="alert">
|
||||
<div class="v-messages__wrapper">
|
||||
<div class="v-messages__message">{{ allErrors() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
//NOTE: this control also captures the date even though it's time only
|
||||
//this is an intentional design decision to support field change to date or date AND time and is considered a display issue
|
||||
export default {
|
||||
props: {
|
||||
label: { type: String, default: null },
|
||||
rules: { type: Array, default: undefined },
|
||||
errorMessages: { type: Array, default: null },
|
||||
value: { type: String, default: null },
|
||||
readonly: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
dataCy: { type: String, default: null }
|
||||
},
|
||||
data: () => ({
|
||||
dlgtime: false,
|
||||
timeZoneName: window.$gz.locale.getResolvedTimeZoneName(),
|
||||
languageName: window.$gz.locale.getResolvedLanguage(),
|
||||
hour12: window.$gz.locale.getHour12()
|
||||
}),
|
||||
computed: {
|
||||
hasErrors() {
|
||||
return this.errorMessages != null && this.errorMessages.length > 0;
|
||||
},
|
||||
timeValue() {
|
||||
return window.$gz.locale.utcDateStringToLocal8601TimeOnlyString(
|
||||
this.value,
|
||||
this.timeZoneName
|
||||
);
|
||||
},
|
||||
dateValue() {
|
||||
return window.$gz.locale.utcDateStringToLocal8601DateOnlyString(
|
||||
this.value,
|
||||
this.timeZoneName
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setNow() {
|
||||
// const v = window.$gz.locale
|
||||
// .nowUTC8601String(this.timeZoneName)
|
||||
// .split("T")[1];
|
||||
|
||||
//now without the milliseconds
|
||||
var nowNoMs = window.$gz.DateTime.local().set({ milliseconds: 0 });
|
||||
const v = nowNoMs.toString().split("T")[1];
|
||||
|
||||
this.updateTimeValue(v);
|
||||
this.dlgtime = false;
|
||||
},
|
||||
allErrors() {
|
||||
let ret = "";
|
||||
|
||||
if (this.errorMessages != null && this.errorMessages.length > 0) {
|
||||
ret += this.errorMessages.toString();
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
readonlyFormat() {
|
||||
return window.$gz.locale.utcDateToShortTimeLocalized(
|
||||
this.value,
|
||||
this.timeZoneName,
|
||||
this.languageName,
|
||||
this.hour12
|
||||
);
|
||||
},
|
||||
updateTimeValue(v) {
|
||||
this.updateValue(this.dateValue, v);
|
||||
},
|
||||
updateValue(theDate, theTime) {
|
||||
const vm = this;
|
||||
|
||||
if (!theDate) {
|
||||
const v = new Date();
|
||||
const fullYear = v.getFullYear();
|
||||
let fullMonth = v.getMonth() + 1;
|
||||
if (fullMonth < 10) {
|
||||
fullMonth = "0" + fullMonth.toString();
|
||||
}
|
||||
let fullDay = v.getDate();
|
||||
if (fullDay < 10) {
|
||||
fullDay = "0" + fullDay.toString();
|
||||
}
|
||||
theDate = fullYear + "-" + fullMonth + "-" + fullDay;
|
||||
}
|
||||
if (!theTime) {
|
||||
theTime = "00:00:00";
|
||||
}
|
||||
const ret = window.$gz.locale.localTimeDateStringToUTC8601String(
|
||||
theDate + "T" + theTime,
|
||||
vm.timeZoneName
|
||||
);
|
||||
vm.$emit("input", ret);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
28
client/src/components/url-control.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<v-text-field
|
||||
dense
|
||||
v-bind="$attrs"
|
||||
type="url"
|
||||
prepend-icon="$sockiExternalLinkAlt"
|
||||
v-on="$listeners"
|
||||
@click:prepend="openUrl"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
openUrl() {
|
||||
if (
|
||||
this.$el &&
|
||||
this.$el.attributes &&
|
||||
this.$el.attributes.value &&
|
||||
this.$el.attributes.value.value
|
||||
) {
|
||||
let link = this.$el.attributes.value.value;
|
||||
link = link.indexOf("://") === -1 ? "//" + link : link;
|
||||
window.open(link, "_blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
1000
client/src/components/wiki-control.vue
Normal file
233
client/src/main.js
Normal file
@@ -0,0 +1,233 @@
|
||||
import "fontsource-roboto/latin.css";
|
||||
import "github-markdown-css";
|
||||
import Vue from "vue";
|
||||
import Vuetify from "./plugins/vuetify";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import store from "./store";
|
||||
import "./registerServiceWorker";
|
||||
import errorHandler from "./api/errorhandler";
|
||||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
import { DateTime } from "luxon";
|
||||
import VueCurrencyInput from "vue-currency-input";
|
||||
//import Pappa from "papaparse";
|
||||
|
||||
//my libs
|
||||
import errorhandler from "./api/errorhandler";
|
||||
import sockeyeVersion from "./api/sockeye-version";
|
||||
import gzeventbus from "./api/eventbus";
|
||||
import gzmenu from "./api/gzmenu";
|
||||
import gzdialog from "./api/gzdialog";
|
||||
import gzutil from "./api/gzutil";
|
||||
import translation from "./api/translation";
|
||||
import locale from "./api/locale";
|
||||
import gzapi from "./api/gzapi";
|
||||
|
||||
import gzform from "./api/gzform";
|
||||
import gzformcustomtemplate from "./api/form-custom-template";
|
||||
import authorizationroles from "./api/authorizationroles";
|
||||
import socktype from "./api/socktype";
|
||||
import gzenums from "./api/enums";
|
||||
import "@/assets/css/main.css";
|
||||
|
||||
//controls
|
||||
import dateTimeControl from "./components/date-time-control.vue";
|
||||
import dateControl from "./components/date-control.vue";
|
||||
import timeControl from "./components/time-control.vue";
|
||||
import tagPicker from "./components/tag-picker.vue";
|
||||
import pickList from "./components/pick-list.vue";
|
||||
import dataTable from "./components/data-table.vue";
|
||||
import dataTableFilterControl from "./components/data-table-filter-control.vue";
|
||||
import dataTableFilterManagerControl from "./components/data-table-filter-manager-control.vue";
|
||||
import dataTableMobileFilterColumnSelectorControl from "./components/data-table-mobile-filter-column-selector-control.vue";
|
||||
import customFieldsControl from "./components/custom-fields-control.vue";
|
||||
import currencyControl from "./components/currency-control.vue";
|
||||
import decimalControl from "./components/decimal-control.vue";
|
||||
import percentControl from "./components/percent-control.vue";
|
||||
import phoneControl from "./components/phone-control.vue";
|
||||
import emailControl from "./components/email-control.vue";
|
||||
import urlControl from "./components/url-control.vue";
|
||||
import roleControl from "./components/role-control.vue";
|
||||
import durationControl from "./components/duration-control.vue";
|
||||
import errorControl from "./components/error-control.vue";
|
||||
import alertControl from "./components/alert-control.vue";
|
||||
import extensionsControl from "./components/extensions-control.vue";
|
||||
import reportSelectorControl from "./components/report-control.vue";
|
||||
import wikiControl from "./components/wiki-control.vue";
|
||||
import attachmentControl from "./components/attachment-control.vue";
|
||||
import chartLineControl from "./components/chart-line-control.vue";
|
||||
import chartBarControl from "./components/chart-bar-control.vue";
|
||||
import chartBarHorizontalControl from "./components/chart-bar-horizontal-control.vue";
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// LIBS AND GLOBAL ITEMS
|
||||
//
|
||||
//
|
||||
|
||||
window.$gz = {
|
||||
translation: translation,
|
||||
locale: locale,
|
||||
formCustomTemplate: gzformcustomtemplate,
|
||||
type: socktype,
|
||||
role: authorizationroles,
|
||||
enums: gzenums,
|
||||
eventBus: gzeventbus,
|
||||
menu: gzmenu,
|
||||
dialog: gzdialog,
|
||||
util: gzutil,
|
||||
DateTime: DateTime,
|
||||
api: gzapi,
|
||||
form: gzform,
|
||||
errorHandler: errorhandler,
|
||||
store: store,
|
||||
clientInfo: sockeyeVersion,
|
||||
dev: process.env.NODE_ENV === "development",
|
||||
erasingDatabase: false
|
||||
};
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// ERROR HANDLING
|
||||
//
|
||||
|
||||
Vue.config.errorHandler = errorHandler.handleVueError;
|
||||
window.onerror = errorHandler.handleGeneralError;
|
||||
|
||||
//warnings, only occur by default in debug mode not production
|
||||
Vue.config.warnHandler = errorHandler.handleVueWarning;
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// AJAX LOADER INDICATOR
|
||||
//
|
||||
//
|
||||
|
||||
// Store a copy of the fetch function
|
||||
const _oldFetch = fetch;
|
||||
|
||||
// Create our new version of the fetch function
|
||||
window.fetch = function() {
|
||||
// Create hooks
|
||||
const fetchStart = new Event("fetchStart", {
|
||||
view: document,
|
||||
bubbles: true,
|
||||
cancelable: false
|
||||
});
|
||||
const fetchEnd = new Event("fetchEnd", {
|
||||
view: document,
|
||||
bubbles: true,
|
||||
cancelable: false
|
||||
});
|
||||
|
||||
// Pass the supplied arguments to the real fetch function
|
||||
const fetchCall = _oldFetch.apply(this, arguments);
|
||||
|
||||
// Trigger the fetchStart event
|
||||
document.dispatchEvent(fetchStart);
|
||||
|
||||
fetchCall
|
||||
.then(function() {
|
||||
// Trigger the fetchEnd event
|
||||
document.dispatchEvent(fetchEnd);
|
||||
})
|
||||
.catch(function() {
|
||||
// Trigger the fetchEnd event
|
||||
document.dispatchEvent(fetchEnd);
|
||||
});
|
||||
|
||||
return fetchCall;
|
||||
};
|
||||
|
||||
document.addEventListener("fetchStart", function() {
|
||||
NProgress.start();
|
||||
});
|
||||
|
||||
document.addEventListener("fetchEnd", function() {
|
||||
NProgress.done();
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
//GZ COMPONENTS
|
||||
//
|
||||
Vue.component("gz-date-time-picker", dateTimeControl);
|
||||
Vue.component("gz-date-picker", dateControl);
|
||||
Vue.component("gz-time-picker", timeControl);
|
||||
Vue.component("gz-tag-picker", tagPicker);
|
||||
Vue.component("gz-pick-list", pickList);
|
||||
Vue.component("gz-data-table", dataTable);
|
||||
Vue.component("gz-data-table-filter", dataTableFilterControl);
|
||||
Vue.component("gz-data-table-filter-manager", dataTableFilterManagerControl);
|
||||
Vue.component(
|
||||
"gz-data-table-mobile-filter-column-selector",
|
||||
dataTableMobileFilterColumnSelectorControl
|
||||
);
|
||||
Vue.component("gz-custom-fields", customFieldsControl);
|
||||
Vue.component("gz-currency", currencyControl);
|
||||
Vue.component("gz-percent", percentControl);
|
||||
Vue.component("gz-decimal", decimalControl);
|
||||
Vue.component("gz-phone", phoneControl);
|
||||
Vue.component("gz-email", emailControl);
|
||||
Vue.component("gz-url", urlControl);
|
||||
Vue.component("gz-role-picker", roleControl);
|
||||
Vue.component("gz-duration-picker", durationControl);
|
||||
Vue.component("gz-error", errorControl);
|
||||
Vue.component("gz-alert", alertControl);
|
||||
Vue.component("gz-report-selector", reportSelectorControl);
|
||||
Vue.component("gz-extensions", extensionsControl);
|
||||
Vue.component("gz-wiki", wikiControl);
|
||||
Vue.component("gz-attachments", attachmentControl);
|
||||
Vue.component("gz-chart-line", chartLineControl);
|
||||
Vue.component("gz-chart-bar", chartBarControl);
|
||||
Vue.component("gz-chart-bar-horizontal", chartBarHorizontalControl);
|
||||
//3rd party components
|
||||
Vue.use(VueCurrencyInput);
|
||||
/////////////////////////////////////////////////////////////
|
||||
//DIRECTIVES
|
||||
//
|
||||
//Auto focus on forms
|
||||
Vue.directive("focus", {
|
||||
// When the bound element is inserted into the DOM...
|
||||
inserted: function(el) {
|
||||
// Focus the element
|
||||
el.focus();
|
||||
}
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// INSTANTIATE
|
||||
//
|
||||
Vue.prototype.$sock = {
|
||||
//development mode, development level error messages etc
|
||||
dev: process.env.NODE_ENV === "development",
|
||||
t: function(tKey) {
|
||||
return translation.get(tKey);
|
||||
},
|
||||
dt: function(timestamp) {
|
||||
return locale.utcDateToShortDateAndTimeLocalized(timestamp);
|
||||
},
|
||||
sd: function(timestamp) {
|
||||
return locale.utcDateToShortDateLocalized(timestamp);
|
||||
},
|
||||
cur: function(value) {
|
||||
return locale.currencyLocalized(value);
|
||||
},
|
||||
dec: function(value) {
|
||||
return locale.decimalLocalized(value);
|
||||
},
|
||||
util: function() {
|
||||
return gzutil;
|
||||
},
|
||||
ayt: function() {
|
||||
return socktype;
|
||||
}
|
||||
};
|
||||
//disable the devtools nag
|
||||
Vue.config.devtools = false;
|
||||
new Vue({
|
||||
vuetify: Vuetify,
|
||||
router,
|
||||
store,
|
||||
// eslint-disable-next-line
|
||||
render: (h) => h(App)
|
||||
}).$mount("#app");
|
||||
1568
client/src/plugins/vuetify.js
Normal file
40
client/src/registerServiceWorker.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/* eslint-disable no-console */
|
||||
//INFO: https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
|
||||
import { register } from "register-service-worker";
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready() {
|
||||
console.log("Service worker is active.");
|
||||
},
|
||||
cached() {
|
||||
console.log("Content has been cached for offline use.");
|
||||
},
|
||||
updatefound() {
|
||||
console.log("New content is downloading.");
|
||||
},
|
||||
updated(registration) {
|
||||
console.log("New content is available; please refresh.");
|
||||
|
||||
//https://medium.com/@dougallrich/give-users-control-over-app-updates-in-vue-cli-3-pwas-20453aedc1f2
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("swUpdated", { detail: registration })
|
||||
);
|
||||
},
|
||||
registered(registration) {
|
||||
//https://medium.com/@dougallrich/give-users-control-over-app-updates-in-vue-cli-3-pwas-20453aedc1f2
|
||||
console.log("Service worker has been registered.");
|
||||
setInterval(() => {
|
||||
registration.update();
|
||||
}, 1000 * 60 * 60); // e.g. hourly checks
|
||||
},
|
||||
offline() {
|
||||
console.log(
|
||||
"No internet connection found. App is running in offline mode."
|
||||
);
|
||||
},
|
||||
error(error) {
|
||||
console.error("Error during service worker registration:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
523
client/src/router.js
Normal file
@@ -0,0 +1,523 @@
|
||||
import Vue from "vue";
|
||||
import Router from "vue-router";
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
// scrollBehavior:
|
||||
// - only available in html5 history mode
|
||||
// - defaults to no scroll behavior
|
||||
// - return false to prevent scroll
|
||||
const scrollBehavior = function(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
// savedPosition is only available for popstate navigations.
|
||||
return savedPosition;
|
||||
} else {
|
||||
const position = {};
|
||||
|
||||
// scroll to anchor by returning the selector
|
||||
if (to.hash) {
|
||||
position.selector = to.hash;
|
||||
|
||||
// specify offset of the element
|
||||
if (to.hash === "#anchor2") {
|
||||
position.offset = { y: 100 };
|
||||
}
|
||||
|
||||
if (document.querySelector(to.hash)) {
|
||||
return position;
|
||||
}
|
||||
|
||||
// if the returned position is falsy or an empty object,
|
||||
// will retain current scroll position.
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
return new Promise((resolve) => {
|
||||
// check if any matched route config has meta that requires scrolling to top
|
||||
// eslint-disable-next-line
|
||||
if (to.matched.some((m) => m.meta.scrollToTop)) {
|
||||
// coords will be used if no selector is provided,
|
||||
// or if the selector didn't match any element.
|
||||
position.x = 0;
|
||||
position.y = 0;
|
||||
}
|
||||
|
||||
// wait for the out transition to complete (if necessary)
|
||||
this.app.$root.$once("triggerScroll", () => {
|
||||
// if the resolved position is falsy or an empty object,
|
||||
// will retain current scroll position.
|
||||
|
||||
resolve(position);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
/**
|
||||
* https://router.vuejs.org/guide/advanced/lazy-loading.html#grouping-components-in-the-same-chunk
|
||||
*
|
||||
*
|
||||
*/
|
||||
export default new Router({
|
||||
mode: "history",
|
||||
base: process.env.BASE_URL,
|
||||
scrollBehavior,
|
||||
routes: [
|
||||
//########################## GENERAL / COMMON GROUP ###################################
|
||||
{
|
||||
path: "/open/:socktype/:recordid",
|
||||
name: "sock-open",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "sock-common" */ "./views/sock-open.vue")
|
||||
},
|
||||
{
|
||||
path: "/viewreport/:oid/:rid",
|
||||
name: "sock-report-view",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/sock-report-view.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
name: "sock-about",
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "sock-common" */ "./views/sock-about.vue")
|
||||
},
|
||||
{
|
||||
path: "/applog",
|
||||
name: "sock-log",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "sock-common" */ "./views/sock-log.vue")
|
||||
},
|
||||
{
|
||||
path: "/customize/:formCustomTemplateKey",
|
||||
name: "sock-customize",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/sock-customize.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/data-list-column-view/:dataListKey",
|
||||
name: "sock-data-list-column-view",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/sock-data-list-column-view.vue"
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
path: "/history/:socktype/:recordid/:userlog?",
|
||||
name: "sock-history",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "sock-common" */ "./views/sock-history.vue")
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
meta: { scrollToTop: true }, //KEEP THIS AS AN EXAMPLE OF HOW TO USE WITH CODE ABOVE
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "sock-common" */ "./views/login.vue")
|
||||
},
|
||||
{
|
||||
path: "/home-dashboard",
|
||||
name: "home-dashboard",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/home-dashboard.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
//path: "/home-search/:socktype?", this was making it sticky and couldn't change type as it was in url path
|
||||
path: "/home-search",
|
||||
name: "home-search",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "sock-common" */ "./views/home-search.vue")
|
||||
},
|
||||
{ path: "/", redirect: "/login" }, //If someone goes blindly to the root of the app, then it should go to login
|
||||
{
|
||||
path: "/home-schedule",
|
||||
name: "home-schedule",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/home-schedule.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/home-memos",
|
||||
name: "home-memos",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "sock-common" */ "./views/home-memos.vue")
|
||||
},
|
||||
{
|
||||
path: "/home-memos/:recordid",
|
||||
name: "memo-edit",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "sock-common" */ "./views/home-memo.vue")
|
||||
},
|
||||
{
|
||||
path: "/home-notifications",
|
||||
name: "home-notifications",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/home-notifications.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/home-notify-direct",
|
||||
name: "home-notify-direct",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/home-notify-direct.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/home-reminders",
|
||||
name: "home-reminders",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/home-reminders.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/home-reminders/:recordid",
|
||||
name: "reminder-edit",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/home-reminder.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/home-reviews/:aType?/:objectId?/:name?",
|
||||
name: "home-reviews",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "sock-common" */ "./views/home-reviews.vue")
|
||||
},
|
||||
{
|
||||
path: "/home-reviews/:recordid/:aType?/:objectId?/:name?",
|
||||
name: "review-edit",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "sock-common" */ "./views/home-review.vue")
|
||||
},
|
||||
{
|
||||
path: "/home-user-settings",
|
||||
name: "home-user-settings",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/home-user-settings.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/home-reset",
|
||||
name: "home-reset",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "sock-common" */ "./views/home-reset.vue")
|
||||
},
|
||||
{
|
||||
path: "/home-password",
|
||||
name: "home-password",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/home-password.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/home-security",
|
||||
name: "home-security",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/home-security.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/home-notify-subscriptions",
|
||||
name: "home-notify-subscriptions",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/home-notify-subscriptions.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/home-notify-subscriptions/:recordid",
|
||||
name: "home-notify-subscription",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/home-notify-subscription.vue"
|
||||
)
|
||||
},
|
||||
//####################### CUSTOMERS GROUP ##############################
|
||||
{
|
||||
path: "/cust-customers",
|
||||
name: "cust-customers",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "cust" */ "./views/cust-customers.vue")
|
||||
},
|
||||
{
|
||||
path: "/cust-customers/:recordid",
|
||||
name: "customer-edit",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "cust" */ "./views/cust-customer.vue")
|
||||
},
|
||||
{
|
||||
path: "/cust-customer-notes/:customerid",
|
||||
name: "customer-notes",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "cust" */ "./views/cust-customer-notes.vue")
|
||||
},
|
||||
{
|
||||
path: "/cust-customer-note/:recordid",
|
||||
name: "customer-note-edit",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "cust" */ "./views/cust-customer-note.vue")
|
||||
},
|
||||
{
|
||||
path: "/cust-users",
|
||||
name: "cust-users",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "cust" */ "./views/cust-users.vue")
|
||||
},
|
||||
{
|
||||
path: "/cust-users/:recordid",
|
||||
name: "cust-user",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "cust" */ "./views/cust-user.vue")
|
||||
},
|
||||
{
|
||||
path: "/cust-head-offices",
|
||||
name: "cust-head-offices",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "cust" */ "./views/cust-head-offices.vue")
|
||||
},
|
||||
{
|
||||
path: "/cust-head-offices/:recordid",
|
||||
name: "head-office-edit",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "cust" */ "./views/cust-head-office.vue")
|
||||
},
|
||||
{
|
||||
path: "/cust-notify-subscriptions",
|
||||
name: "cust-notify-subscriptions",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "cust" */ "./views/customer-notify-subscriptions.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/cust-notify-subscriptions/:recordid",
|
||||
name: "cust-notify-subscription",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "cust" */ "./views/customer-notify-subscription.vue"
|
||||
)
|
||||
},
|
||||
//####################### SERVICE GROUP ##############################
|
||||
{
|
||||
path: "/svc-schedule",
|
||||
name: "svc-schedule",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "svc" */ "./views/svc-schedule.vue")
|
||||
},
|
||||
|
||||
//######################### ADMINISTRATION GROUP #####################################
|
||||
{
|
||||
path: "/adm-global-settings",
|
||||
name: "adm-global-settings",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-global-settings.vue")
|
||||
},
|
||||
{
|
||||
path: "/adm-global-select-templates",
|
||||
name: "adm-global-select-templates",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "adm" */ "./views/adm-global-select-templates.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/adm-global-seeds",
|
||||
name: "adm-global-seeds",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-global-seeds.vue")
|
||||
},
|
||||
{
|
||||
path: "/adm-global-logo",
|
||||
name: "adm-global-logo",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-global-logo.vue")
|
||||
},
|
||||
|
||||
{
|
||||
path: "/adm-users",
|
||||
name: "adm-users",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-users.vue")
|
||||
},
|
||||
{
|
||||
path: "/adm-users/:recordid",
|
||||
name: "adm-user",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-user.vue")
|
||||
},
|
||||
{
|
||||
path: "/adm-translations",
|
||||
name: "adm-translations",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-translations.vue")
|
||||
},
|
||||
{
|
||||
path: "/adm-translations/:recordid",
|
||||
name: "adm-translation",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-translation.vue")
|
||||
},
|
||||
|
||||
{
|
||||
path: "/adm-report-templates",
|
||||
name: "adm-report-templates",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-report-templates.vue")
|
||||
},
|
||||
{
|
||||
path: "/report-edit/:recordid", //Route to edit a report template
|
||||
name: "sock-report-edit",
|
||||
component: () =>
|
||||
import(
|
||||
//it gets it's own chunk name because it's huge and rarely used
|
||||
/* webpackChunkName: "sock-report-edit" */ "./views/sock-report-edit.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/adm-attachments",
|
||||
name: "adm-attachments",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-attachments.vue")
|
||||
},
|
||||
|
||||
{
|
||||
path: "/adm-history",
|
||||
name: "adm-history",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-history.vue")
|
||||
},
|
||||
{
|
||||
path: "/adm-import",
|
||||
name: "adm-import",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-import.vue")
|
||||
},
|
||||
{
|
||||
path: "/adm-integrations",
|
||||
name: "adm-integrations",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-integrations.vue")
|
||||
},
|
||||
{
|
||||
path: "/adm-integrations/:recordid",
|
||||
name: "adm-integration",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "adm" */ "./views/adm-integration.vue")
|
||||
},
|
||||
|
||||
//########################## OPERATIONS GROUP ############################
|
||||
{
|
||||
path: "/ops-backup",
|
||||
name: "ops-backup",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "ops" */ "./views/ops-backup.vue")
|
||||
},
|
||||
|
||||
{
|
||||
path: "/ops-server-state",
|
||||
name: "ops-server-state",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "ops" */ "./views/ops-server-state.vue")
|
||||
},
|
||||
|
||||
{
|
||||
path: "/ops-jobs",
|
||||
name: "ops-jobs",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "ops" */ "./views/ops-jobs.vue")
|
||||
},
|
||||
|
||||
{
|
||||
path: "/ops-log",
|
||||
name: "ops-log",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "ops" */ "./views/ops-log.vue")
|
||||
},
|
||||
{
|
||||
path: "/ops-view-configuration",
|
||||
name: "ops-view-configuration",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "ops" */ "./views/ops-view-configuration.vue"
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
path: "/ops-metrics",
|
||||
name: "ops-metrics",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "ops" */ "./views/ops-metrics.vue")
|
||||
},
|
||||
{
|
||||
path: "/ops-profile",
|
||||
name: "ops-profile",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "ops" */ "./views/ops-profile.vue")
|
||||
},
|
||||
{
|
||||
path: "/ops-notify-queue",
|
||||
name: "ops-notify-queue",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "ops" */ "./views/ops-notify-queue.vue")
|
||||
},
|
||||
{
|
||||
path: "/ops-notification-settings",
|
||||
name: "ops-notification-settings",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "ops" */ "./views/ops-notification-settings.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/ops-notify-log",
|
||||
name: "ops-notify-log",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "ops" */ "./views/ops-notify-log.vue")
|
||||
},
|
||||
{
|
||||
path: "/ops-customer-notify-log",
|
||||
name: "ops-customer-notify-log",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "ops" */ "./views/ops-customer-notify-log.vue"
|
||||
)
|
||||
},
|
||||
|
||||
//############################## SPECIAL ROUTES ###############################
|
||||
|
||||
{
|
||||
//No rights - happens when customer user logs in without access to anything at all due to configuration not allowing it
|
||||
path: "/no-features-available",
|
||||
name: "no-features-available",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "sock-common" */ "./views/nofeaturesavailable.vue"
|
||||
)
|
||||
},
|
||||
{
|
||||
//404 404 404 404 404 404 404 404 404 404 404 404 404 404 404 404 404 404 404 404 404 404 404 404
|
||||
path: "*",
|
||||
name: "notfound",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "sock-common" */ "./views/notfound.vue")
|
||||
}
|
||||
]
|
||||
});
|
||||
210
client/src/store.js
Normal file
@@ -0,0 +1,210 @@
|
||||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import createPersistedState from "vuex-persistedstate";
|
||||
|
||||
const MaxLogLength = 100;
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
//reset all local settings via url
|
||||
//localhost:8080/login?reset
|
||||
if (window.location.search) {
|
||||
var searchParams = new URLSearchParams(window.location.search);
|
||||
if (searchParams.has("reset")) {
|
||||
localStorage.removeItem("Sockeye");
|
||||
console.log("LOCAL SETTINGS RESET");
|
||||
}
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
plugins: [createPersistedState({ key: "Sockeye" })],
|
||||
state: {
|
||||
lastClientVersion: "",
|
||||
authenticated: false,
|
||||
apiToken: "-",
|
||||
downloadToken: "-",
|
||||
l: false, //license lockout flag
|
||||
tfaEnabled: undefined,
|
||||
customerRights: {},
|
||||
userId: 0,
|
||||
userName: "NOT AUTHENTICATED",
|
||||
roles: 0,
|
||||
userType: 0,
|
||||
homePage: undefined,
|
||||
translationText: {},
|
||||
enums: {}, //all enum values with translated text to match stored as key
|
||||
userOptions: {
|
||||
languageOverride: "en-US",
|
||||
timeZoneOverride: null, //use browser tz by default
|
||||
currencyName: "USD",
|
||||
hour12: true,
|
||||
uiColor: "#000000ff",
|
||||
emailAddress: null,
|
||||
mapUrlTemplate: null
|
||||
},
|
||||
globalSettings: {},
|
||||
navItems: [],
|
||||
logArray: [],
|
||||
formSettings: {}, //this is the settings on forms that survive a refresh like grid number of items to show etc
|
||||
formCustomTemplate: {}, //this is the custom fields settings for forms,
|
||||
darkMode: false,
|
||||
nativeDateTimeInput: false,
|
||||
knownPassword: false,
|
||||
newNotificationCount: 0
|
||||
},
|
||||
getters: {
|
||||
/* User types:
|
||||
Service = 1,
|
||||
NotService = 2,
|
||||
Customer = 3,
|
||||
HeadOffice = 4,
|
||||
ServiceContractor = 5
|
||||
*/
|
||||
isCustomerUser: state => {
|
||||
return state.userType == 3 || state.userType == 4;
|
||||
},
|
||||
isSubContractorUser: state => {
|
||||
return state.userType == 5;
|
||||
},
|
||||
isScheduleableUser: state => {
|
||||
return state.userType == 1 || state.userType == 5;
|
||||
},
|
||||
canSubscribeToNotifications: state => {
|
||||
switch (state.userType) {
|
||||
case 1:
|
||||
case 2:
|
||||
return true;
|
||||
case 3:
|
||||
case 4:
|
||||
//customer / headoffice and some notifications are enabled for them
|
||||
return (
|
||||
state.customerRights.notifyServiceImminent == true ||
|
||||
state.customerRights.notifyCSRAccepted == true ||
|
||||
state.customerRights.notifyCSRRejected == true ||
|
||||
state.customerRights.notifyWOCompleted == true ||
|
||||
state.customerRights.notifyWOCreated == true
|
||||
);
|
||||
case 5: //subcontractor for now no notification subscriptions available
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isSuperUser: state => {
|
||||
return state.userId === 1;
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
setLastClientVersion(state, data) {
|
||||
state.lastClientVersion = data;
|
||||
},
|
||||
login(state, data) {
|
||||
// mutate state
|
||||
state.authenticated = data.authenticated;
|
||||
state.userId = data.userId;
|
||||
state.roles = data.roles;
|
||||
state.apiToken = data.apiToken;
|
||||
state.userName = data.userName;
|
||||
state.userType = data.userType;
|
||||
state.downloadToken = data.dlt;
|
||||
state.l = data.l;
|
||||
state.tfaEnabled = data.tfaEnabled;
|
||||
if (data.customerRights) {
|
||||
state.customerRights = data.customerRights;
|
||||
}
|
||||
},
|
||||
logout(state) {
|
||||
//Things that are reset on logout
|
||||
state.apiToken = "-";
|
||||
state.downloadToken = "-";
|
||||
state.l = false;
|
||||
state.tfaEnabled = undefined;
|
||||
state.customerRights = {};
|
||||
state.authenticated = false;
|
||||
state.userId = 0;
|
||||
state.userName = "NOT AUTHENTICATED";
|
||||
state.roles = 0;
|
||||
state.userType = 0;
|
||||
state.homePage = undefined;
|
||||
state.navItems = [];
|
||||
state.translationText = {};
|
||||
state.enums = {};
|
||||
state.formCustomTemplate = {};
|
||||
state.userOptions.languageOverride = "en-US";
|
||||
state.userOptions.timeZoneOverride = null;
|
||||
state.userOptions.currencyName = "USD";
|
||||
state.userOptions.hour12 = true;
|
||||
//state.userOptions.uiColor = "#000000ff";
|
||||
state.userOptions.emailAddress = null;
|
||||
state.userOptions.mapUrlTemplate = null;
|
||||
state.globalSettings = {};
|
||||
state.knownPassword = false;
|
||||
state.newNotificationCount = 0;
|
||||
},
|
||||
addNavItem(state, data) {
|
||||
state.navItems.push(data);
|
||||
},
|
||||
setTranslationText(state, data) {
|
||||
state.translationText[data.key] = data.value;
|
||||
},
|
||||
setFormCustomTemplateItem(state, data) {
|
||||
state.formCustomTemplate[data.formKey + "_concurrencyToken"] =
|
||||
data.concurrency;
|
||||
state.formCustomTemplate[data.formKey] = data.value;
|
||||
},
|
||||
setUserOptions(state, data) {
|
||||
// mutate state
|
||||
state.userOptions.languageOverride = data.languageOverride;
|
||||
state.userOptions.currencyName = data.currencyName;
|
||||
state.userOptions.hour12 = data.hour12;
|
||||
state.userOptions.timeZoneOverride = data.timeZoneOverride;
|
||||
state.userOptions.emailAddress = data.emailAddress;
|
||||
//state.userOptions.uiColor = data.uiColor;
|
||||
state.userOptions.mapUrlTemplate = data.mapUrlTemplate;
|
||||
},
|
||||
setGlobalSettings(state, data) {
|
||||
// mutate state
|
||||
state.globalSettings = data;
|
||||
},
|
||||
setEnum(state, data) {
|
||||
state.enums[data.enumKey] = data.items;
|
||||
},
|
||||
logItem(state, msg) {
|
||||
msg = new Date().toLocaleString("sv-SE") + "|" + msg;
|
||||
state.logArray.push(msg);
|
||||
if (state.logArray.length > MaxLogLength) {
|
||||
//remove beginning elements
|
||||
state.logArray = state.logArray.slice(
|
||||
state.logArray.length - MaxLogLength
|
||||
);
|
||||
}
|
||||
},
|
||||
clearAllFormSettings(state) {
|
||||
state.formSettings = {};
|
||||
},
|
||||
setFormSettings(state, data) {
|
||||
state.formSettings[data.formKey] = data.formSettings;
|
||||
},
|
||||
clearFormSettings(state, formKey) {
|
||||
delete state.formSettings[formKey];
|
||||
},
|
||||
setHomePage(state, data) {
|
||||
state.homePage = data;
|
||||
},
|
||||
setDarkMode(state, data) {
|
||||
state.darkMode = data;
|
||||
},
|
||||
setNativeDateTimeInput(state, data) {
|
||||
state.nativeDateTimeInput = data;
|
||||
},
|
||||
setKnownPassword(state, data) {
|
||||
state.knownPassword = data;
|
||||
},
|
||||
setNewNotificationCount(state, data) {
|
||||
state.newNotificationCount = data;
|
||||
},
|
||||
setTfaEnabled(state, data) {
|
||||
state.tfaEnabled = data;
|
||||
}
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
25
client/src/sw.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// This is the code piece that GenerateSW mode can't provide for us.
|
||||
// This code listens for the user's confirmation to update the app.
|
||||
//https://medium.com/@dougallrich/give-users-control-over-app-updates-in-vue-cli-3-pwas-20453aedc1f2
|
||||
self.addEventListener("message", e => {
|
||||
if (!e.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.data) {
|
||||
case "skipWaiting":
|
||||
self.skipWaiting();
|
||||
break;
|
||||
default:
|
||||
// NOOP
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
workbox.core.clientsClaim(); // Vue CLI 4 and Workbox v4, else
|
||||
// workbox.clientsClaim(); // Vue CLI 3 and Workbox v3.
|
||||
|
||||
// The precaching code provided by Workbox.
|
||||
self.__precacheManifest = [].concat(self.__precacheManifest || []);
|
||||
// workbox.precaching.suppressWarnings(); // Only used with Vue CLI 3 and Workbox v3.
|
||||
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
|
||||
315
client/src/views/adm-attachments.vue
Normal file
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row>
|
||||
<gz-error :error-box-message="formState.errorBoxMessage" />
|
||||
<v-col cols="12">
|
||||
<gz-data-table
|
||||
v-if="!jobActive"
|
||||
form-key="adm-attachments"
|
||||
data-list-key="AttachmentDataList"
|
||||
:show-select="true"
|
||||
:single-select="false"
|
||||
:reload="reload"
|
||||
data-cy="attachTable"
|
||||
@selection-change="handleSelected"
|
||||
/>
|
||||
|
||||
<template v-if="jobActive">
|
||||
<v-progress-circular indeterminate color="primary" :size="60" />
|
||||
</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense justify="center">
|
||||
<v-dialog v-model="moveDialog" persistent max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="text-h5">{{ $sock.t("MoveSelected") }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-select
|
||||
v-model="moveType"
|
||||
dense
|
||||
:items="selectLists.objectTypes"
|
||||
item-text="name"
|
||||
item-value="id"
|
||||
:label="$sock.t('SockType')"
|
||||
/>
|
||||
|
||||
<gz-pick-list
|
||||
v-if="moveType != 0"
|
||||
v-model="moveId"
|
||||
:aya-type="moveType"
|
||||
:show-edit-icon="false"
|
||||
:include-inactive="true"
|
||||
label="Id"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="blue darken-1" text @click="moveDialog = false">
|
||||
{{ $sock.t("Cancel") }}
|
||||
</v-btn>
|
||||
<v-btn color="blue darken-1" text @click="moveSelected()">
|
||||
{{ $sock.t("OK") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
const FORM_KEY = "adm-attachments";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
jobActive: false,
|
||||
reload: false,
|
||||
moveDialog: false,
|
||||
moveType: null,
|
||||
moveId: null,
|
||||
selectedItems: [],
|
||||
rights: window.$gz.role.defaultRightsObject(),
|
||||
selectLists: {
|
||||
objectTypes: []
|
||||
},
|
||||
formState: {
|
||||
ready: true,
|
||||
dirty: false,
|
||||
valid: true,
|
||||
readOnly: false,
|
||||
loading: false,
|
||||
errorBoxMessage: null,
|
||||
appError: null,
|
||||
serverError: {}
|
||||
}
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
this.rights = window.$gz.role.getRights(window.$gz.type.FileAttachment);
|
||||
window.$gz.eventBus.$on("menu-click", clickHandler);
|
||||
await fetchTranslatedText(this);
|
||||
await populateSelectionLists(this);
|
||||
generateMenu(this);
|
||||
this.handleSelected([]); //start out read only no selection state for batch ops options
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.$gz.eventBus.$off("menu-click", clickHandler);
|
||||
},
|
||||
methods: {
|
||||
canBatchOp() {
|
||||
return (
|
||||
this.rights.change &&
|
||||
this.selectedItems &&
|
||||
this.selectedItems.length > 0
|
||||
);
|
||||
},
|
||||
handleSelected(selected) {
|
||||
this.selectedItems = selected;
|
||||
|
||||
if (this.canBatchOp()) {
|
||||
window.$gz.eventBus.$emit(
|
||||
"menu-enable-item",
|
||||
FORM_KEY + ":DELETE_SELECTED"
|
||||
);
|
||||
window.$gz.eventBus.$emit(
|
||||
"menu-enable-item",
|
||||
FORM_KEY + ":MOVE_SELECTED"
|
||||
);
|
||||
} else {
|
||||
window.$gz.eventBus.$emit(
|
||||
"menu-disable-item",
|
||||
FORM_KEY + ":DELETE_SELECTED"
|
||||
);
|
||||
window.$gz.eventBus.$emit(
|
||||
"menu-disable-item",
|
||||
FORM_KEY + ":MOVE_SELECTED"
|
||||
);
|
||||
}
|
||||
},
|
||||
async moveSelected() {
|
||||
const vm = this;
|
||||
try {
|
||||
vm.moveDialog = false;
|
||||
window.$gz.form.deleteAllErrorBoxErrors(vm);
|
||||
const res = await window.$gz.api.upsert("attachment/batch-move", {
|
||||
idList: this.selectedItems,
|
||||
toType: this.moveType,
|
||||
toId: this.moveId
|
||||
});
|
||||
if (res.error) {
|
||||
vm.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(vm);
|
||||
}
|
||||
this.reload = !this.reload;
|
||||
} catch (ex) {
|
||||
window.$gz.errorHandler.handleFormError(ex, vm);
|
||||
}
|
||||
},
|
||||
async deleteSelected() {
|
||||
const vm = this;
|
||||
try {
|
||||
const dialogResult = await window.$gz.dialog.confirmDelete();
|
||||
if (dialogResult != true) {
|
||||
return;
|
||||
}
|
||||
window.$gz.form.deleteAllErrorBoxErrors(vm);
|
||||
const res = await window.$gz.api.upsert(
|
||||
"attachment/batch-delete",
|
||||
this.selectedItems
|
||||
);
|
||||
if (res.error) {
|
||||
vm.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(vm);
|
||||
}
|
||||
this.reload = !this.reload;
|
||||
} catch (ex) {
|
||||
window.$gz.errorHandler.handleFormError(ex, vm);
|
||||
}
|
||||
},
|
||||
async startMaintenanceJob() {
|
||||
const vm = this;
|
||||
try {
|
||||
//warning about job exclusivity
|
||||
const dialogResult = await window.$gz.dialog.confirmGeneric(
|
||||
"JobExclusiveWarning",
|
||||
"warning"
|
||||
);
|
||||
if (dialogResult == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
let jobId = await window.$gz.api.upsert("attachment/maintenance");
|
||||
if (jobId.error) {
|
||||
throw new Error(window.$gz.errorHandler.errorToString(jobId, vm));
|
||||
}
|
||||
jobId = jobId.jobId;
|
||||
vm.jobActive = true;
|
||||
let jobStatus = 1;
|
||||
while (vm.jobActive == true) {
|
||||
await window.$gz.util.sleepAsync(1000);
|
||||
//check if done
|
||||
jobStatus = await window.$gz.api.get(
|
||||
`job-operations/status/${jobId}`
|
||||
);
|
||||
if (jobStatus.error) {
|
||||
throw new Error(
|
||||
window.$gz.errorHandler.errorToString(jobStatus, vm)
|
||||
);
|
||||
}
|
||||
jobStatus = jobStatus.data;
|
||||
if (jobStatus == 4 || jobStatus == 0) {
|
||||
throw new Error("Job failed");
|
||||
}
|
||||
if (jobStatus == 3) {
|
||||
vm.jobActive = false;
|
||||
}
|
||||
}
|
||||
window.$gz.eventBus.$emit("notify-success", vm.$sock.t("JobCompleted"));
|
||||
this.reload = !this.reload;
|
||||
} catch (error) {
|
||||
vm.jobActive = false;
|
||||
window.$gz.errorHandler.handleFormError(error, vm);
|
||||
window.$gz.eventBus.$emit("notify-error", vm.$sock.t("JobFailed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/////////////////////////////
|
||||
//
|
||||
//
|
||||
function clickHandler(menuItem) {
|
||||
if (!menuItem) {
|
||||
return;
|
||||
}
|
||||
const m = window.$gz.menu.parseMenuItem(menuItem);
|
||||
if (m.owner == FORM_KEY && !m.disabled) {
|
||||
switch (m.key) {
|
||||
case "START_MAINTENANCE_JOB":
|
||||
m.vm.startMaintenanceJob();
|
||||
break;
|
||||
case "MOVE_SELECTED":
|
||||
m.vm.moveDialog = true;
|
||||
|
||||
break;
|
||||
case "DELETE_SELECTED":
|
||||
m.vm.deleteSelected();
|
||||
break;
|
||||
default:
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-warning",
|
||||
FORM_KEY + "::context click: [" + m.key + "]"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
//
|
||||
//
|
||||
function generateMenu(vm) {
|
||||
const menuOptions = {
|
||||
isMain: true,
|
||||
icon: "$sockiPaperclip",
|
||||
title: "Attachments",
|
||||
helpUrl: "adm-attachments",
|
||||
menuItems: []
|
||||
};
|
||||
|
||||
if (vm.rights.change) {
|
||||
menuOptions.menuItems.push({
|
||||
title: "StartAttachmentMaintenanceJob",
|
||||
icon: "$sockiRobot",
|
||||
surface: false,
|
||||
key: FORM_KEY + ":START_MAINTENANCE_JOB",
|
||||
vm: vm
|
||||
});
|
||||
|
||||
menuOptions.menuItems.push({
|
||||
title: "MoveSelected",
|
||||
icon: "$sockiExchangeAlt",
|
||||
surface: false,
|
||||
key: FORM_KEY + ":MOVE_SELECTED",
|
||||
vm: vm
|
||||
});
|
||||
|
||||
menuOptions.menuItems.push({
|
||||
title: "DeleteSelected",
|
||||
icon: "$sockiTrashAlt",
|
||||
surface: false,
|
||||
key: FORM_KEY + ":DELETE_SELECTED",
|
||||
vm: vm
|
||||
});
|
||||
}
|
||||
|
||||
window.$gz.eventBus.$emit("menu-change", menuOptions);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// Ensures UI translated text is available
|
||||
//
|
||||
async function fetchTranslatedText() {
|
||||
await window.$gz.translation.cacheTranslations([
|
||||
"StartAttachmentMaintenanceJob",
|
||||
"JobExclusiveWarning",
|
||||
"DeleteSelected",
|
||||
"MoveSelected"
|
||||
]);
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
//
|
||||
//
|
||||
async function populateSelectionLists(vm) {
|
||||
const res = await window.$gz.api.get("enum-list/list/coreall");
|
||||
if (res.error) {
|
||||
vm.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(vm);
|
||||
} else {
|
||||
vm.selectLists.objectTypes = res.data;
|
||||
window.$gz.form.addNoSelectionItem(vm.selectLists.objectTypes);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
282
client/src/views/adm-global-logo.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<v-row v-if="formState.ready" dense>
|
||||
<v-col>
|
||||
<v-form ref="form">
|
||||
<!-- Prevent implicit submission of the form on enter key, this is not necessary on a form with a text area which is why I never noticed it with the other forms -->
|
||||
<button
|
||||
type="submit"
|
||||
disabled
|
||||
style="display: none"
|
||||
aria-hidden="true"
|
||||
></button>
|
||||
<v-row dense>
|
||||
<gz-error :error-box-message="formState.errorBoxMessage"></gz-error>
|
||||
<v-col cols="12">
|
||||
<img
|
||||
class="grey lighten-2"
|
||||
:src="smallUrl"
|
||||
:alt="$sock.t('SmallLogo')"
|
||||
/>
|
||||
<v-file-input
|
||||
v-model="uploadSmall"
|
||||
dense
|
||||
accept="image/*"
|
||||
show-size
|
||||
:label="$sock.t('SmallLogo')"
|
||||
:rules="rules"
|
||||
data-cy="uploadSmall"
|
||||
></v-file-input>
|
||||
<v-btn color="primary" text @click="remove('small')">{{
|
||||
$sock.t("Delete")
|
||||
}}</v-btn>
|
||||
<v-btn color="primary" text @click="upload('small')">{{
|
||||
$sock.t("Upload")
|
||||
}}</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" class="mt-10">
|
||||
<img
|
||||
class="mt-10 grey lighten-2"
|
||||
:src="mediumUrl"
|
||||
:alt="$sock.t('MediumLogo')"
|
||||
/>
|
||||
<v-file-input
|
||||
v-model="uploadMedium"
|
||||
dense
|
||||
accept="image/*"
|
||||
show-size
|
||||
:label="$sock.t('MediumLogo')"
|
||||
:rules="rules"
|
||||
></v-file-input>
|
||||
<v-btn color="primary" text @click="remove('medium')">{{
|
||||
$sock.t("Delete")
|
||||
}}</v-btn>
|
||||
<v-btn color="primary" text @click="upload('medium')">{{
|
||||
$sock.t("Upload")
|
||||
}}</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" class="mt-10">
|
||||
<img
|
||||
class="mt-10 grey lighten-2"
|
||||
:src="largeUrl"
|
||||
:alt="$sock.t('LargeLogo')"
|
||||
/>
|
||||
<v-file-input
|
||||
v-model="uploadLarge"
|
||||
dense
|
||||
accept="image/*"
|
||||
show-size
|
||||
:label="$sock.t('LargeLogo')"
|
||||
:rules="rules"
|
||||
></v-file-input>
|
||||
<v-btn color="primary" text @click="remove('large')">{{
|
||||
$sock.t("Delete")
|
||||
}}</v-btn>
|
||||
<v-btn color="primary" text @click="upload('large')">{{
|
||||
$sock.t("Upload")
|
||||
}}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
//
|
||||
//NOTE: This is a simple form with no need for business rules or validation so stripped out any extraneous code related to all that
|
||||
//
|
||||
const FORM_KEY = "adm-global-logo";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
uploadSmall: null,
|
||||
uploadMedium: null,
|
||||
uploadLarge: null,
|
||||
mediumUrl: null,
|
||||
largeUrl: null,
|
||||
smallUrl: null,
|
||||
fileSizeExceededWarning: "",
|
||||
formState: {
|
||||
ready: false,
|
||||
dirty: false,
|
||||
valid: true,
|
||||
readOnly: false,
|
||||
loading: true,
|
||||
errorBoxMessage: null,
|
||||
appError: null,
|
||||
serverError: {}
|
||||
},
|
||||
rights: window.$gz.role.getRights(window.$gz.type.Global),
|
||||
rules: [
|
||||
value => !value || value.size < 512000 || this.fileSizeExceededWarning
|
||||
]
|
||||
};
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.$gz.eventBus.$off("menu-click", clickHandler);
|
||||
},
|
||||
async created() {
|
||||
const vm = this;
|
||||
try {
|
||||
await initForm(vm);
|
||||
vm.formState.ready = true;
|
||||
vm.readOnly = !vm.rights.change;
|
||||
window.$gz.eventBus.$on("menu-click", clickHandler);
|
||||
//NOTE: this would normally be in getDataFromAPI but this form doesn't really need that function so doing it here
|
||||
generateMenu(vm);
|
||||
vm.smallUrl = `${window.$gz.api.logoUrl("small")}?x=${Date.now()}`;
|
||||
vm.mediumUrl = `${window.$gz.api.logoUrl("medium")}?x=${Date.now()}`;
|
||||
vm.largeUrl = `${window.$gz.api.logoUrl("large")}?x=${Date.now()}`;
|
||||
vm.fileSizeExceededWarning = vm.$sock
|
||||
.t("AyaFileFileTooLarge")
|
||||
.replace("{0}", "512KiB");
|
||||
vm.formState.loading = false;
|
||||
} catch (err) {
|
||||
vm.formState.ready = true;
|
||||
window.$gz.errorHandler.handleFormError(err, vm);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
imageUrl(size) {
|
||||
return window.$gz.api.logoUrl(size);
|
||||
},
|
||||
async upload(size) {
|
||||
//similar code in wiki-control
|
||||
const vm = this;
|
||||
let fileData = null;
|
||||
switch (size) {
|
||||
case "small":
|
||||
fileData = vm.uploadSmall;
|
||||
break;
|
||||
case "medium":
|
||||
fileData = vm.uploadMedium;
|
||||
break;
|
||||
case "large":
|
||||
fileData = vm.uploadLarge;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (fileData == null) {
|
||||
return;
|
||||
}
|
||||
if (fileData.size > 512000) {
|
||||
window.$gz.eventBus.$emit("notify-error", vm.fileSizeExceededWarning);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await window.$gz.api.uploadLogo(fileData, size);
|
||||
if (res.error) {
|
||||
window.$gz.errorHandler.handleFormError(res.error);
|
||||
} else {
|
||||
switch (size) {
|
||||
case "small":
|
||||
vm.smallUrl = `${window.$gz.api.logoUrl(size)}?x=${Date.now()}`;
|
||||
break;
|
||||
case "medium":
|
||||
vm.mediumUrl = `${window.$gz.api.logoUrl(size)}?x=${Date.now()}`;
|
||||
break;
|
||||
case "large":
|
||||
vm.largeUrl = `${window.$gz.api.logoUrl(size)}?x=${Date.now()}`;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error);
|
||||
}
|
||||
},
|
||||
|
||||
async remove(size) {
|
||||
const vm = this;
|
||||
|
||||
try {
|
||||
const dialogResult = await window.$gz.dialog.confirmDelete();
|
||||
if (dialogResult != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = "logo/" + size;
|
||||
|
||||
window.$gz.form.deleteAllErrorBoxErrors(vm);
|
||||
const res = await window.$gz.api.remove(url);
|
||||
if (res.error) {
|
||||
vm.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(vm);
|
||||
} else {
|
||||
switch (size) {
|
||||
case "small":
|
||||
vm.smallUrl = null;
|
||||
break;
|
||||
case "medium":
|
||||
vm.mediumUrl = null;
|
||||
break;
|
||||
case "large":
|
||||
vm.largeUrl = null;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error, vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/////////////////////////////
|
||||
//
|
||||
//
|
||||
function clickHandler(menuItem) {
|
||||
if (!menuItem) {
|
||||
return;
|
||||
}
|
||||
const m = window.$gz.menu.parseMenuItem(menuItem);
|
||||
if (m.owner == FORM_KEY && !m.disabled) {
|
||||
switch (m.key) {
|
||||
default:
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-warning",
|
||||
FORM_KEY + "::context click: [" + m.key + "]"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
//
|
||||
//
|
||||
function generateMenu(vm) {
|
||||
const menuOptions = {
|
||||
isMain: false,
|
||||
readOnly: vm.formState.readOnly,
|
||||
icon: null,
|
||||
title: "GlobalLogo",
|
||||
helpUrl: "adm-global-logo",
|
||||
menuItems: []
|
||||
};
|
||||
window.$gz.eventBus.$emit("menu-change", menuOptions);
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
//
|
||||
//
|
||||
async function initForm(vm) {
|
||||
await fetchTranslatedText(vm);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// Ensures UI translated text is available
|
||||
//
|
||||
async function fetchTranslatedText() {
|
||||
await window.$gz.translation.cacheTranslations([
|
||||
"SmallLogo",
|
||||
"MediumLogo",
|
||||
"LargeLogo",
|
||||
"GlobalLogo",
|
||||
"AyaFileFileTooLarge"
|
||||
]);
|
||||
}
|
||||
</script>
|
||||
298
client/src/views/adm-global-seeds.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<v-row v-if="formState.ready" dense>
|
||||
<v-col>
|
||||
<v-form ref="form">
|
||||
<!-- Prevent implicit submission of the form on enter key, this is not necessary on a form with a text area which is why I never noticed it with the other forms -->
|
||||
<button
|
||||
type="submit"
|
||||
disabled
|
||||
style="display: none"
|
||||
aria-hidden="true"
|
||||
></button>
|
||||
<v-row dense>
|
||||
<gz-error :error-box-message="formState.errorBoxMessage"></gz-error>
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="purchaseOrderNextSerial"
|
||||
v-model="obj.purchaseOrderNextSerial"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('NextPONumber')"
|
||||
data-cy="purchaseOrderNextSerial"
|
||||
:rules="[
|
||||
form().integerValid(this, 'purchaseOrderNextSerial'),
|
||||
form().required(this, 'purchaseOrderNextSerial')
|
||||
]"
|
||||
:error-messages="
|
||||
form().serverErrors(this, 'purchaseOrderNextSerial')
|
||||
"
|
||||
append-outer-icon="$sockiSave"
|
||||
@input="fieldValueChanged('purchaseOrderNextSerial')"
|
||||
@click:append-outer="
|
||||
submit(sockTypes().PurchaseOrder, obj.purchaseOrderNextSerial)
|
||||
"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="workorderNextSerial"
|
||||
v-model="obj.workorderNextSerial"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('NextWorkorderNumber')"
|
||||
data-cy="workorderNextSerial"
|
||||
:rules="[
|
||||
form().integerValid(this, 'workorderNextSerial'),
|
||||
form().required(this, 'workorderNextSerial')
|
||||
]"
|
||||
:error-messages="form().serverErrors(this, 'workorderNextSerial')"
|
||||
append-outer-icon="$sockiSave"
|
||||
@input="fieldValueChanged('workorderNextSerial')"
|
||||
@click:append-outer="
|
||||
submit(sockTypes().WorkOrder, obj.workorderNextSerial)
|
||||
"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="quoteNextSerial"
|
||||
v-model="obj.quoteNextSerial"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('NextQuoteNumber')"
|
||||
data-cy="quoteNextSerial"
|
||||
:rules="[
|
||||
form().integerValid(this, 'quoteNextSerial'),
|
||||
form().required(this, 'quoteNextSerial')
|
||||
]"
|
||||
:error-messages="form().serverErrors(this, 'quoteNextSerial')"
|
||||
append-outer-icon="$sockiSave"
|
||||
@input="fieldValueChanged('quoteNextSerial')"
|
||||
@click:append-outer="
|
||||
submit(sockTypes().Quote, obj.quoteNextSerial)
|
||||
"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="pmNextSerial"
|
||||
v-model="obj.pmNextSerial"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('NextPMNumber')"
|
||||
data-cy="pmNextSerial"
|
||||
:rules="[
|
||||
form().integerValid(this, 'pmNextSerial'),
|
||||
form().required(this, 'pmNextSerial')
|
||||
]"
|
||||
:error-messages="form().serverErrors(this, 'pmNextSerial')"
|
||||
append-outer-icon="$sockiSave"
|
||||
@input="fieldValueChanged('pmNextSerial')"
|
||||
@click:append-outer="submit(sockTypes().PM, obj.pmNextSerial)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
//
|
||||
//NOTE: This is a simple form with no need for business rules or validation so stripped out any extraneous code related to all that
|
||||
//
|
||||
const FORM_KEY = "adm-global-seeds";
|
||||
const API_BASE_URL = "global-biz-setting/seeds/";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
obj: {
|
||||
purchaseOrderNextSerial: null,
|
||||
workorderNextSerial: null,
|
||||
quoteNextSerial: null,
|
||||
pmNextSerial: null
|
||||
},
|
||||
formState: {
|
||||
ready: false,
|
||||
dirty: false,
|
||||
valid: true,
|
||||
readOnly: false,
|
||||
loading: true,
|
||||
errorBoxMessage: null,
|
||||
appError: null,
|
||||
serverError: {}
|
||||
},
|
||||
rights: window.$gz.role.getRights(window.$gz.type.Global)
|
||||
};
|
||||
}, //WATCHERS
|
||||
watch: {
|
||||
formState: {
|
||||
handler: function(val) {
|
||||
if (this.formState.loading) {
|
||||
return;
|
||||
}
|
||||
const canSave = val.dirty && val.valid && !val.readOnly;
|
||||
if (canSave) {
|
||||
window.$gz.eventBus.$emit("menu-enable-item", FORM_KEY + ":save");
|
||||
} else {
|
||||
window.$gz.eventBus.$emit("menu-disable-item", FORM_KEY + ":save");
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.$gz.eventBus.$off("menu-click", clickHandler);
|
||||
},
|
||||
async created() {
|
||||
const vm = this;
|
||||
try {
|
||||
await initForm(vm);
|
||||
vm.formState.ready = true;
|
||||
vm.readOnly = !vm.rights.change;
|
||||
window.$gz.eventBus.$on("menu-click", clickHandler);
|
||||
//NOTE: this would normally be in getDataFromAPI but this form doesn't really need that function so doing it here
|
||||
generateMenu(vm);
|
||||
await vm.getDataFromApi();
|
||||
vm.formState.loading = false;
|
||||
} catch (err) {
|
||||
vm.formState.ready = true;
|
||||
window.$gz.errorHandler.handleFormError(err, vm);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sockTypes: function() {
|
||||
return window.$gz.type;
|
||||
},
|
||||
form() {
|
||||
return window.$gz.form;
|
||||
},
|
||||
fieldValueChanged(ref) {
|
||||
if (
|
||||
this.formState.ready &&
|
||||
!this.formState.loading &&
|
||||
!this.formState.readOnly
|
||||
) {
|
||||
window.$gz.form.fieldValueChanged(this, ref);
|
||||
}
|
||||
},
|
||||
async getDataFromApi() {
|
||||
const vm = this;
|
||||
vm.formState.loading = true;
|
||||
|
||||
window.$gz.form.deleteAllErrorBoxErrors(vm);
|
||||
try {
|
||||
const res = await window.$gz.api.get(API_BASE_URL);
|
||||
|
||||
if (res.error) {
|
||||
vm.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(vm);
|
||||
} else {
|
||||
vm.obj = res.data;
|
||||
|
||||
window.$gz.form.setFormState({
|
||||
vm: vm,
|
||||
dirty: false,
|
||||
valid: true,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.form.setFormState({
|
||||
vm: vm,
|
||||
loading: false
|
||||
});
|
||||
window.$gz.errorHandler.handleFormError(error, vm);
|
||||
}
|
||||
},
|
||||
async submit(aType, nextNumber) {
|
||||
window.$gz.form.deleteAllErrorBoxErrors(this);
|
||||
try {
|
||||
const res = await window.$gz.api.put(
|
||||
`${API_BASE_URL}${aType}/${nextNumber}`
|
||||
);
|
||||
this.formState.loading = false;
|
||||
if (res.error) {
|
||||
this.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(this);
|
||||
} else {
|
||||
window.$gz.form.setFormState({
|
||||
vm: this,
|
||||
dirty: false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.formState.loading = false;
|
||||
window.$gz.errorHandler.handleFormError(error, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/////////////////////////////
|
||||
//
|
||||
//
|
||||
function clickHandler(menuItem) {
|
||||
if (!menuItem) {
|
||||
return;
|
||||
}
|
||||
const m = window.$gz.menu.parseMenuItem(menuItem);
|
||||
if (m.owner == FORM_KEY && !m.disabled) {
|
||||
switch (m.key) {
|
||||
case "save":
|
||||
m.vm.submit();
|
||||
break;
|
||||
case "delete":
|
||||
m.vm.remove();
|
||||
break;
|
||||
default:
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-warning",
|
||||
FORM_KEY + "::context click: [" + m.key + "]"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
//
|
||||
//
|
||||
function generateMenu(vm) {
|
||||
const menuOptions = {
|
||||
isMain: false,
|
||||
readOnly: vm.formState.readOnly,
|
||||
icon: null,
|
||||
title: "GlobalNextSeeds",
|
||||
helpUrl: "adm-global-seeds",
|
||||
formData: {
|
||||
sockType: window.$gz.type.global,
|
||||
formCustomTemplateKey: undefined
|
||||
},
|
||||
menuItems: []
|
||||
};
|
||||
window.$gz.eventBus.$emit("menu-change", menuOptions);
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
//
|
||||
//
|
||||
async function initForm(vm) {
|
||||
await fetchTranslatedText(vm);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// Ensures UI translated text is available
|
||||
//
|
||||
async function fetchTranslatedText() {
|
||||
await window.$gz.translation.cacheTranslations([
|
||||
"GlobalNextSeeds",
|
||||
"NextPONumber",
|
||||
"NextQuoteNumber",
|
||||
"NextWorkorderNumber",
|
||||
"NextPMNumber"
|
||||
]);
|
||||
}
|
||||
</script>
|
||||
479
client/src/views/adm-global-select-templates.vue
Normal file
@@ -0,0 +1,479 @@
|
||||
<template>
|
||||
<v-row v-if="formState.ready">
|
||||
<v-col dense>
|
||||
<v-form ref="form">
|
||||
<!-- Prevent implicit submission of the form on enter key, this is not necessary on a form with a text area which is why I never noticed it with the other forms -->
|
||||
<button
|
||||
type="submit"
|
||||
disabled
|
||||
style="display: none"
|
||||
aria-hidden="true"
|
||||
></button>
|
||||
<v-row dense>
|
||||
<gz-error :error-box-message="formState.errorBoxMessage"></gz-error>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="templateId"
|
||||
dense
|
||||
:items="selectLists.pickListTemplates"
|
||||
item-text="name"
|
||||
item-value="id"
|
||||
:label="$sock.t('PickListTemplates')"
|
||||
data-cy="selectTemplate"
|
||||
:disabled="formState.dirty"
|
||||
@input="templateSelected"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<template v-for="(item, index) in workingArray">
|
||||
<v-col :key="item.key" cols="12" sm="6" lg="4" xl="2" px-2>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ item.title }}
|
||||
</v-card-title>
|
||||
<v-card-subtitle>
|
||||
{{ item.key }}
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-checkbox
|
||||
:ref="item.key"
|
||||
v-model="item.include"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('Include')"
|
||||
:disabled="item.required"
|
||||
:data-cy="item.key + 'Include'"
|
||||
@change="includeChanged(item)"
|
||||
></v-checkbox>
|
||||
<!-- RE-ORDER CONTROL -->
|
||||
<div class="d-flex justify-space-between">
|
||||
<v-btn icon @click="move('start', index)"
|
||||
><v-icon>$sockiStepBackward</v-icon></v-btn
|
||||
>
|
||||
<v-btn icon @click="move('left', index)"
|
||||
><v-icon>$sockiBackward</v-icon></v-btn
|
||||
>
|
||||
<v-btn icon @click="move('right', index)"
|
||||
><v-icon>$sockiForward</v-icon></v-btn
|
||||
>
|
||||
<v-btn icon @click="move('end', index)"
|
||||
><v-icon>$sockiStepForward</v-icon></v-btn
|
||||
>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
//
|
||||
//NOTE: This is a simple form with no need for business rules or validation so stripped out any extraneous code related to all that
|
||||
//
|
||||
const FORM_KEY = "adm-global-select-templates";
|
||||
const API_BASE_URL = "pick-list/template/";
|
||||
export default {
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
if (!this.formState.dirty) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
if ((await window.$gz.dialog.confirmLeaveUnsaved()) === true) {
|
||||
next();
|
||||
} else {
|
||||
next(false);
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
obj: {
|
||||
id: null,
|
||||
concurrency: null,
|
||||
template: null
|
||||
},
|
||||
selectLists: {
|
||||
pickListTemplates: []
|
||||
},
|
||||
availableFields: [],
|
||||
workingArray: [],
|
||||
fieldKeys: [],
|
||||
templateId: 0,
|
||||
lastFetchedTemplateId: 0,
|
||||
formState: {
|
||||
ready: false,
|
||||
dirty: false,
|
||||
valid: true,
|
||||
readOnly: false,
|
||||
loading: true,
|
||||
errorBoxMessage: null,
|
||||
appError: null,
|
||||
serverError: {}
|
||||
},
|
||||
rights: window.$gz.role.getRights(window.$gz.type.FormCustom)
|
||||
};
|
||||
}, //WATCHERS
|
||||
watch: {
|
||||
formState: {
|
||||
handler: function(val) {
|
||||
if (this.formState.loading) {
|
||||
return;
|
||||
}
|
||||
const canSave = val.dirty && val.valid && !val.readOnly;
|
||||
if (canSave) {
|
||||
window.$gz.eventBus.$emit("menu-enable-item", FORM_KEY + ":save");
|
||||
} else {
|
||||
window.$gz.eventBus.$emit("menu-disable-item", FORM_KEY + ":save");
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
templateId(val) {
|
||||
if (val) {
|
||||
window.$gz.eventBus.$emit("menu-enable-item", FORM_KEY + ":delete");
|
||||
} else {
|
||||
window.$gz.eventBus.$emit("menu-disable-item", FORM_KEY + ":delete");
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.$gz.eventBus.$off("menu-click", clickHandler);
|
||||
},
|
||||
async created() {
|
||||
const vm = this;
|
||||
try {
|
||||
await initForm(vm);
|
||||
vm.formState.ready = true;
|
||||
vm.readOnly = !vm.rights.change;
|
||||
window.$gz.eventBus.$on("menu-click", clickHandler);
|
||||
//NOTE: this would normally be in getDataFromAPI but this form doesn't really need that function so doing it here
|
||||
generateMenu(vm);
|
||||
//init disable save button so it can be enabled only on edit to show dirty form
|
||||
window.$gz.eventBus.$emit("menu-disable-item", FORM_KEY + ":save");
|
||||
window.$gz.eventBus.$emit("menu-disable-item", FORM_KEY + ":delete");
|
||||
vm.formState.loading = false;
|
||||
} catch (err) {
|
||||
vm.formState.ready = true;
|
||||
window.$gz.errorHandler.handleFormError(err, vm);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
includeChanged: function() {
|
||||
window.$gz.form.setFormState({
|
||||
vm: this,
|
||||
dirty: true
|
||||
});
|
||||
},
|
||||
move: function(direction, index) {
|
||||
const totalItems = this.workingArray.length;
|
||||
let newIndex = 0;
|
||||
//calculate new index
|
||||
switch (direction) {
|
||||
case "start":
|
||||
newIndex = 0;
|
||||
break;
|
||||
case "left":
|
||||
newIndex = index - 1;
|
||||
if (newIndex < 0) {
|
||||
newIndex = 0;
|
||||
}
|
||||
break;
|
||||
case "right":
|
||||
newIndex = index + 1;
|
||||
if (newIndex > totalItems - 1) {
|
||||
newIndex = totalItems - 1;
|
||||
}
|
||||
|
||||
break;
|
||||
case "end":
|
||||
newIndex = totalItems - 1;
|
||||
break;
|
||||
}
|
||||
|
||||
this.workingArray.splice(
|
||||
newIndex,
|
||||
0,
|
||||
this.workingArray.splice(index, 1)[0]
|
||||
);
|
||||
window.$gz.form.setFormState({
|
||||
vm: this,
|
||||
dirty: true
|
||||
});
|
||||
},
|
||||
templateSelected: function() {
|
||||
const vm = this;
|
||||
if (vm.lastFetchedTemplateId == vm.templateId) {
|
||||
return; //no change
|
||||
}
|
||||
vm.workingArray = [];
|
||||
if (!vm.templateId || vm.templateId == 0) {
|
||||
vm.lastFetchedTemplateId = 0;
|
||||
return;
|
||||
} else {
|
||||
vm.getDataFromApi();
|
||||
}
|
||||
},
|
||||
async getDataFromApi() {
|
||||
const vm = this;
|
||||
vm.formState.loading = true;
|
||||
if (!vm.templateId || vm.templateId == 0) {
|
||||
return;
|
||||
}
|
||||
vm.lastFetchedTemplateId = vm.templateId;
|
||||
window.$gz.form.deleteAllErrorBoxErrors(vm);
|
||||
try {
|
||||
let res = await window.$gz.api.get(
|
||||
API_BASE_URL + "listfields/" + vm.templateId
|
||||
);
|
||||
|
||||
if (res.error) {
|
||||
vm.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(vm);
|
||||
} else {
|
||||
vm.availableFields = res.data;
|
||||
vm.fieldKeys = [];
|
||||
for (let i = 0; i < res.data.length; i++) {
|
||||
vm.fieldKeys.push(res.data[i].tKey);
|
||||
}
|
||||
await window.$gz.translation.cacheTranslations(vm.fieldKeys);
|
||||
}
|
||||
//get current edited template
|
||||
res = await window.$gz.api.get(API_BASE_URL + vm.templateId);
|
||||
if (res.error) {
|
||||
vm.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(vm);
|
||||
} else {
|
||||
vm.obj = res.data;
|
||||
synthesizeWorkingArray(vm);
|
||||
window.$gz.form.setFormState({
|
||||
vm: vm,
|
||||
dirty: false,
|
||||
valid: true,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.form.setFormState({
|
||||
vm: vm,
|
||||
loading: false
|
||||
});
|
||||
window.$gz.errorHandler.handleFormError(error, vm);
|
||||
}
|
||||
},
|
||||
async submit() {
|
||||
window.$gz.form.deleteAllErrorBoxErrors(this);
|
||||
//Create template data object here....
|
||||
//Note that server expects to see a string array of json template, not actual json
|
||||
const newObj = {
|
||||
id: this.templateId,
|
||||
template: "[]"
|
||||
};
|
||||
//temporary array to hold template for later stringification
|
||||
const temp = [];
|
||||
for (let i = 0; i < this.workingArray.length; i++) {
|
||||
const ti = this.workingArray[i];
|
||||
if (ti.include == true) {
|
||||
temp.push({
|
||||
fld: ti.key
|
||||
});
|
||||
}
|
||||
}
|
||||
try {
|
||||
//now set the template as a json string
|
||||
newObj.template = JSON.stringify(temp);
|
||||
const res = await window.$gz.api.upsert(API_BASE_URL, newObj);
|
||||
this.formState.loading = false;
|
||||
if (res.error) {
|
||||
this.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(this);
|
||||
} else {
|
||||
//It's a 204 no data response so no error means it's ok
|
||||
//form is now clean
|
||||
window.$gz.form.setFormState({
|
||||
vm: this,
|
||||
dirty: false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.formState.loading = false;
|
||||
window.$gz.errorHandler.handleFormError(error, this);
|
||||
}
|
||||
},
|
||||
async remove() {
|
||||
if (
|
||||
(await window.$gz.dialog.confirmGeneric(
|
||||
"ResetToDefault",
|
||||
"warning"
|
||||
)) !== true
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.formState.loading = true;
|
||||
if (this.templateId && this.templateId != 0) {
|
||||
window.$gz.form.deleteAllErrorBoxErrors(this);
|
||||
const res = await window.$gz.api.remove(
|
||||
API_BASE_URL + this.templateId
|
||||
);
|
||||
if (res.error) {
|
||||
this.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(this);
|
||||
} else {
|
||||
//trigger reload of form
|
||||
this.getDataFromApi();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error, this);
|
||||
} finally {
|
||||
window.$gz.form.setFormState({
|
||||
vm: this,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/////////////////////////////
|
||||
//
|
||||
//
|
||||
function clickHandler(menuItem) {
|
||||
if (!menuItem) {
|
||||
return;
|
||||
}
|
||||
const m = window.$gz.menu.parseMenuItem(menuItem);
|
||||
if (m.owner == FORM_KEY && !m.disabled) {
|
||||
switch (m.key) {
|
||||
case "save":
|
||||
m.vm.submit();
|
||||
break;
|
||||
case "delete":
|
||||
m.vm.remove();
|
||||
break;
|
||||
default:
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-warning",
|
||||
FORM_KEY + "::context click: [" + m.key + "]"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
//
|
||||
//
|
||||
function generateMenu(vm) {
|
||||
const menuOptions = {
|
||||
isMain: false,
|
||||
readOnly: vm.formState.readOnly,
|
||||
icon: null,
|
||||
title: "PickListTemplates",
|
||||
helpUrl: "adm-global-autocomplete-templates",
|
||||
formData: {
|
||||
sockType: window.$gz.type.FormCustom,
|
||||
formCustomTemplateKey: undefined
|
||||
},
|
||||
menuItems: []
|
||||
};
|
||||
|
||||
if (vm.rights.change) {
|
||||
menuOptions.menuItems.push({
|
||||
title: "Save",
|
||||
icon: "$sockiSave",
|
||||
surface: true,
|
||||
key: FORM_KEY + ":save",
|
||||
vm: vm
|
||||
});
|
||||
|
||||
if (vm.rights.delete) {
|
||||
menuOptions.menuItems.push({
|
||||
title: "ResetToDefault",
|
||||
icon: "$sockiUndo",
|
||||
surface: false,
|
||||
key: FORM_KEY + ":delete",
|
||||
vm: vm
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.$gz.eventBus.$emit("menu-change", menuOptions);
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
//
|
||||
//
|
||||
async function initForm(vm) {
|
||||
await fetchTranslatedText();
|
||||
await populateSelectionLists(vm);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// Ensures UI translated text is available
|
||||
//
|
||||
async function fetchTranslatedText() {
|
||||
await window.$gz.translation.cacheTranslations(["Include", "ResetToDefault"]);
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
//
|
||||
//
|
||||
async function populateSelectionLists(vm) {
|
||||
const res = await window.$gz.api.get(API_BASE_URL + "list");
|
||||
if (res.error) {
|
||||
vm.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(vm);
|
||||
} else {
|
||||
res.data.sort(window.$gz.util.sortByKey("name"));
|
||||
vm.selectLists.pickListTemplates = res.data;
|
||||
window.$gz.form.addNoSelectionItem(vm.selectLists.pickListTemplates);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////
|
||||
//
|
||||
function synthesizeWorkingArray(vm) {
|
||||
vm.workingArray = [];
|
||||
if (vm.obj.template == null) {
|
||||
return;
|
||||
}
|
||||
const template = JSON.parse(vm.obj.template);
|
||||
//first, insert the templated fields into the working array so they are in current selected order
|
||||
for (let i = 0; i < template.length; i++) {
|
||||
const templateItem = template[i];
|
||||
const afItem = vm.availableFields.find(z => z.fieldKey == templateItem.fld);
|
||||
if (afItem != null) {
|
||||
//Push into working array
|
||||
vm.workingArray.push({
|
||||
key: afItem.fieldKey,
|
||||
required: afItem.isRowId == true,
|
||||
include: true,
|
||||
title: vm.$sock.t(afItem.tKey)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//Now iterate all the available fields and insert the ones that were not in the current template
|
||||
for (let i = 0; i < vm.availableFields.length; i++) {
|
||||
const afItem = vm.availableFields[i];
|
||||
//skip the active column
|
||||
if (afItem.isActiveColumn == true) {
|
||||
continue;
|
||||
}
|
||||
//is this field already in the template and was added above?
|
||||
if (template.find(z => z.fld == afItem.fieldKey) != null) {
|
||||
continue;
|
||||
}
|
||||
//Push into working array
|
||||
vm.workingArray.push({
|
||||
key: afItem.fieldKey,
|
||||
required: afItem.isRowId == true,
|
||||
include: false,
|
||||
title: vm.$sock.t(afItem.tKey)
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
686
client/src/views/adm-global-settings.vue
Normal file
@@ -0,0 +1,686 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="formState.ready">
|
||||
<gz-error :error-box-message="formState.errorBoxMessage"></gz-error>
|
||||
<v-form ref="form">
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<div class="text-h4 primary--text">
|
||||
{{ $sock.t("BusinessSettings") }}
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- --------------COMPANY ADDRESS ETC ---------------- -->
|
||||
|
||||
<v-col cols="12">
|
||||
<div class="text-h4 primary--text">
|
||||
{{ $sock.t("CompanyInformation") }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<gz-url
|
||||
ref="webAddress"
|
||||
v-model="obj.webAddress"
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('WebAddress')"
|
||||
data-cy="webAddress"
|
||||
:error-messages="form().serverErrors(this, 'webAddress')"
|
||||
@input="fieldValueChanged('webAddress')"
|
||||
></gz-url>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<gz-email
|
||||
ref="emailAddress"
|
||||
v-model="obj.emailAddress"
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('CompanyEmail')"
|
||||
data-cy="emailAddress"
|
||||
:error-messages="form().serverErrors(this, 'emailAddress')"
|
||||
@input="fieldValueChanged('emailAddress')"
|
||||
></gz-email>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<gz-phone
|
||||
ref="phone1"
|
||||
v-model="obj.phone1"
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('CompanyPhone1')"
|
||||
data-cy="phone1"
|
||||
:error-messages="form().serverErrors(this, 'phone1')"
|
||||
@input="fieldValueChanged('phone1')"
|
||||
></gz-phone>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<gz-phone
|
||||
ref="phone2"
|
||||
v-model="obj.phone2"
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('CompanyPhone2')"
|
||||
data-cy="phone2"
|
||||
:error-messages="form().serverErrors(this, 'phone2')"
|
||||
@input="fieldValueChanged('phone2')"
|
||||
></gz-phone>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<span class="text-subtitle-1">
|
||||
{{ $sock.t("AddressTypePhysical") }}</span
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="address"
|
||||
v-model="obj.address"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AddressDeliveryAddress')"
|
||||
data-cy="address"
|
||||
:error-messages="form().serverErrors(this, 'address')"
|
||||
@input="fieldValueChanged('address')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="city"
|
||||
v-model="obj.city"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AddressCity')"
|
||||
data-cy="city"
|
||||
:error-messages="form().serverErrors(this, 'city')"
|
||||
@input="fieldValueChanged('city')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="region"
|
||||
v-model="obj.region"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AddressStateProv')"
|
||||
data-cy="region"
|
||||
:error-messages="form().serverErrors(this, 'region')"
|
||||
@input="fieldValueChanged('region')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="country"
|
||||
v-model="obj.country"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AddressCountry')"
|
||||
data-cy="country"
|
||||
:error-messages="form().serverErrors(this, 'country')"
|
||||
@input="fieldValueChanged('country')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="addressPostal"
|
||||
v-model="obj.addressPostal"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AddressPostal')"
|
||||
data-cy="addressPostal"
|
||||
:error-messages="form().serverErrors(this, 'addressPostal')"
|
||||
@input="fieldValueChanged('addressPostal')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<gz-decimal
|
||||
ref="latitude"
|
||||
v-model="obj.latitude"
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AddressLatitude')"
|
||||
data-cy="latitude"
|
||||
:rules="[form().decimalValid(this, 'latitude')]"
|
||||
:error-messages="form().serverErrors(this, 'latitude')"
|
||||
:precision="6"
|
||||
@input="fieldValueChanged('latitude')"
|
||||
></gz-decimal>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<gz-decimal
|
||||
ref="longitude"
|
||||
v-model="obj.longitude"
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AddressLongitude')"
|
||||
data-cy="longitude"
|
||||
:rules="[form().decimalValid(this, 'longitude')]"
|
||||
:error-messages="form().serverErrors(this, 'longitude')"
|
||||
:precision="6"
|
||||
@input="fieldValueChanged('longitude')"
|
||||
></gz-decimal>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<span class="text-subtitle-1">
|
||||
{{ $sock.t("AddressTypePostal") }}</span
|
||||
>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="postAddress"
|
||||
v-model="obj.postAddress"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AddressPostalDeliveryAddress')"
|
||||
data-cy="postAddress"
|
||||
:error-messages="form().serverErrors(this, 'postAddress')"
|
||||
@input="fieldValueChanged('postAddress')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="postCity"
|
||||
v-model="obj.postCity"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AddressPostalCity')"
|
||||
data-cy="postCity"
|
||||
:error-messages="form().serverErrors(this, 'postCity')"
|
||||
@input="fieldValueChanged('postCity')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="postRegion"
|
||||
v-model="obj.postRegion"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AddressPostalStateProv')"
|
||||
data-cy="postRegion"
|
||||
:error-messages="form().serverErrors(this, 'postRegion')"
|
||||
@input="fieldValueChanged('postRegion')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="postCountry"
|
||||
v-model="obj.postCountry"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AddressPostalCountry')"
|
||||
data-cy="postCountry"
|
||||
:error-messages="form().serverErrors(this, 'postCountry')"
|
||||
@input="fieldValueChanged('postCountry')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-text-field
|
||||
ref="postCode"
|
||||
v-model="obj.postCode"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('AddressPostalPostal')"
|
||||
data-cy="postCode"
|
||||
:error-messages="form().serverErrors(this, 'postCode')"
|
||||
@input="fieldValueChanged('postCode')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- -------------------- /ADDRESS CONTACT INFO ------------------------------ -->
|
||||
|
||||
<v-col cols="12" class="mt-8">
|
||||
<span class="text-h4 primary--text">
|
||||
{{ $sock.t("UserInterfaceSettings") }}</span
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-btn to="adm-global-logo">{{ $sock.t("GlobalLogo") }}</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-btn to="adm-global-select-templates">{{
|
||||
$sock.t("PickListTemplates")
|
||||
}}</v-btn></v-col
|
||||
>
|
||||
|
||||
<v-col cols="12" class="mt-8">
|
||||
<div class="text-h4 primary--text">
|
||||
{{ $sock.t("CustomerAccessSettings") }}
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- ######################################################## -->
|
||||
<v-col cols="12">
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel key="3">
|
||||
<v-expansion-panel-header>
|
||||
<span class="text-h6">
|
||||
{{ $sock.t("UserSettings") }}
|
||||
</span>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-row dense>
|
||||
<v-col cols="12" sm="6" lg="2">
|
||||
<v-checkbox
|
||||
ref="customerAllowUserSettings"
|
||||
v-model="obj.customerAllowUserSettings"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('Active')"
|
||||
data-cy="customerAllowUserSettings"
|
||||
:error-messages="
|
||||
form().serverErrors(this, 'customerAllowUserSettings')
|
||||
"
|
||||
@change="fieldValueChanged('customerAllowUserSettings')"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
<template v-if="obj.customerAllowUserSettings">
|
||||
<v-col cols="12" sm="6" lg="10">
|
||||
<gz-tag-picker
|
||||
ref="customerAllowUserSettingsInTags"
|
||||
v-model="obj.customerAllowUserSettingsInTags"
|
||||
:readonly="formState.readOnly"
|
||||
:label="
|
||||
$sock.t('ContactCustomerHeadOfficeTaggedWith')
|
||||
"
|
||||
data-cy="customerAllowUserSettingsInTags"
|
||||
:error-messages="
|
||||
form().serverErrors(
|
||||
this,
|
||||
'customerAllowUserSettingsInTags'
|
||||
)
|
||||
"
|
||||
@input="
|
||||
fieldValueChanged('customerAllowUserSettingsInTags')
|
||||
"
|
||||
></gz-tag-picker>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel key="4">
|
||||
<v-expansion-panel-header>
|
||||
<span class="text-h6">
|
||||
{{ $sock.t("NotifySubscriptionList") }}
|
||||
</span>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-row dense>
|
||||
<v-col cols="12" sm="6" lg="3">
|
||||
<v-checkbox
|
||||
ref="customerAllowNotifyServiceImminent"
|
||||
v-model="obj.customerAllowNotifyServiceImminent"
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('NotifyEventCustomerServiceImminent')"
|
||||
data-cy="customerAllowNotifyServiceImminent"
|
||||
:error-messages="
|
||||
form().serverErrors(
|
||||
this,
|
||||
'customerAllowNotifyServiceImminent'
|
||||
)
|
||||
"
|
||||
@change="
|
||||
fieldValueChanged(
|
||||
'customerAllowNotifyServiceImminent'
|
||||
)
|
||||
"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
v-if="obj.customerAllowNotifyServiceImminent"
|
||||
cols="12"
|
||||
sm="6"
|
||||
lg="9"
|
||||
>
|
||||
<gz-tag-picker
|
||||
ref="customerAllowNotifyServiceImminentInTags"
|
||||
v-model="obj.customerAllowNotifyServiceImminentInTags"
|
||||
:readonly="formState.readOnly"
|
||||
:label="$sock.t('ContactCustomerHeadOfficeTaggedWith')"
|
||||
data-cy="customerAllowNotifyServiceImminentInTags"
|
||||
:error-messages="
|
||||
form().serverErrors(
|
||||
this,
|
||||
'customerAllowNotifyServiceImminentInTags'
|
||||
)
|
||||
"
|
||||
@input="
|
||||
fieldValueChanged(
|
||||
'customerAllowNotifyServiceImminentInTags'
|
||||
)
|
||||
"
|
||||
></gz-tag-picker>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-col>
|
||||
<!-- **************************************************** -->
|
||||
</v-row>
|
||||
</v-form>
|
||||
</div>
|
||||
<v-overlay :value="!formState.ready || formState.loading">
|
||||
<v-progress-circular indeterminate :size="64" />
|
||||
</v-overlay>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
const FORM_KEY = "global-settings-edit";
|
||||
const API_BASE_URL = "global-biz-setting/";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
obj: {
|
||||
id: 0,
|
||||
concurrency: 0,
|
||||
filterCaseSensitive: false,
|
||||
customerAllowUserSettings: false,
|
||||
customerAllowUserSettingsInTags: []
|
||||
},
|
||||
formState: {
|
||||
ready: false,
|
||||
dirty: false,
|
||||
valid: true,
|
||||
readOnly: false,
|
||||
loading: true,
|
||||
errorBoxMessage: null,
|
||||
appError: null,
|
||||
serverError: {}
|
||||
},
|
||||
rights: window.$gz.role.defaultRightsObject(),
|
||||
sockType: window.$gz.type.Project,
|
||||
listgroupitems: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
formState: {
|
||||
handler: function(val) {
|
||||
if (this.formState.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (val.dirty && val.valid && !val.readOnly) {
|
||||
window.$gz.eventBus.$emit("menu-enable-item", FORM_KEY + ":save");
|
||||
} else {
|
||||
window.$gz.eventBus.$emit("menu-disable-item", FORM_KEY + ":save");
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
const vm = this;
|
||||
try {
|
||||
await initForm();
|
||||
vm.rights = window.$gz.role.getRights(window.$gz.type.Global);
|
||||
vm.formState.readOnly = !vm.rights.change;
|
||||
window.$gz.eventBus.$on("menu-click", clickHandler);
|
||||
//NOTE: slightly different in this form as there is only ever a single global object so no need for a bunch of code
|
||||
//is there already an obj from a prior operation?
|
||||
if (this.$route.params.obj) {
|
||||
//yes, no need to fetch it
|
||||
this.obj = this.$route.params.obj;
|
||||
window.$gz.form.setFormState({
|
||||
vm: vm,
|
||||
loading: false
|
||||
});
|
||||
} else {
|
||||
await vm.getDataFromApi();
|
||||
}
|
||||
window.$gz.form.setFormState({
|
||||
vm: vm,
|
||||
dirty: false,
|
||||
valid: true
|
||||
});
|
||||
generateMenu(vm);
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error, vm);
|
||||
} finally {
|
||||
vm.formState.ready = true;
|
||||
}
|
||||
},
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
if (!this.formState.dirty) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
if ((await window.$gz.dialog.confirmLeaveUnsaved()) === true) {
|
||||
next();
|
||||
} else {
|
||||
next(false);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.$gz.eventBus.$off("menu-click", clickHandler);
|
||||
},
|
||||
methods: {
|
||||
canSave: function() {
|
||||
return this.formState.valid && this.formState.dirty;
|
||||
},
|
||||
sockTypes: function() {
|
||||
return window.$gz.type;
|
||||
},
|
||||
form() {
|
||||
return window.$gz.form;
|
||||
},
|
||||
fieldValueChanged(ref) {
|
||||
if (
|
||||
this.formState.ready &&
|
||||
!this.formState.loading &&
|
||||
!this.formState.readOnly
|
||||
) {
|
||||
window.$gz.form.fieldValueChanged(this, ref);
|
||||
}
|
||||
},
|
||||
async getDataFromApi() {
|
||||
const vm = this;
|
||||
window.$gz.form.setFormState({
|
||||
vm: vm,
|
||||
loading: true
|
||||
});
|
||||
try {
|
||||
window.$gz.form.deleteAllErrorBoxErrors(vm);
|
||||
const res = await window.$gz.api.get(API_BASE_URL);
|
||||
if (res.error) {
|
||||
if (res.error.code == "2010") {
|
||||
window.$gz.form.handleObjectNotFound(vm);
|
||||
}
|
||||
vm.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(vm);
|
||||
} else {
|
||||
vm.obj = res.data;
|
||||
generateMenu(vm);
|
||||
window.$gz.form.setFormState({
|
||||
vm: vm,
|
||||
dirty: false,
|
||||
valid: true,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error, vm);
|
||||
} finally {
|
||||
window.$gz.form.setFormState({
|
||||
vm: vm,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
async submit() {
|
||||
const vm = this;
|
||||
if (vm.canSave == false) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.$gz.form.setFormState({
|
||||
vm: vm,
|
||||
loading: true
|
||||
});
|
||||
window.$gz.form.deleteAllErrorBoxErrors(vm);
|
||||
const res = await window.$gz.api.put(API_BASE_URL, vm.obj);
|
||||
if (res.error) {
|
||||
vm.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(vm);
|
||||
} else {
|
||||
//PUT
|
||||
vm.obj.concurrency = res.data.concurrency;
|
||||
window.$gz.form.setFormState({
|
||||
vm: vm,
|
||||
dirty: false,
|
||||
valid: true
|
||||
});
|
||||
//refresh the local global settings cache so user can try their settings right away
|
||||
//get the client version of the global settings object values
|
||||
const gsets = await window.$gz.api.get("global-biz-setting/client");
|
||||
if (!gsets.error) {
|
||||
window.$gz.store.commit("setGlobalSettings", gsets.data);
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
window.$gz.errorHandler.handleFormError(ex, vm);
|
||||
} finally {
|
||||
window.$gz.form.setFormState({
|
||||
vm: vm,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/////////////////////////////
|
||||
//
|
||||
//
|
||||
async function clickHandler(menuItem) {
|
||||
if (!menuItem) {
|
||||
return;
|
||||
}
|
||||
const m = window.$gz.menu.parseMenuItem(menuItem);
|
||||
if (m.owner == FORM_KEY && !m.disabled) {
|
||||
switch (m.key) {
|
||||
case "save":
|
||||
m.vm.submit();
|
||||
break;
|
||||
default:
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-warning",
|
||||
FORM_KEY + "::context click: [" + m.key + "]"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
//
|
||||
//
|
||||
function generateMenu(vm) {
|
||||
const menuOptions = {
|
||||
isMain: false,
|
||||
readOnly: vm.formState.readOnly,
|
||||
icon: "$sockiCogs",
|
||||
title: "AdministrationGlobalSettings",
|
||||
helpUrl: "adm-global-settings",
|
||||
formData: {
|
||||
sockType: window.$gz.type.Project,
|
||||
recordId: vm.$route.params.recordid,
|
||||
recordName: vm.$sock.t("AdministrationGlobalSettings")
|
||||
},
|
||||
menuItems: []
|
||||
};
|
||||
if (vm.rights.change) {
|
||||
menuOptions.menuItems.push({
|
||||
title: "Save",
|
||||
icon: "$sockiSave",
|
||||
surface: true,
|
||||
key: FORM_KEY + ":save",
|
||||
vm: vm
|
||||
});
|
||||
}
|
||||
menuOptions.menuItems.push({ divider: true, inset: false });
|
||||
window.$gz.eventBus.$emit("menu-change", menuOptions);
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
//
|
||||
//
|
||||
async function initForm() {
|
||||
await fetchTranslatedText();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// Ensures UI translated text is available
|
||||
//
|
||||
async function fetchTranslatedText() {
|
||||
await window.$gz.translation.cacheTranslations([
|
||||
"DefaultReport",
|
||||
"ContactCustomerHeadOfficeTaggedWith",
|
||||
"WorkOrderCustomerTaggedWith",
|
||||
"WorkOrderContactCustomerHeadOfficeTaggedWith",
|
||||
"CustomerServiceRequestList",
|
||||
"WorkOrderList",
|
||||
"UserSettings",
|
||||
"UserInterfaceSettings",
|
||||
"BusinessSettings",
|
||||
"CustomerAccessSettings",
|
||||
"PickListTemplates",
|
||||
"GlobalLogo",
|
||||
"GlobalUseInventory",
|
||||
"GlobalFilterCaseSensitive",
|
||||
"GlobalTaxPartPurchaseID",
|
||||
"GlobalTaxPartSaleID",
|
||||
"GlobalTaxRateSaleID",
|
||||
"GlobalNextSeeds",
|
||||
"GlobalWorkOrderCompleteByAge",
|
||||
"GlobalLaborSchedUserDfltTimeSpan",
|
||||
"GlobalTravelDfltTimeSpan",
|
||||
"CustomerAccessWorkOrderWiki",
|
||||
"CustomerAccessWorkOrderAttachments",
|
||||
"NotifySubscriptionList",
|
||||
"NotifyEventCustomerServiceImminent",
|
||||
"NotifyEventCSRAccepted",
|
||||
"NotifyEventCSRRejected",
|
||||
"NotifyEventWorkorderCreatedForCustomer",
|
||||
"NotifyEventWorkorderCompleted",
|
||||
"CustomerAccessWorkOrderReport",
|
||||
"CSRInfoText",
|
||||
"CustomerAllowCreateUnit",
|
||||
"CustomerSignature",
|
||||
"GlobalSignatureFooter",
|
||||
"GlobalSignatureHeader",
|
||||
"GlobalSignatureTitle",
|
||||
"GlobalAllowScheduleConflicts",
|
||||
"CompanyInformation",
|
||||
"CompanyEmail",
|
||||
"CompanyPhone1",
|
||||
"CompanyPhone2",
|
||||
"WebAddress",
|
||||
"AddressTypePhysical",
|
||||
"AddressTypePostal",
|
||||
"Address",
|
||||
"AddressPostalDeliveryAddress",
|
||||
"AddressPostalCity",
|
||||
"AddressPostalStateProv",
|
||||
"AddressPostalCountry",
|
||||
"AddressPostalPostal",
|
||||
"AddressDeliveryAddress",
|
||||
"AddressCity",
|
||||
"AddressStateProv",
|
||||
"AddressCountry",
|
||||
"AddressPostal",
|
||||
"AddressLatitude",
|
||||
"AddressLongitude"
|
||||
]);
|
||||
}
|
||||
</script>
|
||||
68
client/src/views/adm-history.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div>
|
||||
<gz-data-table
|
||||
form-key="adm-history"
|
||||
data-list-key="EventDataList"
|
||||
:show-select="false"
|
||||
:single-select="false"
|
||||
:reload="reload"
|
||||
data-cy="historyTable"
|
||||
>
|
||||
</gz-data-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const FORM_KEY = "adm-history";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
uploadFiles: [],
|
||||
rights: window.$gz.role.defaultRightsObject(),
|
||||
reload: false,
|
||||
uploading: false
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
this.rights = window.$gz.role.getRights(window.$gz.type.Global);
|
||||
window.$gz.eventBus.$on("menu-click", clickHandler);
|
||||
generateMenu(this);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.$gz.eventBus.$off("menu-click", clickHandler);
|
||||
}
|
||||
};
|
||||
/////////////////////////////
|
||||
//
|
||||
//
|
||||
function clickHandler(menuItem) {
|
||||
if (!menuItem) {
|
||||
return;
|
||||
}
|
||||
const m = window.$gz.menu.parseMenuItem(menuItem);
|
||||
if (m.owner == FORM_KEY && !m.disabled) {
|
||||
switch (m.key) {
|
||||
default:
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-warning",
|
||||
FORM_KEY + "::context click: [" + m.key + "]"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
//////////////////////
|
||||
//
|
||||
//
|
||||
function generateMenu() {
|
||||
const menuOptions = {
|
||||
isMain: true,
|
||||
icon: "$sockiHistory",
|
||||
title: "History",
|
||||
helpUrl: "adm-history",
|
||||
hideSearch: true,
|
||||
menuItems: []
|
||||
};
|
||||
|
||||
window.$gz.eventBus.$emit("menu-change", menuOptions);
|
||||
}
|
||||
</script>
|
||||
635
client/src/views/adm-import.vue
Normal file
@@ -0,0 +1,635 @@
|
||||
<template>
|
||||
<div v-if="formState.ready">
|
||||
<gz-error :error-box-message="formState.errorBoxMessage"></gz-error>
|
||||
<div>
|
||||
<v-row dense>
|
||||
<v-col cols="12" sm="6" lg="4" xl="3">
|
||||
<v-select
|
||||
ref="sockType"
|
||||
v-model="sockType"
|
||||
dense
|
||||
:items="selectLists.importableSockTypes"
|
||||
item-text="name"
|
||||
item-value="id"
|
||||
:label="$sock.t('SockType')"
|
||||
data-cy="sockType"
|
||||
></v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="sockType != 0" cols="12" sm="6" lg="4" xl="3">
|
||||
<v-checkbox
|
||||
v-model="doImport"
|
||||
dense
|
||||
:label="$sock.t('ImportNewRecords')"
|
||||
></v-checkbox>
|
||||
<v-checkbox
|
||||
v-if="sockType != 67"
|
||||
v-model="doUpdate"
|
||||
dense
|
||||
:label="$sock.t('UpdateExistingRecords')"
|
||||
color="warning"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
v-if="sockType != 0 && (doImport || doUpdate)"
|
||||
cols="12"
|
||||
sm="6"
|
||||
lg="4"
|
||||
xl="3"
|
||||
>
|
||||
<v-file-input
|
||||
v-model="uploadFile"
|
||||
dense
|
||||
:label="$sock.t('FileToImport')"
|
||||
accept=".json, .csv, application/json, text/csv"
|
||||
prepend-icon="$sockiFileUpload"
|
||||
show-size
|
||||
></v-file-input
|
||||
><v-btn
|
||||
v-if="importable"
|
||||
:loading="uploading"
|
||||
color="primary"
|
||||
text
|
||||
@click="process"
|
||||
>{{ $sock.t("Import") }}</v-btn
|
||||
>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="outputText != null" cols="12">
|
||||
<v-textarea
|
||||
v-model="outputText"
|
||||
dense
|
||||
full-width
|
||||
readonly
|
||||
auto-grow
|
||||
data-cy="outputText"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Papa from "papaparse";
|
||||
const FORM_KEY = "adm-import";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
selectLists: {
|
||||
importableSockTypes: []
|
||||
},
|
||||
uploadFile: [],
|
||||
sockType: 0,
|
||||
doImport: false,
|
||||
doUpdate: false,
|
||||
outputText: null,
|
||||
rights: window.$gz.role.defaultRightsObject(),
|
||||
uploading: false,
|
||||
formState: {
|
||||
ready: false,
|
||||
dirty: false,
|
||||
valid: true,
|
||||
readOnly: false,
|
||||
loading: true,
|
||||
errorBoxMessage: null,
|
||||
appError: null,
|
||||
serverError: {}
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
importable() {
|
||||
return (
|
||||
(this.doImport || this.doUpdate) &&
|
||||
this.uploadFile &&
|
||||
this.uploadFile.name &&
|
||||
this.sockType != 0
|
||||
);
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
//NOTE:Global is what is checked for initialize to show this form and at server to allow import
|
||||
this.rights = window.$gz.role.getRights(window.$gz.type.Global);
|
||||
window.$gz.eventBus.$on("menu-click", clickHandler);
|
||||
await fetchTranslatedText(this);
|
||||
await populateSelectionLists(this);
|
||||
generateMenu(this);
|
||||
this.formState.ready = true;
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.$gz.eventBus.$off("menu-click", clickHandler);
|
||||
},
|
||||
methods: {
|
||||
async process() {
|
||||
if (this.uploading) {
|
||||
return;
|
||||
}
|
||||
if (!this.uploadFile) {
|
||||
return;
|
||||
}
|
||||
this.uploading = true;
|
||||
this.outputText = null;
|
||||
try {
|
||||
let fileName = this.uploadFile.name.toLowerCase();
|
||||
if (!fileName.includes("csv") && !fileName.includes("json")) {
|
||||
window.$gz.store.commit(
|
||||
"logItem",
|
||||
`administration -> import unrecognized import file, name: ${this.uploadFile.name}, type: ${this.uploadFile.type}, size: ${this.uploadFile.size}`
|
||||
);
|
||||
throw new Error("Not supported file type, must be .csv or .json");
|
||||
}
|
||||
const isCSV = fileName.includes("csv");
|
||||
let dat = null;
|
||||
if (isCSV) {
|
||||
let res = await parseCSVFile(this.uploadFile);
|
||||
if (res.errors.length > 0) {
|
||||
this.outputText =
|
||||
"LT:CSV parsing errors:\n" + JSON.stringify(res.errors);
|
||||
throw new Error("LT:Errors in CSV file import can not proceed");
|
||||
}
|
||||
if (res.data) {
|
||||
dat = res.data;
|
||||
}
|
||||
//transform the input csv if it's not a direct match to json (part assembly etc)
|
||||
transform(dat, this.sockType);
|
||||
} else {
|
||||
dat = await parseJSONFile(this.uploadFile);
|
||||
}
|
||||
|
||||
//strip out any unsupported fields before transmission
|
||||
cleanData(dat, this.sockType);
|
||||
|
||||
// console.log(
|
||||
// "CSV FORMAT:\n",
|
||||
// Papa.unparse(dat)
|
||||
// );
|
||||
|
||||
//upload the data
|
||||
await this.upload(dat);
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error);
|
||||
} finally {
|
||||
this.uploading = false;
|
||||
}
|
||||
},
|
||||
async upload(dat) {
|
||||
try {
|
||||
if (this.doUpdate == true) {
|
||||
let dialogResult = await window.$gz.dialog.confirmGeneric(
|
||||
"AdminImportUpdateWarning",
|
||||
"warning"
|
||||
);
|
||||
if (dialogResult == false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await window.$gz.api.post("import", {
|
||||
data: dat,
|
||||
sockType: this.sockType,
|
||||
doImport: this.doImport,
|
||||
doUpdate: this.doUpdate
|
||||
});
|
||||
if (res.error) {
|
||||
window.$gz.errorHandler.handleFormError(res.error);
|
||||
} else {
|
||||
//result is an array of strings
|
||||
let outText = "";
|
||||
res.data.forEach(function appendImportResultItem(value) {
|
||||
outText += value + "\n";
|
||||
});
|
||||
outText += "LT:ProcessCompleted\n";
|
||||
this.outputText = await window.$gz.translation.translateStringWithMultipleKeysAsync(
|
||||
outText
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error);
|
||||
}
|
||||
},
|
||||
handleSelected() {}
|
||||
}
|
||||
};
|
||||
|
||||
/////////////////////////////
|
||||
//
|
||||
//
|
||||
function clickHandler(menuItem) {
|
||||
if (!menuItem) {
|
||||
return;
|
||||
}
|
||||
const m = window.$gz.menu.parseMenuItem(menuItem);
|
||||
if (m.owner == FORM_KEY && !m.disabled) {
|
||||
switch (m.key) {
|
||||
default:
|
||||
window.$gz.eventBus.$emit(
|
||||
"notify-warning",
|
||||
FORM_KEY + "::context click: [" + m.key + "]"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
//
|
||||
//
|
||||
function generateMenu() {
|
||||
const menuOptions = {
|
||||
isMain: true,
|
||||
icon: "$sockiFileImport",
|
||||
title: "Import",
|
||||
helpUrl: "adm-import",
|
||||
menuItems: []
|
||||
};
|
||||
|
||||
window.$gz.eventBus.$emit("menu-change", menuOptions);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// Ensures UI translated text is available
|
||||
//
|
||||
async function fetchTranslatedText() {
|
||||
await window.$gz.translation.cacheTranslations([
|
||||
"SockType",
|
||||
"ImportNewRecords",
|
||||
"UpdateExistingRecords",
|
||||
"AdminImportUpdateWarning",
|
||||
"FileToImport",
|
||||
"ProcessCompleted"
|
||||
]);
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
//
|
||||
//
|
||||
async function populateSelectionLists(vm) {
|
||||
await window.$gz.enums.fetchEnumList("importable");
|
||||
vm.selectLists.importableSockTypes = window.$gz.enums.getSelectionList(
|
||||
"importable"
|
||||
);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// Parse csv and return results as JSON, handle errors if any
|
||||
//
|
||||
async function parseCSVFile(file) {
|
||||
return new Promise(function(complete, error) {
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
// dynamicTyping: true,
|
||||
worker: true,
|
||||
complete,
|
||||
error
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// reformat JSON that was imported for types that need
|
||||
// to be transformed (partassembly etc)
|
||||
//
|
||||
function transform(dat, sockType) {
|
||||
switch (sockType) {
|
||||
case window.$gz.type.PartAssembly:
|
||||
//json from csv needs reformatting
|
||||
dat.forEach(z => {
|
||||
var newItems = [];
|
||||
z.Items.split(",").forEach(x => {
|
||||
let o = x.split("|");
|
||||
newItems.push({
|
||||
PartNameViz: o[0],
|
||||
Quantity: Number.parseFloat(o[1])
|
||||
});
|
||||
});
|
||||
z.Items = newItems;
|
||||
});
|
||||
break;
|
||||
case window.$gz.type.TaskGroup:
|
||||
dat.forEach(z => {
|
||||
var newItems = [];
|
||||
z.Items.split(",").forEach((x, i) => {
|
||||
newItems.push({
|
||||
Sequence: i + 1,
|
||||
Task: x
|
||||
});
|
||||
});
|
||||
z.Items = newItems;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// Open local json file, read, parse and return results as JSON, handle errors if any
|
||||
//
|
||||
async function parseJSONFile(file) {
|
||||
return new Promise(function(complete) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener(
|
||||
"load",
|
||||
() => {
|
||||
// this will then display a text file
|
||||
complete(JSON.parse(reader.result));
|
||||
},
|
||||
false
|
||||
);
|
||||
if (file) {
|
||||
reader.readAsText(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
//
|
||||
// remove unsupported props from data
|
||||
//
|
||||
function cleanData(dat, sockType) {
|
||||
var allowedProps = [];
|
||||
//Note: convention here is any ID field that is linked object we want to support gets renamed here to replace *id with *viz if viz not already present
|
||||
//at back end it will attempt to match up but not create if not existing
|
||||
switch (sockType) {
|
||||
case window.$gz.type.Customer:
|
||||
allowedProps.push(
|
||||
...[
|
||||
"Name",
|
||||
"Active",
|
||||
"Notes",
|
||||
"Wiki",
|
||||
"Tags",
|
||||
"WebAddress",
|
||||
"AlertNotes",
|
||||
"BillHeadOffice",
|
||||
"HeadOfficeViz",
|
||||
"TechNotes",
|
||||
"AccountNumber",
|
||||
"ContractViz",
|
||||
"ContractExpires",
|
||||
"Phone1",
|
||||
"Phone2",
|
||||
"Phone3",
|
||||
"Phone4",
|
||||
"Phone5",
|
||||
"EmailAddress",
|
||||
"PostAddress",
|
||||
"PostCity",
|
||||
"PostRegion",
|
||||
"PostCountry",
|
||||
"PostCode",
|
||||
"Address",
|
||||
"City",
|
||||
"Region",
|
||||
"Country",
|
||||
"AddressPostal",
|
||||
"Latitude",
|
||||
"Longitude"
|
||||
]
|
||||
);
|
||||
break;
|
||||
case window.$gz.type.HeadOffice:
|
||||
allowedProps.push(
|
||||
...[
|
||||
"Name",
|
||||
"Active",
|
||||
"Notes",
|
||||
"Wiki",
|
||||
"Tags",
|
||||
"WebAddress",
|
||||
"TechNotes",
|
||||
"AccountNumber",
|
||||
"ContractViz",
|
||||
"ContractExpires",
|
||||
"Phone1",
|
||||
"Phone2",
|
||||
"Phone3",
|
||||
"Phone4",
|
||||
"Phone5",
|
||||
"EmailAddress",
|
||||
"PostAddress",
|
||||
"PostCity",
|
||||
"PostRegion",
|
||||
"PostCountry",
|
||||
"PostCode",
|
||||
"Address",
|
||||
"City",
|
||||
"Region",
|
||||
"Country",
|
||||
"AddressPostal",
|
||||
"Latitude",
|
||||
"Longitude"
|
||||
]
|
||||
);
|
||||
break;
|
||||
case window.$gz.type.Part:
|
||||
allowedProps.push(
|
||||
...[
|
||||
"Name",
|
||||
"Active",
|
||||
"Description",
|
||||
"Notes",
|
||||
"Wiki",
|
||||
"Tags",
|
||||
"ManufacturerViz",
|
||||
"ManufacturerNumber",
|
||||
"WholeSalerViz",
|
||||
"WholeSalerNumber",
|
||||
"AlternativeWholeSalerViz",
|
||||
"AlternativeWholeSalerNumber",
|
||||
"Cost",
|
||||
"Retail",
|
||||
"UnitOfMeasure",
|
||||
"UPC",
|
||||
"PartSerialsViz"
|
||||
]
|
||||
);
|
||||
break;
|
||||
case window.$gz.type.PartAssembly:
|
||||
allowedProps.push(
|
||||
...[
|
||||
"Name",
|
||||
"Active",
|
||||
"Notes",
|
||||
"Wiki",
|
||||
"Tags",
|
||||
"Items",
|
||||
"PartNameViz",
|
||||
"Quantity"
|
||||
]
|
||||
);
|
||||
break;
|
||||
case window.$gz.type.PartInventory:
|
||||
allowedProps.push(
|
||||
...["Description", "PartViz", "PartWarehouseViz", "Quantity"]
|
||||
);
|
||||
break;
|
||||
|
||||
case window.$gz.type.PartWarehouse:
|
||||
allowedProps.push(...["Name", "Active", "Notes", "Wiki", "Tags"]);
|
||||
break;
|
||||
case window.$gz.type.Project:
|
||||
allowedProps.push(
|
||||
...[
|
||||
"Name",
|
||||
"Active",
|
||||
"Notes",
|
||||
"Wiki",
|
||||
"Tags",
|
||||
"DateStarted",
|
||||
"DateCompleted",
|
||||
"ProjectOverseerViz",
|
||||
"AccountNumber"
|
||||
]
|
||||
);
|
||||
break;
|
||||
case window.$gz.type.ServiceRate:
|
||||
allowedProps.push(
|
||||
...[
|
||||
"Name",
|
||||
"Active",
|
||||
"Notes",
|
||||
"Wiki",
|
||||
"Tags",
|
||||
"AccountNumber",
|
||||
"Cost",
|
||||
"Charge",
|
||||
"Unit",
|
||||
"ContractOnly"
|
||||
]
|
||||
);
|
||||
break;
|
||||
case window.$gz.type.TaskGroup:
|
||||
allowedProps.push(...["Name", "Active", "Notes", "Items"]);
|
||||
break;
|
||||
case window.$gz.type.TravelRate:
|
||||
allowedProps.push(
|
||||
...[
|
||||
"Name",
|
||||
"Active",
|
||||
"Notes",
|
||||
"Wiki",
|
||||
"Tags",
|
||||
"AccountNumber",
|
||||
"Cost",
|
||||
"Charge",
|
||||
"Unit",
|
||||
"ContractOnly"
|
||||
]
|
||||
);
|
||||
break;
|
||||
case window.$gz.type.Unit:
|
||||
allowedProps.push(
|
||||
...[
|
||||
"Serial",
|
||||
"Active",
|
||||
"Notes",
|
||||
"Wiki",
|
||||
"Tags",
|
||||
"CustomerViz",
|
||||
"ParentUnitViz",
|
||||
"UnitModelNameViz",
|
||||
"UnitHasOwnAddress",
|
||||
"BoughtHere",
|
||||
"PurchasedFromVendorViz",
|
||||
"Receipt",
|
||||
"PurchasedDate",
|
||||
"Description",
|
||||
"ReplacedByUnitViz",
|
||||
"OverrideModelWarranty",
|
||||
"WarrantyLength",
|
||||
"WarrantyTerms",
|
||||
"ContractViz",
|
||||
"ContractExpires",
|
||||
"Metered",
|
||||
"LifeTimeWarranty",
|
||||
"Text1",
|
||||
"Text2",
|
||||
"Text3",
|
||||
"Text4",
|
||||
"Address",
|
||||
"City",
|
||||
"Region",
|
||||
"Country",
|
||||
"AddressPostal",
|
||||
"Latitude",
|
||||
"Longitude"
|
||||
]
|
||||
);
|
||||
break;
|
||||
case window.$gz.type.UnitModel:
|
||||
allowedProps.push(
|
||||
...[
|
||||
"Name",
|
||||
"Active",
|
||||
"Notes",
|
||||
"Wiki",
|
||||
"Tags",
|
||||
"VendorViz",
|
||||
"UPC",
|
||||
"LifeTimeWarranty",
|
||||
"IntroducedDate",
|
||||
"Discontinued",
|
||||
"DiscontinuedDate",
|
||||
"WarrantyLength",
|
||||
"WarrantyTerms"
|
||||
]
|
||||
);
|
||||
break;
|
||||
case window.$gz.type.Vendor:
|
||||
allowedProps.push(
|
||||
...[
|
||||
"Name",
|
||||
"Active",
|
||||
"Notes",
|
||||
"Wiki",
|
||||
"Tags",
|
||||
"Contact",
|
||||
"ContactNotes",
|
||||
"AlertNotes",
|
||||
"WebAddress",
|
||||
"AccountNumber",
|
||||
"Phone1",
|
||||
"Phone2",
|
||||
"Phone3",
|
||||
"Phone4",
|
||||
"Phone5",
|
||||
"EmailAddress",
|
||||
"PostAddress",
|
||||
"PostCity",
|
||||
"PostRegion",
|
||||
"PostCountry",
|
||||
"PostCode",
|
||||
"Address",
|
||||
"City",
|
||||
"Region",
|
||||
"Country",
|
||||
"AddressPostal",
|
||||
"Latitude",
|
||||
"Longitude"
|
||||
]
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
//Strip out any records that have fields not on our allowed list
|
||||
dat.forEach(z => {
|
||||
for (const prop in z) {
|
||||
if (allowedProps.includes(prop) == false) {
|
||||
delete z[prop];
|
||||
} else {
|
||||
if (prop == "Tags") {
|
||||
//if it's coming from csv then Tags will be a string with comma separated items like this: blue,white,red
|
||||
//if it's json it will already be an array
|
||||
if (z.Tags && typeof z.Tags === "string") {
|
||||
z.Tags = z.Tags.split(",");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||