This commit is contained in:
2022-12-16 06:08:11 +00:00
parent 8ef3348dfd
commit 8252872e5a
160 changed files with 81168 additions and 0 deletions

1
client/.eslintignore Normal file
View File

@@ -0,0 +1 @@
src/libs/*.js

24
client/.gitignore vendored Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
npm run serve

947
client/audit.txt Normal file
View 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
View File

@@ -0,0 +1,10 @@
module.exports = {
presets: [
[
"@vue/cli-plugin-babel/preset",
{
useBuiltIns: "entry"
}
]
]
};

19
client/build/binding.sln Normal file
View 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

View 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=&quot;node.exe&quot;;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;&quot;C:\\Users\\cardj\\AppData\\Local\\node-gyp\\Cache\\16.13.1\\x64\\node.lib&quot;</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=&quot;node.exe&quot;;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=&quot;node.exe&quot;;%(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;&quot;C:\\Users\\cardj\\AppData\\Local\\node-gyp\\Cache\\16.13.1\\x64\\node.lib&quot;</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=&quot;node.exe&quot;;%(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>

View 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
View 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
View 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
View File

@@ -0,0 +1 @@
test new svn

30380
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

89
client/package.json Normal file
View 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"
]
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

18
client/public/index.html Normal file
View 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>

View 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
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

419
client/src/App.vue Normal file
View 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>

View 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
View 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();
}

View 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 }
};

View 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
View 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;
}
};

View 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
*/

View File

@@ -0,0 +1,2 @@
import Vue from "vue";
export default new Vue();

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

438
client/src/api/gzmenu.js Normal file
View 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
View 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
};

View 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
View 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]
);
}
};

View 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;
}
};

View 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
View 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;
}
};

View 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;
}
};

View File

@@ -0,0 +1,4 @@
export default {
version: "8.0.28",
copyright: "© 1999-2022, Ground Zero Tech-Works Inc."
};

View 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)
*
*
*/

View 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");
}
};

View 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);
} */

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 96 KiB

1414
client/src/assets/logo.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 96 KiB

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

233
client/src/main.js Normal file
View 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");

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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, {});

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

Some files were not shown because too many files have changed in this diff Show More