initial commit
This commit is contained in:
+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>
|
||||
Reference in New Issue
Block a user