added input masks to EmployeeDetails, started implementing Employees/Index. Updated to Vue 3.2.21
This commit is contained in:
890
package-lock.json
generated
890
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
|
||||
<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)
|
||||
|
||||
@@ -188,10 +216,6 @@ async function onUpdateEmployee() {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
84
src/views/Employees/Index.vue
Normal file
84
src/views/Employees/Index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user