initial commit

This commit is contained in:
Torben Indorf
2020-02-02 13:47:47 +01:00
commit 2c2176937c
69 changed files with 13730 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
> 1%
last 2 versions
+6
View File
@@ -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
+10
View File
@@ -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
+10
View File
@@ -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
View File
@@ -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?
+29
View File
@@ -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/).
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}
+11077
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -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"
}
}
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

+19
View File
@@ -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
View File
@@ -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>
+23
View File
@@ -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();
}
},
};
+26
View File
@@ -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;
}
}
+24
View File
@@ -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
});
}
});
}
}
+11
View File
@@ -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);
}
}
+69
View File
@@ -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
});
}
}
});
}
}
+20
View File
@@ -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);
}
}
+34
View File
@@ -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

+23
View File
@@ -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

+101
View File
@@ -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>
+110
View File
@@ -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
View File
@@ -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);
});
+14
View File
@@ -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);
}
+63
View File
@@ -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();
}
}
+68
View File
@@ -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'
}
]
}
];
+10
View File
@@ -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
View File
@@ -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;
}
+33
View File
@@ -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"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"message": "hello i18n !!"
}
+47
View File
@@ -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');
+42
View File
@@ -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;
}
}
+22
View File
@@ -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));
});
}
}
}
}
+22
View File
@@ -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;
+27
View File
@@ -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',
};
+29
View File
@@ -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,
},
},
});
+76
View File
@@ -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')
}
]
}
]
});
+13
View File
@@ -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;
}
}
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
+36
View File
@@ -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);
+70
View File
@@ -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;
+47
View File
@@ -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;
+62
View File
@@ -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;
+6
View File
@@ -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/*
*/
+21
View File
@@ -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)}.";
}
}
+18
View File
@@ -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;
+209
View File
@@ -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;
}
+4
View File
@@ -0,0 +1,4 @@
/**
* This line is necessary to declare all *.png imports as type any
*/
declare module '*.png'
+27
View File
@@ -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;
}
}
+27
View File
@@ -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>
+57
View File
@@ -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>
+14
View File
@@ -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>
+16
View File
@@ -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>
+127
View File
@@ -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&#45;&#45;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>
+129
View File
@@ -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>
+16
View File
@@ -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>
+16
View File
@@ -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>
+43
View File
@@ -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
View File
@@ -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
}
}
+43
View File
@@ -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')
});
});
});
}
};