initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
VUE_APP_TITLE=GelegeApp
|
||||||
|
VUE_APP_BASE_URL=
|
||||||
|
VUE_APP_AUTH_URL= auth/
|
||||||
|
|
||||||
|
VUE_APP_I18N_LOCALE=de
|
||||||
|
VUE_APP_I18N_FALLBACK_LOCALE=en
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
VUE_APP_TITLE=GelegeApp - Dev
|
||||||
|
|
||||||
|
VUE_APP_BASE_URL= http://localhost:1337/
|
||||||
|
VUE_APP_AUTH_URL= auth/
|
||||||
|
|
||||||
|
VUE_APP_I18N_LOCALE=de
|
||||||
|
VUE_APP_I18N_FALLBACK_LOCALE=en
|
||||||
|
|
||||||
|
VUE_APP_DEFAULT_USERNAME=t.indorf@28apps.de
|
||||||
|
VUE_APP_DEFAULT_PASSWORD=test
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
VUE_APP_TITLE=reinigenJETZT[LOCAL]
|
||||||
|
|
||||||
|
VUE_APP_BASE_URL= http://localhost:1337/
|
||||||
|
VUE_APP_AUTH_URL= auth/
|
||||||
|
|
||||||
|
VUE_APP_I18N_LOCALE=de
|
||||||
|
VUE_APP_I18N_FALLBACK_LOCALE=en
|
||||||
|
|
||||||
|
VUE_APP_DEFAULT_USERNAME=t.indorf@28apps.de
|
||||||
|
VUE_APP_DEFAULT_PASSWORD=test
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# 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?
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# ReinigenJetzt (Web Client)
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve / npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run your tests
|
||||||
|
```
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/app'
|
||||||
|
]
|
||||||
|
}
|
||||||
Generated
+11077
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "ReinigenJetzt",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "vue-cli-service serve",
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"serve:local": "vue-cli-service serve --mode localhost",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint",
|
||||||
|
"i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/chart.js": "^2.9.7",
|
||||||
|
"@types/leaflet": "^1.5.7",
|
||||||
|
"@types/vuelidate": "^0.7.9",
|
||||||
|
"axios": "^0.19.0",
|
||||||
|
"chart.js": "^2.9.3",
|
||||||
|
"core-js": "^2.6.11",
|
||||||
|
"leaflet": "^1.6.0",
|
||||||
|
"material-design-icons-iconfont": "^5.0.1",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"simplify-js": "^1.2.3",
|
||||||
|
"vee-validate": "^2.2.5",
|
||||||
|
"vue": "^2.6.11",
|
||||||
|
"vue-chartjs": "^3.5.0",
|
||||||
|
"vue-class-component": "^7.0.2",
|
||||||
|
"vue-i18n": "^8.15.3",
|
||||||
|
"vue-notification": "^1.3.20",
|
||||||
|
"vue-property-decorator": "^8.3.0",
|
||||||
|
"vue-router": "^3.1.3",
|
||||||
|
"vue-the-mask": "^0.11.1",
|
||||||
|
"vue2-leaflet": "^2.4.2",
|
||||||
|
"vuelidate": "^0.7.4",
|
||||||
|
"vuetify": "^2.2.0",
|
||||||
|
"vuex": "^3.1.2",
|
||||||
|
"vuex-class": "^0.3.2",
|
||||||
|
"vuex-persist": "^2.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@mdi/font": "^3.9.97",
|
||||||
|
"@types/webpack": "^4.41.1",
|
||||||
|
"@vue/cli-plugin-babel": "^3.12.1",
|
||||||
|
"@vue/cli-plugin-typescript": "^3.12.1",
|
||||||
|
"@vue/cli-service": "^3.12.1",
|
||||||
|
"sass": "^1.24.0",
|
||||||
|
"sass-loader": "^7.3.1",
|
||||||
|
"stylus": "^0.54.7",
|
||||||
|
"stylus-loader": "^3.0.1",
|
||||||
|
"typescript": "^3.7.4",
|
||||||
|
"vue-cli-plugin-i18n": "^0.6.0",
|
||||||
|
"vue-cli-plugin-vuetify": "^0.5.0",
|
||||||
|
"vue-template-compiler": "^2.6.11",
|
||||||
|
"vuetify-loader": "^1.4.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title><%= VUE_APP_TITLE %></title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= VUE_APP_TITLE %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<v-app id="app">
|
||||||
|
<router-view></router-view>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Watch } from 'vue-property-decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry Vue class. Sets up the application base layout.
|
||||||
|
*/
|
||||||
|
@Component({})
|
||||||
|
export default class App extends Vue {
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import AuthRepository from '@/api/repositories/AuthRepository';
|
||||||
|
import AreaRepository from '@/api/repositories/AreaRepository';
|
||||||
|
import UserRepository from '@/api/repositories/UserRepository';
|
||||||
|
|
||||||
|
|
||||||
|
const repositories: any = {
|
||||||
|
auth: {repository: AuthRepository, instance: null},
|
||||||
|
area: {repository: AreaRepository, instance: null},
|
||||||
|
user: {repository: UserRepository, instance: null}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory to create repositories. All created repositories are singleton instances.
|
||||||
|
*/
|
||||||
|
export const RepositoryFactory = {
|
||||||
|
get: (name: string) => {
|
||||||
|
if (repositories[name].instance) {
|
||||||
|
return repositories[name].instance;
|
||||||
|
} else {
|
||||||
|
return new repositories[name].repository();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Simple error wrapper class to normalize error cases
|
||||||
|
*/
|
||||||
|
export default class ApiError {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* http status code
|
||||||
|
*/
|
||||||
|
public status: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* error message
|
||||||
|
*/
|
||||||
|
public message: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* error data
|
||||||
|
*/
|
||||||
|
public data: any;
|
||||||
|
|
||||||
|
constructor(status: number, message: string, data: any) {
|
||||||
|
this.status = status;
|
||||||
|
this.message = message;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import BaseRepository from '@/api/repositories/BaseRepository';
|
||||||
|
|
||||||
|
export default class AreaRepository extends BaseRepository {
|
||||||
|
|
||||||
|
public loadAllAreas(): Promise<any> {
|
||||||
|
return this.axiosClient(`area`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public loadArea(areaId: string): Promise<any> {
|
||||||
|
return this.axiosClient(`area/${areaId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public createArea(area: any): Promise<any> {
|
||||||
|
return this.axiosClient.post(`area`, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
public editArea(area: any): Promise<any> {
|
||||||
|
return this.axiosClient.put(`area/${area.id}`, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setUsers(area: any, userIds: any): Promise<any> {
|
||||||
|
return this.axiosClient.post(`area/${area.id}/setUsers`, {userIds: userIds});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import ApiError from '@/api/misc/ApiError';
|
||||||
|
import { Vue } from 'vue-property-decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base repository for auth repositories
|
||||||
|
*/
|
||||||
|
export default class AuthBaseRepository {
|
||||||
|
|
||||||
|
protected axiosClient!: AxiosInstance;
|
||||||
|
private baseURL = process.env.VUE_APP_BASE_URL;
|
||||||
|
private authURL = process.env.VUE_APP_AUTH_URL;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.createHttpClient();
|
||||||
|
this.setInterceptors();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createHttpClient() {
|
||||||
|
this.axiosClient = axios.create({
|
||||||
|
baseURL: `${this.baseURL}${this.authURL}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setInterceptors() {
|
||||||
|
// -- normalize response
|
||||||
|
this.axiosClient.interceptors.response.use(
|
||||||
|
(response) => response.data,
|
||||||
|
(error) => {
|
||||||
|
if (error.response) {
|
||||||
|
if (error.response.status === 500) {
|
||||||
|
// OPTIMIZE use i18n
|
||||||
|
Vue.notifyError('General Server Error', 'Something bad happens on our server');
|
||||||
|
return new Promise(() => {
|
||||||
|
// cancels process of the interceptor chain
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new ApiError(error.response.status, error.message, error.response.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// OPTIMIZE use i18n
|
||||||
|
Vue.notifyError('General Network Error', 'Something is wrong with your network');
|
||||||
|
return new Promise(() => {
|
||||||
|
// cancels process of the interceptor chain
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import AuthBaseRepository from '@/api/repositories/AuthBaseRepository';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth repository that provides authentication api methods
|
||||||
|
*/
|
||||||
|
export default class AuthRepository extends AuthBaseRepository {
|
||||||
|
|
||||||
|
public async login(payload: object): Promise<any> {
|
||||||
|
return this.axiosClient.post('loginUser', payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import axios, {AxiosInstance, CancelTokenStatic} from 'axios';
|
||||||
|
import store from './../../store';
|
||||||
|
import ApiError from '@/api/misc/ApiError';
|
||||||
|
import {Vue} from 'vue-property-decorator';
|
||||||
|
import router from '@/router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base repository class
|
||||||
|
*/
|
||||||
|
export default class EntityBaseRepository {
|
||||||
|
|
||||||
|
protected axiosClient!: AxiosInstance;
|
||||||
|
private baseURL = process.env.VUE_APP_BASE_URL;
|
||||||
|
protected cancelTokenFactory: CancelTokenStatic = axios.CancelToken;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.createHttpClient();
|
||||||
|
this.setInterceptors();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createHttpClient() {
|
||||||
|
this.axiosClient = axios.create({
|
||||||
|
baseURL: this.baseURL,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setInterceptors() {
|
||||||
|
// -- set token for each request
|
||||||
|
this.axiosClient.interceptors.request.use((config) => {
|
||||||
|
if (store.getters['auth/token']) {
|
||||||
|
config.headers.token = `Bearer ${store.getters['auth/token']}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}, (error) => {
|
||||||
|
throw Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- normalize response
|
||||||
|
this.axiosClient.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (axios.isCancel(error)) {
|
||||||
|
return new Promise(() => {
|
||||||
|
// cancels process of the interceptor chain
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (error.response) {
|
||||||
|
console.log(error, error.response);
|
||||||
|
// handle invalid (or expired) token ...
|
||||||
|
if (error.response.status === 401) {
|
||||||
|
store.commit('auth/clearLogin');
|
||||||
|
router.push({name: 'login'});
|
||||||
|
} else if (error.response.status === 500) {
|
||||||
|
return new Promise(() => {
|
||||||
|
// cancels process of the interceptor chain
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new ApiError(error.response.status, error.message, error.response.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return new Promise(() => {
|
||||||
|
// cancels processing of the interceptor chain
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import BaseRepository from '@/api/repositories/BaseRepository';
|
||||||
|
|
||||||
|
export default class UserRepository extends BaseRepository {
|
||||||
|
|
||||||
|
public loadAllUsers(): Promise<any> {
|
||||||
|
return this.axiosClient(`user`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public loadUser(userId: string): Promise<any> {
|
||||||
|
return this.axiosClient(`user/${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public createUser(user: any): Promise<any> {
|
||||||
|
return this.axiosClient.post(`user`, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public editUser(user: any): Promise<any> {
|
||||||
|
return this.axiosClient.put(`user/${user.id}`, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="174px" height="202px" viewBox="0 0 174 202" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: sketchtool 58 (101010) - https://sketch.com -->
|
||||||
|
<title>BD0AA448-BE01-4BCC-B00F-AA95D68BC19A</title>
|
||||||
|
<desc>Created with sketchtool.</desc>
|
||||||
|
<defs>
|
||||||
|
<path d="M8,0 L158,0 C162.418278,-8.11624501e-16 166,3.581722 166,8 L166,186 C166,190.418278 162.418278,194 158,194 L8,194 C3.581722,194 5.41083001e-16,190.418278 0,186 L0,8 C-5.41083001e-16,3.581722 3.581722,8.11624501e-16 8,0 Z" id="path-1"></path>
|
||||||
|
<filter x="-4.2%" y="-2.6%" width="108.4%" height="107.2%" filterUnits="objectBoundingBox" id="filter-2">
|
||||||
|
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g id="User-/-Work-sessions" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="No-Work-sessions-" transform="translate(-933.000000, -504.000000)">
|
||||||
|
<g id="Group-4" transform="translate(915.000000, 506.000000)">
|
||||||
|
<g id="Group-3" transform="translate(22.000000, 0.000000)">
|
||||||
|
<g id="Rectangle-Copy">
|
||||||
|
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
|
||||||
|
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
|
||||||
|
</g>
|
||||||
|
<rect id="Rectangle-Copy-8" fill="#DFE1E6" x="16" y="154" width="24" height="24" rx="12"></rect>
|
||||||
|
<rect id="Rectangle-Copy-9" fill="#DFE1E6" x="46" y="154" width="24" height="24" rx="12"></rect>
|
||||||
|
<rect id="Rectangle-Copy-10" fill="#DFE1E6" x="76" y="154" width="24" height="24" rx="12"></rect>
|
||||||
|
<rect id="Rectangle-Copy-4" fill="#DFE1E6" x="16" y="50" width="134" height="24" rx="12"></rect>
|
||||||
|
<rect id="Rectangle-Copy-5" fill="#DFE1E6" x="16" y="90" width="134" height="12" rx="6"></rect>
|
||||||
|
<rect id="Rectangle-Copy-6" fill="#DFE1E6" x="16" y="108" width="134" height="12" rx="6"></rect>
|
||||||
|
<rect id="Rectangle-Copy-7" fill="#DFE1E6" x="16" y="126" width="44" height="12" rx="6"></rect>
|
||||||
|
<rect id="Rectangle-Copy-3" fill="#DFE1E6" x="16" y="24" width="70" height="12" rx="6"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: sketchtool 58 (101010) - https://sketch.com -->
|
||||||
|
<title>E20E0719-69F7-4807-B0EB-A3B8E520CFE4</title>
|
||||||
|
<desc>Created with sketchtool.</desc>
|
||||||
|
<g id="Admin-V2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Admin-/-Firma-dashboard" transform="translate(-16.000000, -18.000000)" fill="#FFFFFF">
|
||||||
|
<g id="Group-8">
|
||||||
|
<g id="Group-[74h]" transform="translate(16.000000, 18.000000)">
|
||||||
|
<g id="REIN_demo_logo">
|
||||||
|
<g id="Group-[26h]">
|
||||||
|
<g id="logo_white_v4">
|
||||||
|
<path d="M14.1487224,3.53718059 C18.0557842,3.53718059 21.2230835,6.70447998 21.2230835,10.6115418 C21.2230835,14.5186036 18.0557842,17.685903 14.1487224,17.685903 C10.2416606,17.685903 7.07436118,14.5186036 7.07436118,10.6115418 C7.07436118,10.4561071 7.07937403,10.3018433 7.08924687,10.1489031 C7.30539746,13.3710794 9.98756002,15.9173127 13.2644272,15.9173127 C16.6831063,15.9173127 19.4544933,13.1459257 19.4544933,9.72724663 C19.4544933,6.45037943 16.90826,3.76821687 13.6863698,3.55133527 L13.9104595,3.54111743 L13.9104595,3.54111743 Z" id="Oval-6" transform="translate(14.148722, 10.611542) rotate(170.000000) translate(-14.148722, -10.611542) "></path>
|
||||||
|
<path d="M15.4751651,1.7685903 C21.0915664,1.7685903 25.6445593,6.32158316 25.6445593,11.9379845 C25.6445593,17.5543858 21.0915664,22.1073787 15.4751651,22.1073787 C9.85876376,22.1073787 5.30577089,17.5543858 5.30577089,11.9379845 C5.30577089,11.7070202 5.31347053,11.4778542 5.32862713,11.2507292 C5.65638204,15.8365026 9.48003871,19.4544933 14.1487224,19.4544933 C19.0325496,19.4544933 22.9916738,15.495369 22.9916738,10.6115418 C22.9916738,5.94285811 19.3736832,2.11920145 14.7885103,1.79138013 C15.0150348,1.77628994 15.2442008,1.7685903 15.4751651,1.7685903 Z"></path>
|
||||||
|
<path d="M13.7065748,3.55271368e-14 C14.0156412,3.55271368e-14 14.3223185,0.010229409 14.6262864,0.030368126 C8.4293416,0.465007926 3.53718059,5.6301835 3.53718059,11.9379845 C3.53718059,18.5311513 8.88199831,23.875969 15.4751651,23.875969 C21.7822899,23.875969 26.9470342,18.9848568 27.3833037,12.788727 C27.4029648,13.0921718 27.4131496,13.3981839 27.4131496,13.7065748 C27.4131496,21.276507 21.276507,27.4131496 13.7065748,27.4131496 C6.13664256,27.4131496 3.55271368e-14,21.276507 3.55271368e-14,13.7065748 C3.55271368e-14,6.13664256 6.13664256,3.55271368e-14 13.7065748,3.55271368e-14 Z" id="Oval-8"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-data-table
|
||||||
|
:headers="tableHeaders" :items="allAreas" class="elevation-1" item-key="id">
|
||||||
|
<template v-slot:item.options="{ item }">
|
||||||
|
<div class="d-flex justify-end ma-3">
|
||||||
|
<v-btn color="primary" small @click="handleEdit(item)" class="mr-1"><v-icon>mdi-pencil</v-icon></v-btn>
|
||||||
|
<v-btn color="primary" small @click="handleAreaUsers(item)"><v-icon>mdi-account-group</v-icon></v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
<create-area-component v-if="showEditAreaDialog" :showDialog="showEditAreaDialog" :selectedArea="selectedArea"
|
||||||
|
@createdArea="createdArea" @closeDialog="showEditAreaDialog = false">
|
||||||
|
</create-area-component>
|
||||||
|
<area-users-component v-if="showUsersDialog" :showDialog="showUsersDialog" :selectedArea="selectedArea"
|
||||||
|
@savedAreaUsers="savedAreaUsers" @closeDialog="showUsersDialog = false">
|
||||||
|
</area-users-component>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Vue} from 'vue-property-decorator';
|
||||||
|
import {Area} from '@/models/Area';
|
||||||
|
import {namespace} from 'vuex-class';
|
||||||
|
|
||||||
|
const AreaStore = namespace('area');
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
CreateAreaComponent: () => import(
|
||||||
|
/* webpackChunkName: "CreateAreaComponent" */
|
||||||
|
'@/components/area/CreateArea.component.vue'
|
||||||
|
),
|
||||||
|
AreaUsersComponent: () => import(
|
||||||
|
/* webpackChunkName: "AreaUsersComponent" */
|
||||||
|
'@/components/area/AreaUsers.component.vue'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class AreaTableComponent extends Vue {
|
||||||
|
|
||||||
|
@AreaStore.Action('loadAreasAction')
|
||||||
|
private loadAreas!: () => Promise<Area[]>;
|
||||||
|
|
||||||
|
@AreaStore.Getter('areas')
|
||||||
|
public allAreas?: Area[];
|
||||||
|
|
||||||
|
public tableHeaders: any[] = [];
|
||||||
|
|
||||||
|
private showUsersDialog: boolean = false;
|
||||||
|
private showEditAreaDialog: boolean = false;
|
||||||
|
|
||||||
|
public selectedArea?: Area;
|
||||||
|
|
||||||
|
public created() {
|
||||||
|
this.tableHeaders = [{
|
||||||
|
text: 'Gebietsname',
|
||||||
|
value: 'name'
|
||||||
|
}, {
|
||||||
|
text: 'Anzahl der Benutzer',
|
||||||
|
value: 'users.length'
|
||||||
|
}, {
|
||||||
|
text: 'Gespeicherte Gelege',
|
||||||
|
value: 'nestCount'
|
||||||
|
}, {
|
||||||
|
text: 'Optionen',
|
||||||
|
value: 'options',
|
||||||
|
align: 'right',
|
||||||
|
width: 200
|
||||||
|
}];
|
||||||
|
|
||||||
|
this.loadAreas();
|
||||||
|
}
|
||||||
|
|
||||||
|
private savedAreaUsers() {
|
||||||
|
this.loadAreas();
|
||||||
|
this.showUsersDialog = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleEdit(selectedArea: Area) {
|
||||||
|
this.selectedArea = selectedArea;
|
||||||
|
this.showEditAreaDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleAreaUsers(selectedArea: Area) {
|
||||||
|
this.selectedArea = selectedArea;
|
||||||
|
this.showUsersDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createdArea() {
|
||||||
|
this.loadAreas();
|
||||||
|
this.showEditAreaDialog = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<v-dialog title="Gebiet erstellen" v-model="showDialog" width="500" @close="closeDialog"
|
||||||
|
@click:outside="closeDialog">
|
||||||
|
<v-card class="pa-6 white">
|
||||||
|
<div class="headline text-center mb-6">{{ selectedArea.name }} - Benutzer verwalten
|
||||||
|
</div>
|
||||||
|
<v-card-text>
|
||||||
|
<v-autocomplete
|
||||||
|
v-model="selectedUsers"
|
||||||
|
:items="allUsers"
|
||||||
|
filled
|
||||||
|
chips
|
||||||
|
color="blue-grey lighten-2"
|
||||||
|
label="Benutzer auswählen"
|
||||||
|
item-text="firstName"
|
||||||
|
item-value="id"
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<template v-slot:selection="data">
|
||||||
|
<v-chip
|
||||||
|
v-bind="data.attrs"
|
||||||
|
:input-value="data.selected"
|
||||||
|
close
|
||||||
|
@click="data.select"
|
||||||
|
@click:close="removeUser(data.item)"
|
||||||
|
>
|
||||||
|
{{ data.item.firstName }} {{ data.item.lastName }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
<template v-slot:item="data">
|
||||||
|
<template>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ data.item.firstName }} {{ data.item.lastName }}
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</v-autocomplete>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn color="error" @click="closeDialog">Abbrechen</v-btn>
|
||||||
|
<v-btn color="primary" @click="saveUsers">Speichern
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||||
|
import User from '@/models/User';
|
||||||
|
import {Area} from '@/models/Area';
|
||||||
|
import {namespace} from 'vuex-class';
|
||||||
|
|
||||||
|
const UserStore = namespace('user');
|
||||||
|
const AreaStore = namespace('area');
|
||||||
|
|
||||||
|
@Component({})
|
||||||
|
export default class AreaUsersComponent extends Vue {
|
||||||
|
|
||||||
|
@Prop({default: false})
|
||||||
|
private showDialog?: boolean;
|
||||||
|
|
||||||
|
@Prop({})
|
||||||
|
private selectedArea?: Area;
|
||||||
|
|
||||||
|
@UserStore.Action('loadUsersAction')
|
||||||
|
private loadUsers!: () => Promise<User[]>;
|
||||||
|
|
||||||
|
@AreaStore.Action('setUsersAction')
|
||||||
|
private setAreaUsers!: (payload: {area: Area, userIds: string[] }) => Promise<Area>;
|
||||||
|
|
||||||
|
@UserStore.Getter('users')
|
||||||
|
public allUsers?: User[];
|
||||||
|
|
||||||
|
public selectedUsers: string[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async created() {
|
||||||
|
await this.loadUsers();
|
||||||
|
|
||||||
|
this.selectedUsers = this.selectedArea!.users.map((user: User) => user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeDialog() {
|
||||||
|
this.$emit('closeDialog');
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeUser(user: User) {
|
||||||
|
this.selectedUsers = this.selectedUsers.filter((id: string) => id !== user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveUsers() {
|
||||||
|
await this.setAreaUsers({
|
||||||
|
area: this.selectedArea!,
|
||||||
|
userIds: this.selectedUsers
|
||||||
|
});
|
||||||
|
this.$emit('savedAreaUsers');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<v-dialog title="Gebiet erstellen" v-model="showDialog" width="500" @close="closeDialog"
|
||||||
|
@click:outside="closeDialog">
|
||||||
|
<v-card class="pa-6 white">
|
||||||
|
<div class="headline text-center mb-6">Gebiet erstellen
|
||||||
|
</div>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form class="d-flex flex-column">
|
||||||
|
<v-row class="d-flex flex-column">
|
||||||
|
<v-text-field hide-details outlined placeholder="Gebietsname" name="name" data-vv-as="Gebietsname"
|
||||||
|
v-validate="'required'"
|
||||||
|
v-model="selectedArea.name"/>
|
||||||
|
<span class="input-error text-center">{{ errors.first('name') }}</span>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-checkbox label="Aktiv" v-model="selectedArea.active"/>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn color="error" @click="closeDialog">Abbrechen</v-btn>
|
||||||
|
<v-btn color="primary" @click="saveArea">
|
||||||
|
{{ selectedArea.id ? 'Gebiet speichern' : 'Gebiet erstellen' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Vue, Prop} from 'vue-property-decorator';
|
||||||
|
import {Area} from '@/models/Area';
|
||||||
|
import {namespace} from 'vuex-class';
|
||||||
|
|
||||||
|
const AreaStore = namespace('area');
|
||||||
|
|
||||||
|
@Component({})
|
||||||
|
export default class CreateAreaComponent extends Vue {
|
||||||
|
|
||||||
|
@Prop({default: false})
|
||||||
|
private showDialog?: boolean;
|
||||||
|
|
||||||
|
@Prop({default: () => new Area(null)})
|
||||||
|
public selectedArea?: Area;
|
||||||
|
|
||||||
|
@AreaStore.Action('createAreaAction')
|
||||||
|
private createAreaAction!: (areaData: any) => Promise<Area[]>;
|
||||||
|
|
||||||
|
@AreaStore.Action('editAreaAction')
|
||||||
|
private editAreaAction!: (areaData: any) => Promise<Area[]>;
|
||||||
|
|
||||||
|
private closeDialog() {
|
||||||
|
this.$emit('closeDialog');
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveArea() {
|
||||||
|
this.$validator.validateAll().then(async (result) => {
|
||||||
|
if (result) {
|
||||||
|
if (this.selectedArea!.id) {
|
||||||
|
await this.editAreaAction(this.selectedArea);
|
||||||
|
} else {
|
||||||
|
await this.createAreaAction(this.selectedArea);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div id="nav">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang='ts'>
|
||||||
|
import {Component, Prop, Vue, Watch} from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
name: 'Navigation',
|
||||||
|
})
|
||||||
|
export default class Navigation extends Vue {
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang='scss'>
|
||||||
|
|
||||||
|
</style>
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
import { Vue } from 'vue-property-decorator';
|
||||||
|
import moment, {min} from 'moment';
|
||||||
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global filters functions are defined here
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an ISO date string to a locale date. The locale date is retrieved from i18n.locale
|
||||||
|
*/
|
||||||
|
Vue.filter('toDate', (value: string) => {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const time = moment(value, moment.ISO_8601, true);
|
||||||
|
if (!time.isValid()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (i18n.locale) {
|
||||||
|
case 'de':
|
||||||
|
return time.format('DD.MM.YYYY');
|
||||||
|
break;
|
||||||
|
case 'en':
|
||||||
|
default:
|
||||||
|
return time.format('YYYY-MM-DD');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an zone respecting date string to HH:mm time string
|
||||||
|
*/
|
||||||
|
Vue.filter('toZoneTime', (value: string) => {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const time = moment(value, moment.ISO_8601, true);
|
||||||
|
if (!time.isValid()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return time.format('HH:mm');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an ISO date string to HH:mm time string
|
||||||
|
*/
|
||||||
|
Vue.filter('toTime', (value: string) => {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const time = moment.utc(value, moment.ISO_8601, true);
|
||||||
|
if (!time.isValid()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return time.format('HH:mm');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a HH:mm string or a timestamp to an ISO date string
|
||||||
|
*/
|
||||||
|
Vue.filter('fromTime', (value: string | number) => {
|
||||||
|
const type = typeof value;
|
||||||
|
switch (type) {
|
||||||
|
case 'number':
|
||||||
|
return moment.utc(value).format();
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
value = value as string;
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const timeRegEx = new RegExp('^\\d\\d:\\d\\d$');
|
||||||
|
if (!timeRegEx.test(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const timeParts = value.split(':');
|
||||||
|
return moment.utc(0).add(timeParts[0], 'h').add(timeParts[1], 'm').format();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a duration in milliseconds to a friendly duration representation HH:ss
|
||||||
|
*/
|
||||||
|
Vue.filter('toDuration', (value: number, verbose: boolean = true) => {
|
||||||
|
const durationMoment = moment.duration(value);
|
||||||
|
if (verbose) {
|
||||||
|
if (value || value === 0) {
|
||||||
|
const hours = durationMoment.get('hours');
|
||||||
|
const minutes = durationMoment.get('minutes');
|
||||||
|
const hoursTranslation = i18n.t('GENERAL.UNITS.ABBRV.HOUR');
|
||||||
|
const minutesTranslation = i18n.t('GENERAL.UNITS.ABBRV.MINUTES');
|
||||||
|
return `${hours} ${hoursTranslation} ${minutes} ${minutesTranslation}`;
|
||||||
|
} else {
|
||||||
|
return `-- ${i18n.t('GENERAL.UNITS.ABBRV.HOUR')} -- ${i18n.t('GENERAL.UNITS.ABBRV.MINUTES')}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (value) {
|
||||||
|
return `${durationMoment.get('hours')}:${durationMoment.get('minutes')}`;
|
||||||
|
} else {
|
||||||
|
return `00:00`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the first letter of the string to uppercase
|
||||||
|
*/
|
||||||
|
Vue.filter('capitalizeFirst', (value: string) => {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
value = value.toString();
|
||||||
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import {helpers} from 'vuelidate/lib/validators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bunch of useful validation functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test function that checks if the given value is in format 1970-01-01T03:30:00Z
|
||||||
|
*/
|
||||||
|
export const isTimeFormat = helpers.regex('isTimeFormat', /^1970-01-01T(\d{2})\:(\d{2})\:(\d{2})Z$/);
|
||||||
|
|
||||||
|
export function isPhoneNumber(phoneNumber: string) {
|
||||||
|
return new RegExp('^[+]*[(]?[0-9]{1,4}[)]?[-\\s\\./0-9]*$').test(phoneNumber);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
import { Validation } from 'vuelidate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class that creates error messages according an error object. If more than one error is contained inside the error object, only the
|
||||||
|
* error with the biggest priority is returned.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
export default class ErrorMessageHandlerMixin extends Vue {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The definition of the error priorities. The smaller the index, the bigger the priority.
|
||||||
|
*/
|
||||||
|
private priorities = [
|
||||||
|
'required',
|
||||||
|
'isTimeFormat',
|
||||||
|
'minLength',
|
||||||
|
'maxLength',
|
||||||
|
'numeric',
|
||||||
|
'decimal',
|
||||||
|
'minValue',
|
||||||
|
'maxValue',
|
||||||
|
'email',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the identification of the needed error message. Returns null if no error is present.
|
||||||
|
* @param validation
|
||||||
|
*/
|
||||||
|
public identifyErrorMessage(validation: Validation): string | null {
|
||||||
|
|
||||||
|
if (validation === undefined) {
|
||||||
|
throw new Error('Validation model property binding is not correct. Ist there a typo?');
|
||||||
|
}
|
||||||
|
if (validation.$error) {
|
||||||
|
for (const errorType of this.priorities) {
|
||||||
|
if (errorType in validation && !(validation as any)[errorType]) {
|
||||||
|
const params = (validation as any).$params[errorType];
|
||||||
|
return this.$t('ERROR_MESSAGES.' + errorType.toUpperCase(), params).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if no error message was provided for this error
|
||||||
|
return this.$t('ERROR_MESSAGES.UNDEFINED').toString();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the validation for the given propertyPath. That means:
|
||||||
|
* properyPath = "customer.address.street" triggers the validation for customer.address.street
|
||||||
|
* @param propertyPath
|
||||||
|
*/
|
||||||
|
public triggerValidation(propertyPath: string) {
|
||||||
|
const parts = propertyPath.split('.');
|
||||||
|
let validationObject = (this.$v as any)[parts.shift()!];
|
||||||
|
while (parts.length > 0) {
|
||||||
|
validationObject = validationObject[parts.shift()!];
|
||||||
|
}
|
||||||
|
validationObject.$touch();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { NavigationItemInterface } from '@/interfaces/NavigationItem.interface';
|
||||||
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
|
export const NavigationItemStorage: NavigationItemInterface[] = [
|
||||||
|
{
|
||||||
|
title: i18n.t('GENERAL.DASHBOARD').toString(),
|
||||||
|
routerName: 'dashboard',
|
||||||
|
dropdownOptions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t('GENERAL.USERS').toString(),
|
||||||
|
routerName: 'userOverview',
|
||||||
|
dropdownOptions: [
|
||||||
|
{
|
||||||
|
title: 'NAVIGATION.DROPDOWN.ALL_USERS',
|
||||||
|
routerName: 'usersOverview',
|
||||||
|
icon: 'mdi-user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NAVIGATION.DROPDOWN.CREATE_USER',
|
||||||
|
routerName: 'createUser',
|
||||||
|
icon: 'mdi-plus-circle-outline'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t('GENERAL.AREAS').toString(),
|
||||||
|
routerName: 'areasOverview',
|
||||||
|
dropdownOptions: [
|
||||||
|
{
|
||||||
|
title: 'NAVIGATION.DROPDOWN.ALL_AREAS',
|
||||||
|
routerName: 'areasOverview',
|
||||||
|
icon: 'mdi-account-multiple'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NAVIGATION.DROPDOWN.CREATE_AREA',
|
||||||
|
routerName: 'createArea',
|
||||||
|
icon: 'mdi-plus-circle-outline'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t('NAVIGATION.DROPDOWN.MISC').toString(),
|
||||||
|
routerName: 'entitiesOverview',
|
||||||
|
dropdownOptions: [
|
||||||
|
{
|
||||||
|
title: 'NAVIGATION.DROPDOWN.ALL_ENTITIES',
|
||||||
|
routerName: 'entityOverview',
|
||||||
|
icon: 'mdi-account-multiple'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NAVIGATION.DROPDOWN.CREATE_ENTITY',
|
||||||
|
routerName: 'createEntity',
|
||||||
|
icon: 'mdi-plus-circle-outline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NAVIGATION.DROPDOWN.ALL_STATUS',
|
||||||
|
routerName: 'statusOverview',
|
||||||
|
icon: 'mdi-account-multiple'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NAVIGATION.DROPDOWN.CREATE_STATUS',
|
||||||
|
routerName: 'createStatus',
|
||||||
|
icon: 'mdi-plus-circle-outline'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import store from './../store';
|
||||||
|
import {Vue} from 'vue-property-decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class contains a bunch of routing helper functions
|
||||||
|
*/
|
||||||
|
export class RoutingHelper {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import VueI18n, { LocaleMessages } from 'vue-i18n';
|
||||||
|
|
||||||
|
Vue.use(VueI18n);
|
||||||
|
|
||||||
|
function loadLocaleMessages(): LocaleMessages {
|
||||||
|
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i);
|
||||||
|
const messages: LocaleMessages = {};
|
||||||
|
locales.keys().forEach((key) => {
|
||||||
|
const matched = key.match(/([A-Za-z0-9-_]+)\./i);
|
||||||
|
if (matched && matched.length > 1) {
|
||||||
|
const locale = matched[1];
|
||||||
|
messages[locale] = locales(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new VueI18n({
|
||||||
|
locale: process.env.VUE_APP_I18N_LOCALE || 'en',
|
||||||
|
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
|
||||||
|
messages: loadLocaleMessages(),
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { NavigationSubItemInterface } from '@/interfaces/NavigationSubItem.interface';
|
||||||
|
|
||||||
|
export interface NavigationItemInterface {
|
||||||
|
title: string;
|
||||||
|
routerName: string;
|
||||||
|
dropdownOptions: NavigationSubItemInterface[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface NavigationSubItemInterface {
|
||||||
|
title: string;
|
||||||
|
routerName: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"GENERAL": {
|
||||||
|
"DASHBOARD": "Dashboard",
|
||||||
|
"USERS": "Benutzer",
|
||||||
|
"AREAS": "Gebiete",
|
||||||
|
"STATUS": "Status",
|
||||||
|
"ENTITIES": "Entitäten"
|
||||||
|
},
|
||||||
|
"LOGIN": {
|
||||||
|
"LOGIN_TITLE": "Login",
|
||||||
|
"LOGIN": "Login",
|
||||||
|
"PASSWORD": "Passwort",
|
||||||
|
"NEXT": "Anmelden",
|
||||||
|
"DISCLAIMER": {
|
||||||
|
"COPYRIGHT": "Copyright",
|
||||||
|
"DATA_PROTECTION": "Datenschutz",
|
||||||
|
"IMPRINT": "Impressum"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"NAVIGATION": {
|
||||||
|
"DROPDOWN": {
|
||||||
|
"MISC": "Weiteres",
|
||||||
|
"ALL_USERS": "Alle Benutzer",
|
||||||
|
"ALL_AREAS": "Alle Gebiete",
|
||||||
|
"ALL_STATUS": "Alle Status",
|
||||||
|
"ALL_ENTITIES": "Alle Entitäten",
|
||||||
|
"CREATE_AREA": "Gebiet erstellen",
|
||||||
|
"CREATE_USER": "Benutzer erstellen",
|
||||||
|
"CREATE_STATUS": "Status erstellen",
|
||||||
|
"CREATE_ENTITY": "Entität erstellen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"message": "hello i18n !!"
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import router from './router';
|
||||||
|
import store from './store';
|
||||||
|
import i18n from './i18n';
|
||||||
|
import CustomNotifications from '@/plugins/custom-notifications/CustomNotifications';
|
||||||
|
import vuetify from '@/plugins/vuetify/vuetify';
|
||||||
|
import './filters';
|
||||||
|
|
||||||
|
import VeeValidate, { Validator } from 'vee-validate';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import de from 'vee-validate/dist/locale/de';
|
||||||
|
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
// import global scss
|
||||||
|
import '@/styles/styles.scss';
|
||||||
|
import moment from 'moment';
|
||||||
|
import 'material-design-icons-iconfont/dist/material-design-icons.css';
|
||||||
|
|
||||||
|
// -- misc
|
||||||
|
// - moment
|
||||||
|
// @ts-ignore
|
||||||
|
// this property is not defined in typings but exists anyway
|
||||||
|
moment.suppressDeprecationWarnings = true;
|
||||||
|
moment.locale('de');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
fieldsBagName: 'v-fields',
|
||||||
|
inject: true,
|
||||||
|
locale: 'de',
|
||||||
|
};
|
||||||
|
|
||||||
|
Validator.localize('de', de);
|
||||||
|
|
||||||
|
Vue.use(VeeValidate, config);
|
||||||
|
Vue.use(CustomNotifications);
|
||||||
|
|
||||||
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
router,
|
||||||
|
store,
|
||||||
|
i18n,
|
||||||
|
vuetify,
|
||||||
|
render: (h) => h(App),
|
||||||
|
}).$mount('#app');
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
export default abstract class Parseable {
|
||||||
|
public static parseFromObject(object: any): Parseable {
|
||||||
|
// static abstract methods are not allowed in ts :(
|
||||||
|
throw new Error('Overwrite me!!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static parseFromArray(array: any[]): Parseable[] {
|
||||||
|
if (!Array.isArray(array)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return array.map((obj) => this.parseFromObject(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract parseToObject(): any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a copy based on the parsedFromObject and parsedToObject functions.
|
||||||
|
*/
|
||||||
|
public copy(): Parseable {
|
||||||
|
// TODO improve this copy mechanism, maybe it is to general…
|
||||||
|
return (this.constructor as any).parseFromObject(this.parseToObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://stackoverflow.com/a/8831937/2164158
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
public hashCode(): number {
|
||||||
|
const str = JSON.stringify(this.parseToObject());
|
||||||
|
let hash = 0;
|
||||||
|
if (str.length === 0) {
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str.charCodeAt(i);
|
||||||
|
// tslint:disable:no-bitwise
|
||||||
|
hash = ((hash << 5) - hash) + char;
|
||||||
|
hash = hash & hash; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import User from '@/models/User';
|
||||||
|
|
||||||
|
export class Area {
|
||||||
|
|
||||||
|
public id?: string;
|
||||||
|
public name?: string;
|
||||||
|
public active: boolean = false;
|
||||||
|
public users: User[] = [];
|
||||||
|
|
||||||
|
constructor(data: any) {
|
||||||
|
Object.assign(this, data);
|
||||||
|
if (data) {
|
||||||
|
this.users = [];
|
||||||
|
|
||||||
|
if (data.users) {
|
||||||
|
data.users.map((userData: any) => {
|
||||||
|
this.users.push(new User(userData));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Presents an user. A user is defined through its role.
|
||||||
|
*/
|
||||||
|
import {Area} from '@/models/Area';
|
||||||
|
|
||||||
|
export default class User {
|
||||||
|
|
||||||
|
public id!: string;
|
||||||
|
public email!: string;
|
||||||
|
public firstName!: string;
|
||||||
|
public lastName!: string;
|
||||||
|
public active: boolean = false;
|
||||||
|
public areas: Area[] = [];
|
||||||
|
|
||||||
|
constructor(data: any) {
|
||||||
|
Object.assign(this, data);
|
||||||
|
|
||||||
|
if (data.areas) {
|
||||||
|
this.areas = data.areas.map((areaData: any) => new Area(areaData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import Notifications from 'vue-notification';
|
||||||
|
import { Vue as _Vue } from 'vue-property-decorator';
|
||||||
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small plugin to abstract and ease the invocation of vue-notification notifications.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Register base vue-notification plugin
|
||||||
|
_Vue.use(Notifications);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default options
|
||||||
|
*/
|
||||||
|
const defaultOptions = {
|
||||||
|
duration: 3000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomNotifications = {
|
||||||
|
install(Vue: any, options: any = {}) {
|
||||||
|
|
||||||
|
// handle options
|
||||||
|
Object.assign(defaultOptions, options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstraction wrapper for success notification
|
||||||
|
* @param title
|
||||||
|
* @param text
|
||||||
|
* @param duration
|
||||||
|
*/
|
||||||
|
const notifySuccess = (title: string, text: string, duration?: number) => {
|
||||||
|
|
||||||
|
// handle custom duration
|
||||||
|
const callOptions = {...defaultOptions};
|
||||||
|
callOptions.duration = duration ? duration : callOptions.duration;
|
||||||
|
|
||||||
|
Vue.notify({
|
||||||
|
group: 'notification',
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
type: 'success',
|
||||||
|
...callOptions,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Vue.prototype.$notifySuccess = notifySuccess;
|
||||||
|
Vue.notifySuccess = notifySuccess;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstraction wrapper for error notification
|
||||||
|
* @param title
|
||||||
|
* @param text
|
||||||
|
* @param duration
|
||||||
|
*/
|
||||||
|
const notifyError = (title: string, text: string, duration?: number) => {
|
||||||
|
|
||||||
|
// handle custom duration
|
||||||
|
const callOptions = {...defaultOptions};
|
||||||
|
callOptions.duration = duration ? duration : callOptions.duration;
|
||||||
|
|
||||||
|
Vue.notify({
|
||||||
|
group: 'notification',
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
type: 'error',
|
||||||
|
...callOptions,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Vue.prototype.$notifyError = notifyError;
|
||||||
|
Vue.notifyError = notifyError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstraction wrapper for info notification
|
||||||
|
* @param title
|
||||||
|
* @param text
|
||||||
|
* @param duration
|
||||||
|
*/
|
||||||
|
const notifyInfo = (title: string, text: string, duration?: number) => {
|
||||||
|
|
||||||
|
// handle custom duration
|
||||||
|
const callOptions = {...defaultOptions};
|
||||||
|
callOptions.duration = duration ? duration : callOptions.duration;
|
||||||
|
|
||||||
|
Vue.notify({
|
||||||
|
group: 'notification',
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
type: 'info',
|
||||||
|
...callOptions,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Vue.prototype.$notifyInfo = notifyInfo;
|
||||||
|
Vue.notifyInfo = notifyInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstraction wrapper for warning notification
|
||||||
|
* @param title
|
||||||
|
* @param text
|
||||||
|
* @param duration
|
||||||
|
*/
|
||||||
|
const notifyWarning = (title: string, text: string, duration?: number) => {
|
||||||
|
|
||||||
|
// handle custom duration
|
||||||
|
const callOptions = {...defaultOptions};
|
||||||
|
callOptions.duration = duration ? duration : callOptions.duration;
|
||||||
|
|
||||||
|
Vue.notify({
|
||||||
|
group: 'notification',
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
type: 'warning',
|
||||||
|
...callOptions,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Vue.prototype.$notifyWarning = notifyWarning;
|
||||||
|
Vue.notifyWarning = notifyWarning;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplified version of an success notification.
|
||||||
|
* @param key
|
||||||
|
* @param duration
|
||||||
|
*/
|
||||||
|
const notifySuccessSimplified = (key: string, duration?: number) =>
|
||||||
|
notifySuccess(
|
||||||
|
i18n.t(`${key}.TITLE`).toString(),
|
||||||
|
i18n.t(`${key}.TEXT`).toString(),
|
||||||
|
duration,
|
||||||
|
);
|
||||||
|
Vue.prototype.$notifySuccessSimplified = notifySuccessSimplified;
|
||||||
|
Vue.notifySuccessSimplified = notifySuccessSimplified;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplified version of an error notification.
|
||||||
|
* @param key
|
||||||
|
* @param duration
|
||||||
|
*/
|
||||||
|
const notifyErrorSimplified = (key: string, duration?: number) =>
|
||||||
|
notifyError(
|
||||||
|
i18n.t(`${key}.TITLE`).toString(),
|
||||||
|
i18n.t(`${key}.TEXT`).toString(),
|
||||||
|
duration,
|
||||||
|
);
|
||||||
|
Vue.prototype.$notifyErrorSimplified = notifyErrorSimplified;
|
||||||
|
Vue.notifyErrorSimplified = notifyErrorSimplified;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplified version of an error notification.
|
||||||
|
* @param key
|
||||||
|
* @param duration
|
||||||
|
*/
|
||||||
|
const notifyInfoSimplified = (key: string, duration?: number) =>
|
||||||
|
notifyInfo(
|
||||||
|
i18n.t(`${key}.TITLE`).toString(),
|
||||||
|
i18n.t(`${key}.TEXT`).toString(),
|
||||||
|
duration,
|
||||||
|
);
|
||||||
|
Vue.prototype.$notifyInfoSimplified = notifyInfoSimplified;
|
||||||
|
Vue.notifyInfoSimplified = notifyInfoSimplified;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplified version of an warning notification.
|
||||||
|
* @param key
|
||||||
|
* @param duration
|
||||||
|
*/
|
||||||
|
const notifyWarningSimplified = (key: string, duration?: number) =>
|
||||||
|
notifyWarning(
|
||||||
|
i18n.t(`${key}.TITLE`).toString(),
|
||||||
|
i18n.t(`${key}.TEXT`).toString(),
|
||||||
|
duration,
|
||||||
|
);
|
||||||
|
Vue.prototype.$notifyWarningSimplified = notifyWarningSimplified;
|
||||||
|
Vue.notifyWarningSimplified = notifyWarningSimplified;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default CustomNotifications;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* These theme colors properties are a mix from material design specs:
|
||||||
|
* https://material.io/design/color/#color-theme-creation
|
||||||
|
* and from vuetify theme management:
|
||||||
|
* https://vuetifyjs.com/en/customization/theme#theme
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
// vuetify theme colors
|
||||||
|
'primary': '#3787f6',
|
||||||
|
'secondary': '#F5F5F5',
|
||||||
|
'accent': '#ffffff', // TODO set accent color
|
||||||
|
'error': '#bf2828',
|
||||||
|
'info': '#3787f6',
|
||||||
|
'success': '#92d048',
|
||||||
|
'warning': '#FFC107',
|
||||||
|
'disabled': '#e0e0e0',
|
||||||
|
// material design specs colors
|
||||||
|
// -- general
|
||||||
|
'background': '#f3f3f3',
|
||||||
|
'surface': '#3c3f4a',
|
||||||
|
// -- font
|
||||||
|
'on-primary': '#ffffff',
|
||||||
|
'on-secondary': '#313131',
|
||||||
|
'on-background': '#3c3f4a',
|
||||||
|
'on-surface': '#ffffff',
|
||||||
|
'on-surface-variant': '#787b82',
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* These theme colors properties are a mix from material design specs:
|
||||||
|
* https://material.io/design/color/#color-theme-creation
|
||||||
|
* and from vuetify theme management:
|
||||||
|
* https://vuetifyjs.com/en/customization/theme#theme
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
// REIN light theme colors
|
||||||
|
'primary': '#1867c0',
|
||||||
|
'secondary': '#172B4D',
|
||||||
|
'accent': '#00A252',
|
||||||
|
'error': '#E02020',
|
||||||
|
'info': '#5CBBF6',
|
||||||
|
'success': '#00A252',
|
||||||
|
'warning': '#FFAE42',
|
||||||
|
'disabled': '#EDEEF0',
|
||||||
|
// material design specs colors
|
||||||
|
// -- general
|
||||||
|
'background': '#ffffff',
|
||||||
|
'background-variant': '#FAFBFC',
|
||||||
|
'surface': '#ffffff',
|
||||||
|
// -- font
|
||||||
|
'on-primary': '#ffffff',
|
||||||
|
'on-secondary': '#ffffff',
|
||||||
|
'on-background': '#6B778C',
|
||||||
|
'on-background-variant': '#172B4D',
|
||||||
|
'on-background-variant-2': '#DFE1E6',
|
||||||
|
'on-surface': '#172B4D',
|
||||||
|
'on-surface-variant': '#172B4D',
|
||||||
|
'on-success': '#ffffff',
|
||||||
|
'on-error': '#ffffff',
|
||||||
|
'on-warning': '#ffffff',
|
||||||
|
'on-info': '#ffffff',
|
||||||
|
// colors for mitarbeiter profile image
|
||||||
|
// ATTENTION: if employee colors are being updated, change the digitSum function in UserInitialsComponent accordingly.
|
||||||
|
'employee_color_1': '#4FC2F8',
|
||||||
|
'employee_color_2': '#FE8B66',
|
||||||
|
'employee_color_3': '#9CCC66',
|
||||||
|
'employee_color_4': '#9E9E9E',
|
||||||
|
'employee_color_5': '#A0887E',
|
||||||
|
'employee_color_6': '#4CD0E2',
|
||||||
|
'employee_color_7': '#4BB6AC',
|
||||||
|
'employee_color_8': '#5F97F6',
|
||||||
|
'employee_color_9': '#9FA8DB',
|
||||||
|
'employee_color_10': '#B39DDB',
|
||||||
|
// mics colors
|
||||||
|
'bar-yellow': '#F7B500',
|
||||||
|
'job-started': '#E9F7F2',
|
||||||
|
'admin-navigation-bar-bg': '#EDEFF5',
|
||||||
|
'signature-background': '#EEEEEE',
|
||||||
|
'map-radius-border' : '#B8C9E3',
|
||||||
|
'map-radius-background' : '#3282FC',
|
||||||
|
'button-success': '#02875A',
|
||||||
|
// dashboard icons
|
||||||
|
'total-customer': '#5CBBF6',
|
||||||
|
'total-employee': '#F7B500',
|
||||||
|
'total-location': '#B39DDB',
|
||||||
|
'total-workTime': '#00D086',
|
||||||
|
// chart
|
||||||
|
'chart-fill-color': 'rgba(0, 208, 134, 0.2)',
|
||||||
|
'chart-stroke-color': '#00D086',
|
||||||
|
|
||||||
|
// job states
|
||||||
|
'comment_color': '#FB8C00',
|
||||||
|
'cancelled_color': '#E02020',
|
||||||
|
'images_color': '#4CAF50',
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import '@mdi/font/css/materialdesignicons.css'; // Ensure you are using css-loader
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Vuetify from 'vuetify/lib';
|
||||||
|
import defaultThemeLight from './themes/defaultLight';
|
||||||
|
import defaultThemeDark from './themes/defaultDark';
|
||||||
|
|
||||||
|
// Translation provided by Vuetify (typescript)
|
||||||
|
import de from 'vuetify/src/locale/de';
|
||||||
|
|
||||||
|
Vue.use(Vuetify);
|
||||||
|
|
||||||
|
export default new Vuetify({
|
||||||
|
icons: {
|
||||||
|
iconfont: 'mdi', // default value
|
||||||
|
},
|
||||||
|
lang: {
|
||||||
|
locales: { de },
|
||||||
|
current: 'de',
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
options: {
|
||||||
|
customProperties: true,
|
||||||
|
},
|
||||||
|
themes: {
|
||||||
|
light: defaultThemeLight,
|
||||||
|
dark: defaultThemeDark,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import Router from 'vue-router';
|
||||||
|
import MainView from '@/views/Main.view.vue';
|
||||||
|
|
||||||
|
Vue.use(Router);
|
||||||
|
|
||||||
|
// Handle NavigationDuplicated stack error:
|
||||||
|
// https://github.com/vuejs/vue-router/issues/2881#issuecomment-520554378
|
||||||
|
const originalPush = Router.prototype.push;
|
||||||
|
// @ts-ignore
|
||||||
|
Router.prototype.push = function push(location, onResolve, onReject) {
|
||||||
|
if (onResolve || onReject) {
|
||||||
|
return originalPush.call(this, location, onResolve, onReject);
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
return originalPush.call(this, location).catch((err) => err);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default new Router({
|
||||||
|
mode: 'history',
|
||||||
|
base: process.env.BASE_URL,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'default',
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "LoginView" */ './views/shared/Login.view.vue'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'login',
|
||||||
|
path: '/login',
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "LoginView" */ './views/shared/Login.view.vue'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/home',
|
||||||
|
component: MainView,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'main',
|
||||||
|
component: () => import('./views/dashboard/Dashboard.view.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
name: 'dashboard',
|
||||||
|
component: () => import('./views/dashboard/Dashboard.view.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'users',
|
||||||
|
name: 'usersOverview',
|
||||||
|
component: () => import('./views/users/Users.view.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'areas',
|
||||||
|
name: 'areasOverview',
|
||||||
|
component: () => import('./views/areas/Areas.view.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'entities',
|
||||||
|
name: 'entitiesOverview',
|
||||||
|
component: () => import('./views/entities/Entites.view.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'status',
|
||||||
|
name: 'statusOverview',
|
||||||
|
component: () => import('./views/status/Status.view.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import Vue, { VNode } from 'vue';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
// tslint:disable no-empty-interface
|
||||||
|
interface Element extends VNode {}
|
||||||
|
// tslint:disable no-empty-interface
|
||||||
|
interface ElementClass extends Vue {}
|
||||||
|
interface IntrinsicElements {
|
||||||
|
[elem: string]: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.vue' {
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import Vuex, { StoreOptions } from 'vuex';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import VuexPersistence from 'vuex-persist';
|
||||||
|
import authStore from './stores/auth.store';
|
||||||
|
import areaStore from '@/stores/area.store';
|
||||||
|
import userStore from '@/stores/user.store';
|
||||||
|
|
||||||
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
// set up persistence storage plugin
|
||||||
|
const vuexPersistence = new VuexPersistence({
|
||||||
|
key: process.env.VUE_APP_TITLE,
|
||||||
|
storage: window.localStorage,
|
||||||
|
});
|
||||||
|
|
||||||
|
// set up store
|
||||||
|
const store: StoreOptions<any> = {
|
||||||
|
state: {},
|
||||||
|
modules: {
|
||||||
|
auth: {
|
||||||
|
namespaced: true,
|
||||||
|
...authStore,
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
namespaced: true,
|
||||||
|
...areaStore
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
namespaced: true,
|
||||||
|
...userStore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [vuexPersistence.plugin],
|
||||||
|
// strict: process.env.NODE_ENV !== 'production',
|
||||||
|
};
|
||||||
|
export default new Vuex.Store<any>(store);
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {ActionTree, Module} from 'vuex';
|
||||||
|
import {Area} from '@/models/Area';
|
||||||
|
import AreaRepository from '@/api/repositories/AreaRepository';
|
||||||
|
import {RepositoryFactory} from '@/api/RepositoryFactory';
|
||||||
|
import User from '@/models/User';
|
||||||
|
import userStore from '@/stores/user.store';
|
||||||
|
|
||||||
|
const areaRepository: AreaRepository = RepositoryFactory.get('area');
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
areas: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions: ActionTree<any, any> = {
|
||||||
|
loadAreasAction: async ({commit}): Promise<Area[]> => {
|
||||||
|
const areasRaw = await areaRepository.loadAllAreas();
|
||||||
|
const areas = areasRaw.data.map((data: any) => new Area(data));
|
||||||
|
commit('storeAreas', areas);
|
||||||
|
return areas;
|
||||||
|
},
|
||||||
|
loadAreaAction: async ({commit}, areaId: string): Promise<Area> => {
|
||||||
|
const areaRaw = await areaRepository.loadArea(areaId);
|
||||||
|
const area = new Area(areaRaw.data);
|
||||||
|
return area;
|
||||||
|
},
|
||||||
|
createAreaAction: async ({commit}, areaData: any): Promise<Area> => {
|
||||||
|
const areaRaw = await areaRepository.createArea(areaData);
|
||||||
|
const area = new Area(areaRaw.data);
|
||||||
|
commit('handleArea', area);
|
||||||
|
return area;
|
||||||
|
},
|
||||||
|
editAreaAction: async ({commit}, areaData: any): Promise<Area> => {
|
||||||
|
const areaRaw = await areaRepository.createArea(areaData);
|
||||||
|
const area = new Area(areaRaw.data);
|
||||||
|
commit('handleArea', area);
|
||||||
|
return area;
|
||||||
|
},
|
||||||
|
setUsersAction: async ({commit}, payload: {area: Area, userIds: string[] }): Promise<Area> => {
|
||||||
|
const areaRaw = await areaRepository.setUsers(payload.area, payload.userIds);
|
||||||
|
const area = new Area(areaRaw.data);
|
||||||
|
commit('handleArea', area);
|
||||||
|
return area;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
storeAreas: (state: any, areas: Area[]) => state.areas = areas,
|
||||||
|
handleArea: (state: any, area: Area) => {
|
||||||
|
const index = state.areas.findIndex((item: Area) => item.id === area.id);
|
||||||
|
// replace or add area object
|
||||||
|
if (index >= 0) {
|
||||||
|
state.areas.splice(index, 1, area);
|
||||||
|
} else {
|
||||||
|
state.areas.push(area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getters = {
|
||||||
|
areas: (state: any) => state.areas,
|
||||||
|
};
|
||||||
|
|
||||||
|
const areaStore: Module<any, any> = {
|
||||||
|
state: store,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
getters,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default areaStore;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { ActionTree, Module } from 'vuex';
|
||||||
|
import { RepositoryFactory } from '@/api/RepositoryFactory';
|
||||||
|
import AuthRepository from '@/api/repositories/AuthRepository';
|
||||||
|
import User from '@/models/User';
|
||||||
|
|
||||||
|
const authRepository: AuthRepository = RepositoryFactory.get('auth');
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
token: undefined,
|
||||||
|
user: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions: ActionTree<any, any> = {
|
||||||
|
loginAction: async ({commit}, payload): Promise<any> => {
|
||||||
|
const userRaw = await authRepository.login(payload);
|
||||||
|
commit('saveLogin', userRaw);
|
||||||
|
return userRaw;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
saveLogin: (state: any, payload: any) => {
|
||||||
|
state.user = new User(payload.user);
|
||||||
|
state.token = payload.token;
|
||||||
|
},
|
||||||
|
clearLogin: (state: any) => {
|
||||||
|
state.user = undefined;
|
||||||
|
state.token = undefined;
|
||||||
|
},
|
||||||
|
saveUser: (state: any, user: any) => {
|
||||||
|
state.user = new User(user);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const getters = {
|
||||||
|
token: (state: any) => state.token,
|
||||||
|
user: (state: any) => state.user,
|
||||||
|
};
|
||||||
|
|
||||||
|
const authStore: Module<any, any> = {
|
||||||
|
state: store,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
getters,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default authStore;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import {ActionTree, Module} from 'vuex';
|
||||||
|
import {RepositoryFactory} from '@/api/RepositoryFactory';
|
||||||
|
import UserRepository from '@/api/repositories/UserRepository';
|
||||||
|
import User from '@/models/User';
|
||||||
|
|
||||||
|
const userRepository: UserRepository = RepositoryFactory.get('user');
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
users: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions: ActionTree<any, any> = {
|
||||||
|
loadUsersAction: async ({commit}): Promise<User[]> => {
|
||||||
|
const userRaw = await userRepository.loadAllUsers();
|
||||||
|
const users = userRaw.data.map((data: any) => new User(data));
|
||||||
|
commit('storeUsers', users);
|
||||||
|
return users;
|
||||||
|
},
|
||||||
|
loadUserAction: async ({commit}, userId: string): Promise<User> => {
|
||||||
|
const userRaw = await userRepository.loadUser(userId);
|
||||||
|
const user = new User(userRaw.data);
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
createUserAction: async ({commit}, userData: any): Promise<User> => {
|
||||||
|
const userRaw = await userRepository.createUser(userData);
|
||||||
|
const user = new User(userRaw.data);
|
||||||
|
commit('handleUser', user);
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
editUserAction: async ({commit}, userData: any): Promise<User> => {
|
||||||
|
const userRaw = await userRepository.createUser(userData);
|
||||||
|
const user = new User(userRaw.data);
|
||||||
|
commit('handleUser', user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
storeUsers: (state: any, users: User[]) => state.users = users,
|
||||||
|
handleUser: (state: any, user: User) => {
|
||||||
|
const index = state.users.findIndex((item: User) => item.id === user.id);
|
||||||
|
// replace or add user object
|
||||||
|
if (index >= 0) {
|
||||||
|
state.users.splice(index, 1, user);
|
||||||
|
} else {
|
||||||
|
state.users.push(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getters = {
|
||||||
|
users: (state: any) => state.users,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userStore: Module<any, any> = {
|
||||||
|
state: store,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
getters,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default userStore;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/*
|
||||||
|
* Attention!
|
||||||
|
* Please consider that all colors that are defined here, will not be respected in theming. Thus on a theme change
|
||||||
|
* these colors stay the same and elements that are using this colors are not affected. For theme respecting colors
|
||||||
|
* add your desired color to src/plugins/vuetify/themes/*
|
||||||
|
*/
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
$breakpoints: (
|
||||||
|
'sm': ( min-width: 767px ),
|
||||||
|
'md': ( min-width: 992px ),
|
||||||
|
'lg': ( min-width: 1200px )
|
||||||
|
) !default;
|
||||||
|
|
||||||
|
@mixin respond-to($breakpoint) {
|
||||||
|
// If the key exists in the map
|
||||||
|
@if map-has-key($breakpoints, $breakpoint) {
|
||||||
|
// Prints a media query based on the value
|
||||||
|
@media #{inspect(map-get($breakpoints, $breakpoint))} {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the key doesn't exist in the map
|
||||||
|
@else {
|
||||||
|
@warn "Unfortunately, no value could be retrieved from `#{$breakpoint}`. "
|
||||||
|
+ "Available breakpoints are: #{map-keys($breakpoints)}.";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// MISC
|
||||||
|
//$top_bar_height: 100px;
|
||||||
|
//$top_bar_spacing: 20px;
|
||||||
|
|
||||||
|
$default-space: 24px; // pa-6 or ma-6
|
||||||
|
$default-space-half: 12px; // pa-3 or ma-3
|
||||||
|
$default-space-third: 8px; // pa-2 or ma-2
|
||||||
|
|
||||||
|
$default-border-radius: 4px;
|
||||||
|
|
||||||
|
// NAVIGATION-BAR
|
||||||
|
$navigation-bar-height: 65px;
|
||||||
|
$navigation-bar-item-width: 135px;
|
||||||
|
|
||||||
|
$admin-navigation-bar-width: 60px;
|
||||||
|
|
||||||
|
|
||||||
|
$max-width-medium-input: 280px;
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
// -- general styles
|
||||||
|
html, body, html > * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus,
|
||||||
|
input:-webkit-autofill:active {
|
||||||
|
-webkit-box-shadow: 0 0 0 30px white inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme--light.v-application {
|
||||||
|
color: var(--v-on-background-base);
|
||||||
|
background-color: var(--v-background-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme--dark.v-application {
|
||||||
|
color: var(--v-on-background-base);
|
||||||
|
background-color: var(--v-background-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: var(--v-on-background-variant-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add new typography classes by respecting vuetify convention (https://vuetifyjs.com/en/styles/typography)
|
||||||
|
.body-3 {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-4 {
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - base styles
|
||||||
|
|
||||||
|
.application-logo-container {
|
||||||
|
max-width: 50px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
&.big {
|
||||||
|
width: 35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-name {
|
||||||
|
font-family: NotoSerif, serif;
|
||||||
|
font-size: 18px;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-container {
|
||||||
|
height: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-hover {
|
||||||
|
tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-headline {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--v-on-background-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// - grids
|
||||||
|
.tile-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
grid-template-rows: 200px;
|
||||||
|
grid-gap: 50px;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-hover:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--v-disabled-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// - toast styles
|
||||||
|
.vue-notification-wrapper {
|
||||||
|
// this padding is necessary to show shadow properly
|
||||||
|
padding: 6px !important;
|
||||||
|
|
||||||
|
.notifications-container {
|
||||||
|
position: relative;
|
||||||
|
border-radius: $default-border-radius;
|
||||||
|
// means elevation 3
|
||||||
|
box-shadow: 0px 3px 3px -2px rgba(0, 0, 0, 0.2),
|
||||||
|
0px 3px 4px 0px rgba(0, 0, 0, 0.14),
|
||||||
|
0px 1px 8px 0px rgba(0, 0, 0, 0.12) !important;
|
||||||
|
|
||||||
|
.border {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-bottom-left-radius: $default-border-radius;
|
||||||
|
border-top-left-radius: $default-border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-badge {
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// - navigation styles
|
||||||
|
.nav-hover {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- customs styles
|
||||||
|
.user-badge {
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- vuetify styles
|
||||||
|
.v-stepper {
|
||||||
|
|
||||||
|
.no-transition .v-stepper__wrapper {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-icon.mdi-loading::before {
|
||||||
|
animation: rotation 1.5s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotation {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(359deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn--tile {
|
||||||
|
border-radius: 3px;
|
||||||
|
min-width: 54px !important;
|
||||||
|
padding: 0 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-data-table {
|
||||||
|
&.elevation-1 {
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
color: var(--v-on-background-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme--light.v-btn:not(.v-btn--flat):not(.v-btn--text):not(.v-btn--outlined) {
|
||||||
|
background-color: var(--v-background);
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-slider__thumb,
|
||||||
|
.v-slider__thumb-label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- animations
|
||||||
|
|
||||||
|
.appearance-animation {
|
||||||
|
animation: appearance-animation .4s ease-out;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
|
||||||
|
@keyframes appearance-animation {
|
||||||
|
from {
|
||||||
|
transform: scale(.3, .3);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -- validation
|
||||||
|
|
||||||
|
.input-error {
|
||||||
|
margin-top: 5px;
|
||||||
|
color: red;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* This line is necessary to declare all *.png imports as type any
|
||||||
|
*/
|
||||||
|
declare module '*.png'
|
||||||
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
declare module 'vue/types/vue' {
|
||||||
|
interface Vue {
|
||||||
|
$notifySuccess: (title: string, text: string, duration?: number) => void;
|
||||||
|
$notifyError: (title: string, text: string, duration?: number) => void;
|
||||||
|
$notifyInfo: (title: string, text: string, duration?: number) => void;
|
||||||
|
$notifyWarning: (title: string, text: string, duration?: number) => void;
|
||||||
|
|
||||||
|
$notifySuccessSimplified: (key: string, duration?: number) => void;
|
||||||
|
$notifyErrorSimplified: (key: string, duration?: number) => void;
|
||||||
|
$notifyInfoSimplified: (key: string, duration?: number) => void;
|
||||||
|
$notifyWarningSimplified: (key: string, duration?: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VueConstructor {
|
||||||
|
notifySuccess: (title: string, text: string, duration?: number) => void;
|
||||||
|
notifyError: (title: string, text: string, duration?: number) => void;
|
||||||
|
notifyInfo: (title: string, text: string, duration?: number) => void;
|
||||||
|
notifyWarning: (title: string, text: string, duration?: number) => void;
|
||||||
|
|
||||||
|
notifySuccessSimplified: (key: string, duration?: number) => void;
|
||||||
|
notifyErrorSimplified: (key: string, duration?: number) => void;
|
||||||
|
notifyInfoSimplified: (key: string, duration?: number) => void;
|
||||||
|
notifyWarningSimplified: (key: string, duration?: number) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div id="main">
|
||||||
|
<Navigation class="navigation-bar"></Navigation>
|
||||||
|
<div id="app-content">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
|
import Navigation from '@/views/shared/Navigation.view.vue';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
name: 'MainView',
|
||||||
|
components: {
|
||||||
|
Navigation
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class MainView extends Vue {
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div id="areas" class="ma-6">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<div class="right-header">
|
||||||
|
<h1>Gebiete</h1>
|
||||||
|
</div>
|
||||||
|
<div class="left-header">
|
||||||
|
<v-btn @click="openCreateDialog()" color="primary"
|
||||||
|
>Gebiet erstellen
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<area-table-component class="mt-3"/>
|
||||||
|
<create-area-component v-if="showCreateDialog" :showDialog="showCreateDialog"
|
||||||
|
@createdArea="createdArea" @closeDialog="showCreateDialog = false"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Vue} from 'vue-property-decorator';
|
||||||
|
import {Area} from '@/models/Area';
|
||||||
|
import {namespace} from 'vuex-class';
|
||||||
|
|
||||||
|
const AreaStore = namespace('area');
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
AreaTableComponent: () => import(
|
||||||
|
/* webpackChunkName: "AreaTableComponent" */
|
||||||
|
'@/components/area/AreaTable.component.vue'),
|
||||||
|
CreateAreaComponent: () => import(
|
||||||
|
/* webpackChunkName: "CreateAreaComponent" */
|
||||||
|
'@/components/area/CreateArea.component.vue'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class AreasView extends Vue {
|
||||||
|
|
||||||
|
@AreaStore.Action('loadAreasAction')
|
||||||
|
private loadAreas!: () => Promise<Area[]>;
|
||||||
|
|
||||||
|
private showCreateDialog: boolean = false;
|
||||||
|
|
||||||
|
private openCreateDialog() {
|
||||||
|
this.showCreateDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createdArea() {
|
||||||
|
this.loadAreas();
|
||||||
|
this.showCreateDialog = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Component({})
|
||||||
|
export default class Dashboard extends Vue {
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Vue} from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Component({})
|
||||||
|
export default class EntitesView extends Vue {
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login d-flex primary base-container">
|
||||||
|
<div
|
||||||
|
class="login-form-container d-flex justify-center_ align-center flex-column"
|
||||||
|
>
|
||||||
|
<!-- spacer -->
|
||||||
|
<div class="mt-12"></div>
|
||||||
|
<div class="my-12 on-primary--text d-flex align-center">
|
||||||
|
<div class="application-logo-container mr-3">
|
||||||
|
<img class="big" src="../../assets/logo.svg" alt="logo" />
|
||||||
|
</div>
|
||||||
|
<span class="application-name">
|
||||||
|
{{ applicationName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="background login-form text-center elevation-3">
|
||||||
|
<div class="inner-form ma-10">
|
||||||
|
<form @submit.prevent="onLogin">
|
||||||
|
<div class="header mb-6">
|
||||||
|
<h4>{{ $t('LOGIN.LOGIN_TITLE') }}</h4>
|
||||||
|
</div>
|
||||||
|
<v-text-field
|
||||||
|
v-model="email"
|
||||||
|
:label="$t('LOGIN.LOGIN')"
|
||||||
|
required
|
||||||
|
outlined
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="password"
|
||||||
|
:label="$t('LOGIN.PASSWORD')"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
outlined
|
||||||
|
></v-text-field>
|
||||||
|
<div class="footer d-flex justify-space-between">
|
||||||
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="accent"
|
||||||
|
:disabled="!password || !email"
|
||||||
|
>
|
||||||
|
{{ $t('LOGIN.NEXT') }}
|
||||||
|
</v-btn>
|
||||||
|
<!-- <span class="primary--text d-flex align-center caption">-->
|
||||||
|
<!-- {{$t('LOGIN.FORGOT_PASSWORD')}}-->
|
||||||
|
<!-- </span>-->
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="disclaimer mt-3">
|
||||||
|
<div class="copyright d-flex justify-center">
|
||||||
|
<span class="white--text caption">{{
|
||||||
|
$t('LOGIN.DISCLAIMER.COPYRIGHT')
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-center">
|
||||||
|
<span class="white--text caption">{{
|
||||||
|
$t('LOGIN.DISCLAIMER.DATA_PROTECTION')
|
||||||
|
}}</span>
|
||||||
|
<span class="white--text caption pl-1 pr-1">-</span>
|
||||||
|
<span class="white--text caption">{{
|
||||||
|
$t('LOGIN.DISCLAIMER.IMPRINT')
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
import { namespace } from 'vuex-class';
|
||||||
|
import User from '../../models/User';
|
||||||
|
|
||||||
|
const AuthStore = namespace('auth');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This view provides a login for each user role.
|
||||||
|
*/
|
||||||
|
@Component({})
|
||||||
|
export default class LoginView extends Vue {
|
||||||
|
public applicationName: string = process.env.VUE_APP_TITLE!;
|
||||||
|
|
||||||
|
@AuthStore.Action('loginAction')
|
||||||
|
private loginAction!: (value: { email: string; password: string }) => any;
|
||||||
|
private email: string | null = process.env.VUE_APP_DEFAULT_USERNAME || null;
|
||||||
|
private password: string | null =
|
||||||
|
process.env.VUE_APP_DEFAULT_PASSWORD || null;
|
||||||
|
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onLogin() {
|
||||||
|
try {
|
||||||
|
const loginData = await this.loginAction({
|
||||||
|
email: this.email!,
|
||||||
|
password: this.password!
|
||||||
|
});
|
||||||
|
this.$router.push({name: 'dashboard'});
|
||||||
|
} catch (e) {
|
||||||
|
if (this.$te(`LOGIN.NOTIFICATIONS.${e.status}.TEXT`)) {
|
||||||
|
this.$notifyError(
|
||||||
|
this.$t(`LOGIN.NOTIFICATIONS.GENERAL_TITLE`).toString(),
|
||||||
|
this.$t(`LOGIN.NOTIFICATIONS.${e.status}.TEXT`).toString()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$notifyErrorSimplified('LOGIN.NOTIFICATIONS.GENERAL_ERROR');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.login {
|
||||||
|
.login-form-container {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-toolbar flat class="primary" height="65px">
|
||||||
|
<div class="toolbar d-flex justify-space-between white--text">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<div class="application-logo-container mt-1">
|
||||||
|
<img
|
||||||
|
src="../../assets/logo.svg"
|
||||||
|
alt="logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="application-name ml-6 mr-12">
|
||||||
|
<template>
|
||||||
|
{{ applicationName }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="navigation-container d-flex">
|
||||||
|
<div
|
||||||
|
class="nav-item nav-hover mx-3"
|
||||||
|
v-for="item in navigationItems"
|
||||||
|
v-bind:key="item.routerName"
|
||||||
|
v-bind:class="{
|
||||||
|
itemSelected: selectedNavItem === item.routerName
|
||||||
|
}"
|
||||||
|
@click="
|
||||||
|
item.dropdownOptions.length === 0 ? onNavItemClick(item) : {}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-menu offset-y>
|
||||||
|
<template v-slot:activator="{ on }">
|
||||||
|
<div
|
||||||
|
class="fill-height d-flex align-center justify-center px-3"
|
||||||
|
v-on="on"
|
||||||
|
>
|
||||||
|
<div>{{ item.title }}</div>
|
||||||
|
<v-icon
|
||||||
|
v-if="item.dropdownOptions.length > 0"
|
||||||
|
class="white--text ml-3"
|
||||||
|
>
|
||||||
|
mdi-chevron-down
|
||||||
|
</v-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<v-list v-if="item.dropdownOptions.length > 0" :dense="true">
|
||||||
|
<div
|
||||||
|
v-for="(dropdownItem, index) in item.dropdownOptions"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<v-list-item @click="onNavItemClick(dropdownItem)">
|
||||||
|
<v-icon class="mr-6 primary--text">
|
||||||
|
{{ dropdownItem.icon }}
|
||||||
|
</v-icon>
|
||||||
|
<v-list-item-title>{{
|
||||||
|
$t(dropdownItem.title)
|
||||||
|
}}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-divider
|
||||||
|
class="mr-3 ml-3"
|
||||||
|
v-if="index < item.dropdownOptions.length - 1"
|
||||||
|
>
|
||||||
|
</v-divider>
|
||||||
|
</div>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center"></div>
|
||||||
|
</div>
|
||||||
|
</v-toolbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Watch } from 'vue-property-decorator';
|
||||||
|
import { NavigationItemInterface } from '@/interfaces/NavigationItem.interface';
|
||||||
|
import { NavigationItemStorage } from '@/helper/NavigationItemStorage';
|
||||||
|
|
||||||
|
@Component({})
|
||||||
|
export default class NavigationView extends Vue {
|
||||||
|
public applicationName: string = process.env.VUE_APP_TITLE!;
|
||||||
|
private navigationItems: NavigationItemInterface[] = [];
|
||||||
|
private selectedNavItem = 'companyDashboard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On click handler that activates a route
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
public onNavItemClick(item: any) {
|
||||||
|
switch (item.routerName) {
|
||||||
|
case 'createCustomer':
|
||||||
|
break;
|
||||||
|
case 'createUser':
|
||||||
|
break;
|
||||||
|
case 'createCompany':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.selectedNavItem = item.routerName;
|
||||||
|
this.$router.push({ name: item.routerName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch the route url for changing the navigation bar
|
||||||
|
*/
|
||||||
|
@Watch('$route', { immediate: true, deep: true })
|
||||||
|
private onUrlChange(routerValue: any) {
|
||||||
|
if (routerValue.name) {
|
||||||
|
this.selectedNavItem = routerValue.name;
|
||||||
|
}
|
||||||
|
this.setNavigationItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the navigation items regarding the current user role
|
||||||
|
*/
|
||||||
|
private setNavigationItems() {
|
||||||
|
this.navigationItems = NavigationItemStorage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.navigation-container {
|
||||||
|
height: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Vue} from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Component({})
|
||||||
|
export default class StatusView extends Vue {
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Vue} from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Component({})
|
||||||
|
export default class UsersView extends Vue {
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "esnext",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"importHelpers": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"suppressImplicitAnyIndexErrors": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"types": [
|
||||||
|
"webpack-env",
|
||||||
|
"webpack",
|
||||||
|
"webpack-env",
|
||||||
|
"vuetify"
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lib": [
|
||||||
|
"esnext",
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"scripthost"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue",
|
||||||
|
"tests/**/*.ts",
|
||||||
|
"tests/**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"defaultSeverity": "warning",
|
||||||
|
"extends": [
|
||||||
|
"tslint:recommended"
|
||||||
|
],
|
||||||
|
"linterOptions": {
|
||||||
|
"exclude": [
|
||||||
|
"node_modules/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"quotemark": [true, "single"],
|
||||||
|
"indent": [true, "spaces", 2],
|
||||||
|
"interface-name": false,
|
||||||
|
"ordered-imports": false,
|
||||||
|
"object-literal-sort-keys": false,
|
||||||
|
"no-consecutive-blank-lines": false,
|
||||||
|
"no-console": false,
|
||||||
|
"max-line-length": [true, 150],
|
||||||
|
"variable-name": {
|
||||||
|
"options": [
|
||||||
|
"ban-keywords",
|
||||||
|
"check-format",
|
||||||
|
"allow-leading-underscore",
|
||||||
|
"allow-pascal-case",
|
||||||
|
"require-const-for-all-caps"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"radix": false,
|
||||||
|
"member-ordering": false,
|
||||||
|
"no-implicit-dependencies": false,
|
||||||
|
"trailing-comma": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* This array must contains variables files only!!!! These declared files are attached to each vue
|
||||||
|
* component and therefore has to be as small as possible. Global styles should be defined in styles.scss and not here.
|
||||||
|
*
|
||||||
|
* The "chainWebpack" property is needed to get the scss work together with sass files
|
||||||
|
* from 3 party libraries like Vuetify.
|
||||||
|
*
|
||||||
|
* For further information take a look at:
|
||||||
|
* https://joshuatz.com/posts/2019/vue-mixing-sass-with-scss-with-vuetify-as-an-example/
|
||||||
|
*/
|
||||||
|
const globalScssConfigFiles = [
|
||||||
|
'~@/styles/_colors.scss',
|
||||||
|
'~@/styles/_responsive.scss',
|
||||||
|
'~@/styles/_variables.scss',
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
pluginOptions: {
|
||||||
|
i18n: {
|
||||||
|
locale: 'de',
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
localeDir: 'locales',
|
||||||
|
enableInSFC: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
loaderOptions: {
|
||||||
|
sass: {
|
||||||
|
data: globalScssConfigFiles.map((src)=>'@import "' + src + '";').join('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
chainWebpack: config => {
|
||||||
|
["vue-modules", "vue", "normal-modules", "normal"].forEach((match) => {
|
||||||
|
config.module.rule('sass').oneOf(match).use('sass-loader')
|
||||||
|
.tap(opt => {
|
||||||
|
return Object.assign(opt, {
|
||||||
|
data: globalScssConfigFiles.map((src)=>'@import "' + src + '"').join('\n')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user