added input masks to EmployeeDetails, started implementing Employees/Index. Updated to Vue 3.2.21

This commit is contained in:
Sockenklaus
2021-11-07 02:27:13 +01:00
parent cbd0db7ec5
commit e018db9e0a
10 changed files with 712 additions and 455 deletions

890
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"build-test": "vite build",
"serve": "vite preview"
},
"dependencies": {
@@ -18,7 +19,8 @@
"pinia": "^2.0.0-rc.13",
"pinia-plugin-persist": "^0.0.92",
"uuid": "^8.3.2",
"vue": "^3.2.20",
"vue": "^3.2.21",
"vue-imask": "^6.2.2",
"vue-router": "^4.0.12",
"vue-tsc": "^0.3.0"
},

View File

@@ -1,8 +1,8 @@
<template lang="pug">
AddEmployeeModal(
:searchData="employees"
:searchFields="['name']"
:searchData="rows"
:searchFields="['first_name', 'last_name']"
:searchRow="addEmployeeRow"
:modalId="modalId"
@emitResult="addEmployee($event)"
@@ -144,7 +144,7 @@ AddEmployeeModal(
const addEmployeeRow : Ref<number> = ref(-1)
const addEmployeeMonth : Ref<number> = ref(-1)
const scheduleData : Ref<ScheduleData> = ref(getScheduleData())
const { employees } = storeToRefs(store)
const { rows } = storeToRefs(store)
const selected : Ref<string[]> = ref([])
const shiftAnchor : Ref<Coordinates | undefined> = ref(undefined)

View File

@@ -16,18 +16,19 @@ async function onLogout() {
<template>
<nav class="nav justify-content-center border-bottom mb-4 pb-2 d-flex">
<router-link class="nav-link" to="/">Home</router-link>
<router-link class="nav-link" :to="{name: 'Home'}">Home</router-link>
<router-link v-if="userStore.isAdmin" :to="{name: 'Employees/Index'}" class="nav-link">Mitarbeiter</router-link>
<div class="ms-auto"></div>
<router-link
v-if="userStore.isLoggedIn"
class="nav-link"
to="/employees/me"
:to="{name: 'Employees/Details', params: {id: 'me'}}"
>
Profile
Profil
</router-link>
<a v-if="userStore.isLoggedIn" href="#" @click="onLogout()" class="nav-link">Logout</a>
<router-link v-else to="Login" class="nav-link">Login</router-link>
<router-link v-else :to="{name: 'Login'}" class="nav-link">Login</router-link>
</nav>
</template>

View File

@@ -34,7 +34,7 @@ function onCancel() {
Mitarbeiter bearbeiten
</button>
<button v-if="isActive" type="button" @click="onSave" class="btn btn-success ms-auto">
<button v-if="isActive" type="submit" @click.prevent="onSave" class="btn btn-success ms-auto">
<i class="bi bi-save"></i>
Mitarbeiter speichern
</button>

View File

@@ -21,10 +21,19 @@ const routes: Array<RouteRecordRaw> = [
requiresAdmin: false
}
},
{
path: '/employees',
name: 'Employees/Index',
component: () => import('@/views/Employees/Index.vue'),
meta: {
requiresAuth: true,
requiresAdmin: true,
}
},
{
path: '/employees/:id',
name: 'EmployeesDetails',
component: () => import('@/views/EmployeesDetails.vue'),
name: 'Employees/Details',
component: () => import('@/views/Employees/Details.vue'),
meta: {
requiresAuth: true,
requiresAdmin: false

View File

@@ -11,7 +11,7 @@ type ResultData = {
phone: string | undefined,
mobile: string | undefined,
email: string | undefined,
contractHours: number | undefined,
contractHours: number,
isActive: boolean,
username: string | undefined,
role: string
@@ -34,7 +34,7 @@ export const useEmployee = defineStore({
phone: '',
mobile: '',
email: '',
contractHours: NaN,
contractHours: 0,
isActive: false,
username: '',
password: '',
@@ -50,7 +50,7 @@ export const useEmployee = defineStore({
phone: '',
mobile: '',
email: '',
contractHours: NaN,
contractHours: 0,
isActive: false,
username: '',
password: '',
@@ -60,15 +60,24 @@ export const useEmployee = defineStore({
},
actions: {
assignTruthyValues(target: Object, source: Object) {
for (const key in source) {
if (source[key]) {
target[key] = source[key]
}
}
},
async fetchFromApi(id: string | string[]) {
this.$reset()
try {
const data : ResultData = await <ResultData>(await axios.get('employees/'+id, {
headers: user.header
})).data
Object.assign(this.clean.employee, data)
Object.assign(this.employee, data)
this.assignTruthyValues(this.employee, data)
this.assignTruthyValues(this.clean.employee, data)
}
catch(err){
if(err instanceof Error) notifications.add('danger', err.message, -1)
@@ -99,8 +108,6 @@ export const useEmployee = defineStore({
}
)
console.log(result)
this.employee.password = ''
this.employee.passwordConfirm = ''

View File

@@ -1,12 +1,68 @@
import { defineStore } from 'pinia'
import { useUser } from '@/stores/user'
import axios from '@/axios'
import emplJSON from '../sample-data/employees.json'
const user = useUser()
export const useEmployees = defineStore('employees', {
state: () => {
return {
/** @type {{id: number, name: string, handle: string, contractHours: number }[]} */
employees: emplJSON
/**
* @type {}[]
*/
rows: Array<Object>(),
meta: {
current_page: NaN,
first_page: NaN,
last_page: NaN,
per_page: NaN,
total: NaN
},
columns: Array<string>()
}
},
actions: {
/**
* @param: {number} limit - QueryString: limit=20 etc.
* @param: {number} page - QueryString: page=1 etc.
* @param: {string} sortBy - QueryString: sort_by=asc(col1),desc(col2),...
* @param: {string} simpleSearch - QueryString: simple_search=query(col1,col2,...)
**/
async fetchFromApi(limit?: number, page?:number, sort_by?: string, simple_search?: string) {
try {
const data : any= (await axios.get(
'/employees',
{
params: {
limit,
page,
sort_by,
simple_search
},
headers: user.header
}
)).data
Object.assign(this.meta, data?.meta)
Object.assign(this.rows, data?.data)
this.columns = this.fetchColumns()
}
catch(err) {
}
},
fetchColumns() : string[] {
if(this.rows[0]){
return Object.keys(this.rows[0])
}
return []
}
}
})

View File

@@ -1,8 +1,8 @@
<template>
<VProfileControls class="mb-5" :isActive="editEmployee || createUser" @save="onUpdateEmployee" @toggleEdit="onToggleEdit" />
<form class="text-start">
<VProfileControls class="mb-5" :isActive="editEmployee || createUser" @save="onUpdateEmployee" @toggleEdit="onToggleEdit" />
<form @keydown.enter="onEnter" class="text-start">
<div class="row mb-5">
<div class="col pe-5">
<h4 class="">Persönliche Informationen</h4>
@@ -10,7 +10,7 @@
<label for="first-name" class="form-label">Vorname:</label>
<input
type="text"
v-model.trim="state.employee.firstName"
v-model.trim="employee.firstName"
id="first-name"
class="form-control"
:class="classIsInvalid(v$.firstName)"
@@ -24,31 +24,49 @@
</div>
<label for="last-name" class="form-label">Nachname:</label>
<input type="text" v-model.trim="state.employee.lastName" id="last-name" class="form-control" :disabled="!editEmployee">
<input type="text" v-model.trim="employee.lastName" id="last-name" class="form-control" :disabled="!editEmployee">
<label for="shorthand" class="form-label">Kürzel:</label>
<input
type="text"
v-model.trim="state.employee.shorthand"
v-model.trim="employee.shorthand"
id="shorthand"
class="form-control"
:class="classIsInvalid(v$.shorthand)"
:disabled="!editEmployee"
>
<div v-for="(error) in v$.shorthand.$errors" class="invalid-feedback" id="shorthandFeedback">
{{ error.$message }}
</div>
</div>
<div class="col ps-5 border-start">
<h4 class="">Kontaktdaten</h4>
<label for="phone" class="form-label">Telefonnummer:</label>
<input type="phone" v-model.trim="state.employee.phone" id="phone" class="form-control" :disabled="!editEmployee">
<MaskInput
type="tel"
id="phone"
v-model="employee.phone"
:mask="'000[00]{ / }0000 [0000]'"
class="form-control"
:disabled="!editEmployee"
placeholder="_____ / ____ ____"
/>
<label for="mobile" class="form-label">Handynummer:</label>
<input type="mobile" v-model.trim="state.employee.mobile" id="mobile" class="form-control" :disabled="!editEmployee">
<MaskInput
type="tel"
v-model="employee.mobile"
:mask="'000[00]{ / }[0000] [0000]'"
id="mobile"
class="form-control"
:disabled="!editEmployee"
placeholder="_____ / ____ ____"
/>
<label for="email" class="form-label">E-Mail-Adresse:</label>
<input
type="email"
v-model.trim="state.employee.email"
v-model.trim="employee.email"
id="email"
class="form-control"
:class="classIsInvalid(v$.email)"
@@ -63,15 +81,21 @@
<div class="row">
<div class="col pe-5">
<h4 class="">Vertragsinformationen:</h4>
<label for="contract-hours" class="form-label">Wochenstunden:</label>
<input
type="number"
v-model.number="state.employee.contractHours"
<label for="contract-hours" min="0" max="40" class="form-label">Wochenstunden:</label>
<MaskInput
v-model:typed="employee.contractHours"
v-model="strContractHours"
:mask="Number"
:signed="false"
@click="$event.target.select()"
id="contract-hours"
class="form-control"
:class="classIsInvalid(v$.contractHours)"
:disabled="!editEmployee"
>
/>
<div v-for="(error) in v$.contractHours.$errors" class="invalid-feedback" id="contractHoursFeedback">
{{error.$message}}
</div>
@@ -93,7 +117,7 @@
<label for="username" class="form-label">Benutzername:</label>
<input
type="text"
v-model.trim="state.employee.username"
v-model.trim="employee.username"
id="username"
class="form-control"
:class="classIsInvalid(v$.username)"
@@ -105,7 +129,7 @@
<label for="password" class="form-label">Neues Passwort:</label>
<input
type="password"
v-model="state.employee.password"
v-model="employee.password"
id="password"
class="form-control"
:class="classIsInvalid(v$.password)"
@@ -117,7 +141,7 @@
<label for="password-repeat" class="form-label">Neues Passwort wiederholen:</label>
<input
type="password"
v-model="state.employee.passwordConfirm"
v-model="employee.passwordConfirm"
id="password-repeat"
class="form-control mb-3"
:class="classIsInvalid(v$.passwordConfirm)"
@@ -130,10 +154,9 @@
</template>
</div>
</div>
<VProfileControls class="mt-5" :isActive="editEmployee || createUser" @save="onUpdateEmployee" @toggleEdit="onToggleEdit" />
</form>
<VProfileControls class="mt-5" :isActive="editEmployee || createUser" @save="onUpdateEmployee" @toggleEdit="onToggleEdit" />
</template>
@@ -143,13 +166,18 @@ import VProfileControls from '@/components/VProfileControls.vue';
import { onMounted, computed, ref, watch } from 'vue'
import { useEmployee } from '@/stores/employee'
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import useVuelidate from '@vuelidate/core'
import { required, email, between, decimal, sameAs, helpers, requiredIf } from '@vuelidate/validators'
import { required, email, between, decimal, sameAs, requiredIf } from '@vuelidate/validators'
import { IMaskComponent as MaskInput } from 'vue-imask'
const route = useRoute()
const state = useEmployee()
const { employee } = storeToRefs(state)
const editEmployee = ref(false)
const strContractHours = ref('')
const rules = computed(() => ({
firstName: {
@@ -172,12 +200,12 @@ const rules = computed(() => ({
requiredIf: requiredIf(() => createUser.value)
},
passwordConfirm: {
sameAs: sameAs(state.employee.password)
sameAs: sameAs(employee.value.password)
}
}))
const v$ = useVuelidate(rules, state.employee)
const v$ = useVuelidate(rules, employee)
const createUser = ref(false)
@@ -186,11 +214,7 @@ async function onUpdateEmployee() {
await state.persist()
onToggleEdit()
}
}
function onEnter() {
onUpdateEmployee
}
}
function onToggleCreateUser() {
createUser.value = !createUser.value
@@ -219,7 +243,7 @@ function classIsInvalid(object : any) : string {
return object.$dirty && object.$invalid ? 'is-invalid' : ''
}
onMounted(async () => {
onMounted(() => {
state.fetchFromApi(route.params.id)
})

View File

@@ -0,0 +1,84 @@
<template>
<table class="table table-hover">
<thead>
<tr>
<th
v-for="col in columns"
@click="onSortBy(col)"
>
{{col}}
<i :style="{visibility: colIsSelected(col) ? 'visible' : 'hidden'}" class="bi" :class="iconSort"></i>
</th>
</tr>
</thead>
<tbody>
<tr v-for="employee in rows" @click="router.push({name: 'Employees/Details', params: {id: employee.id}})">
<td v-for="col in columns">
{{ employee[col] }}
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { onMounted, reactive, watch, computed } from 'vue'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useEmployees } from '@/stores/employees'
const store = useEmployees()
const router = useRouter()
const { rows, meta, columns } = storeToRefs(store)
const sort_by = reactive({
asc: true,
column: 'id'
})
watch(sort_by, () => {
store.fetchFromApi(undefined, undefined, (sort_by.asc ? "asc(" : "desc(")+sort_by.column+")")
})
onMounted(() => {
store.fetchFromApi(undefined, undefined, (sort_by.asc ? "asc(" : "desc(")+sort_by.column+")")
})
function colIsSelected(col : string) {
return col === sort_by.column
}
function onSortBy(col : string) {
if(col !== sort_by.column) sort_by.asc = true
else sort_by.asc = !sort_by.asc
sort_by.column = col
}
const iconSort = computed(() => {
return 'bi-sort-' + (sort_by.asc ? 'down' : 'up')+'-alt'
})
</script>
<style scoped>
table th {
-ms-user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
-khtml-user-select: none;
user-select: none;
cursor: pointer;
}
tr {
cursor: pointer;
}
</style>