Employees-Index: Create new User.

This commit is contained in:
Sockenklaus
2021-11-17 03:35:49 +01:00
parent 8071e8ef91
commit 6f237ff8fa
12 changed files with 408 additions and 347 deletions

View File

@@ -0,0 +1,307 @@
<template>
<EmployeeFormControls
class="mb-5"
:isActive="editEmployee || createUser"
@save="submitForm"
@toggleEdit="toggleEdit"
/>
<form class="text-start" @keyup.enter="submitForm">
<div class="row mb-5">
<div class="col pe-5">
<h4 class="">Persönliche Informationen</h4>
<label for="first-name" class="form-label">Vorname:</label>
<input
type="text"
v-model.trim="employee.firstName"
id="first-name"
class="form-control"
:class="classIsInvalid(v$.firstName)"
:disabled="!editEmployee"
>
<div
v-for="(error) in v$.firstName.$errors"
class="invalid-feedback"
id="firstNameFeedback">
{{error.$message}}
</div>
<label for="last-name" class="form-label">Nachname:</label>
<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="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>
<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>
<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="employee.email"
id="email"
class="form-control"
:class="classIsInvalid(v$.email)"
:disabled="!editEmployee"
>
<div v-for="(error) in v$.email.$errors" class="invalid-feedback" id="emailFeedback">
{{error.$message}}
</div>
</div>
</div>
<div class="row">
<div class="col pe-5">
<h4 class="">Vertragsinformationen:</h4>
<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 || !userStore.isAdmin"
/>
<div v-for="(error) in v$.contractHours.$errors" class="invalid-feedback" id="contractHoursFeedback">
{{error.$message}}
</div>
</div>
<div class="col ps-5 border-start">
<div class="form-check form-switch">
<input
type="checkbox"
role="switch"
class="form-check-input"
id="userEnabledSwitch"
:checked="createUser || userEnabled"
@click="toggleCreateUser"
:disabled="userEnabled"
>
<label for="userEnabledSwitch" class="form-check-label h5">{{ labelUserEnabled }}</label>
</div>
<template v-if="userEnabled || createUser">
<label for="username" class="form-label">Benutzername:</label>
<input
type="text"
v-model.trim="employee.username"
id="username"
class="form-control"
:class="classIsInvalid(v$.username)"
:disabled="!createUser"
>
<div v-for="(error) in v$.username.$errors" class="invalid-feedback" id="usernameFeedback">
{{ error.$message }}
</div>
<label for="password" class="form-label">Neues Passwort:</label>
<input
type="password"
v-model="employee.password"
id="password"
class="form-control"
:class="classIsInvalid(v$.password)"
:disabled="!editEmployee && !createUser"
>
<div v-for="(error) in v$.password.$errors" id="passwordFeedback" class="invalid-feedback">
{{error.$message}}
</div>
<label for="password-repeat" class="form-label">Neues Passwort wiederholen:</label>
<input
type="password"
v-model="employee.passwordConfirm"
id="password-repeat"
class="form-control mb-3"
:class="classIsInvalid(v$.passwordConfirm)"
:disabled="!editEmployee && !createUser"
>
<div v-for="(error) in v$.passwordConfirm.$errors" class="invalid-feedback" id="passwordRepeatFeedback">
{{error.$message}}
</div>
</template>
</div>
</div>
</form>
<EmployeeFormControls
class="mt-5"
:isActive="editEmployee || createUser"
@save="submitForm"
@toggleEdit="toggleEdit"
/>
</template>
<script setup lang="ts">
import { toRefs, ref, computed, onMounted } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import { required, email, between, decimal, sameAs, requiredIf, helpers } from '@vuelidate/validators'
import { storeToRefs } from 'pinia'
import { useUser } from '@/stores/user'
import { useEmployee } from '@/stores/employee'
import EmployeeFormControls from '@/components/Employees/EmployeeFormControls.vue'
import { IMaskComponent as MaskInput } from 'vue-imask'
/**
* Props
*/
const props = defineProps<{
id?: string | string[]
}>()
/**
* Stores
*/
const userStore = useUser()
const employeeStore = useEmployee()
const { employee, userEnabled } = storeToRefs(employeeStore)
/**
* Local Refs
*/
const createUser = ref(false)
const strContractHours = ref('')
const editEmployee = ref(props.id ? false : true)
/**
* Vuelidate Validator
*/
function validateApiErrors(field: string, rule: string) {
const apiErrors = employeeStore.apiValidationErrors
const error = apiErrors.find((element) => {
return element.field === field && element.rule === rule
})
if(error === undefined){
return true
}
else return helpers.withMessage(error.message, () => false)
}
const rules = computed(() => ({
firstName: {
required: required
},
shorthand: {
required: required,
validateApiErrors: validateApiErrors("shorthand", "unique")
},
email: {
email: email
},
contractHours: {
decimal,
betweenValue: between(0, 40)
},
username: {
requiredIf: requiredIf(() => createUser.value),
unique: validateApiErrors('username', 'unique')
},
password: {
requiredIf: requiredIf(() => createUser.value)
},
passwordConfirm: {
sameAs: sameAs(employee.value.password)
}
}))
const v$ = useVuelidate(rules, employee)
/**
* Getters for dynamic classes and labels
*/
function classIsInvalid(validator: any) : string {
return validator.$dirty && validator.$invalid ? 'is-invalid' : ''
}
const labelUserEnabled = computed(() => {
return userEnabled ? 'Benutzerinformationen' : 'Kein Benutzer vorhanden'
})
/**
* Actions
*/
function toggleCreateUser() {
createUser.value = !createUser.value
}
function toggleEdit() {
if(createUser.value) {
editEmployee.value = false
createUser.value = false
}
else editEmployee.value = !editEmployee.value
v$.value.$reset()
employeeStore.reset()
}
async function submitForm(){
if(await v$.value.$validate()) {
if(employee.value.id){
employeeStore.patchEmployee()
// werte apiValidation aus!!!
toggleEdit()
}
else {
employeeStore.postEmployee()
// werte apiValidation aus!!!
}
}
}
/**
* Initialize the Component
*/
onMounted(() => {
if(props.id !== undefined) {
employeeStore.fetchFromApi(props.id)
}
else {
employeeStore.$reset()
}
})
</script>
<script lang="scss">
</script>

View File

@@ -3,7 +3,7 @@
AddEmployeeModal(
:searchData="store.state.rows"
:searchFields="['first_name', 'last_name']"
:searchFields="['firstName', 'lastName']"
:searchRow="addEmployeeRow"
:modalId="modalId"
@emitResult="addEmployee($event)"

View File

@@ -2,6 +2,7 @@
<nav class="nav justify-content-center border-bottom mb-4 pb-2 d-flex">
<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>
<router-link v-if="userStore.isAdmin" :to="{name: 'Employees/New'}" class="nav-link">Neuer Mitarbeiter</router-link>
<div class="ms-auto"></div>
<router-link
v-if="userStore.isLoggedIn"

View File

@@ -33,7 +33,7 @@ const routes: Array<RouteRecordRaw> = [
{
path: '/employees/new',
name: 'Employees/New',
component: () => import('@/views/Employees/Details.vue'),
component: () => import('@/views/Employees/New.vue'),
meta: {
requiresAuth: true,
requiresAdmin: true

View File

@@ -2,6 +2,7 @@ import { defineStore, acceptHMRUpdate } from 'pinia'
import axios from '@/axios'
import { useUser } from './user'
import { useNotifications } from './notifications'
import Axios from 'axios'
const user = useUser()
const notifications = useNotifications()
@@ -12,7 +13,7 @@ export const useEmployee = defineStore({
state: () => {
return {
clean: {
/** @type Employee */
/** @type { Employee } */
employee: {
id: NaN,
firstName: '',
@@ -25,7 +26,8 @@ export const useEmployee = defineStore({
isActive: false,
username: '',
password: '',
passwordConfirm: ''
passwordConfirm: '',
role: ''
},
},
@@ -42,8 +44,12 @@ export const useEmployee = defineStore({
isActive: false,
username: '',
password: '',
passwordConfirm: ''
passwordConfirm: '',
role: ''
},
/** @type { EmployeeApiValidationError[] } */
apiValidationErrors: Array<EmployeeApiValidationError>(),
}
},
@@ -77,8 +83,17 @@ export const useEmployee = defineStore({
Object.assign(this.employee, this.clean.employee)
},
/** TODO: #23 Persist user if password is changed */
async persist() {
if(this.employee.id){
this.patchEmployee()
}
else {
this.postEmployee()
}
},
async patchEmployee() {
try {
let result
let payload = Object.assign({}, this.employee)
@@ -109,7 +124,35 @@ export const useEmployee = defineStore({
}
else console.log(error)
}
}
},
async postEmployee() {
let result
try {
result = await axios.post<Employee>(
'employees',
this.employee,
{
headers: user.header
}
)
this.assignTruthyValues(this.employee, result.data)
this.assignTruthyValues(this.clean.employee, result.data)
notifications.add('success', result.statusText)
console.log(result)
}
catch(error){
console.log("enter catch")
if(Axios.isAxiosError(error)) {
console.log("enter axios error")
let data = error.response?.data as { errors: EmployeeApiValidationError[] }
console.log(data)
this.apiValidationErrors = [...data.errors]
}
}
},
},
getters: {

View File

@@ -9,7 +9,7 @@ export default defineStore('employees', () => {
const user = useUser()
const state = reactive({
rows: Array<any>(),
rows: Array<Employee>(),
columns: Array<string>(),
meta : {
@@ -91,89 +91,4 @@ export default defineStore('employees', () => {
setPage
}
})
/*
export const useEmployees = defineStore('employees', {
state: () => {
return {
/**
* @type any[]
rows: Array<any>(),
meta: {
current_page: NaN,
first_page: NaN,
last_page: NaN,
per_page: NaN,
total: NaN
},
columns: Array<string>(),
limit: 10,
page: 1,
sort_by: '',
simple_search: ''
}
},
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() {
try {
const data : any = (await axios.get(
'/employees',
{
params: {
limit: this.limit,
page: this.page,
sort_by: this.sort_by,
simple_search: this.simple_search
},
headers: user.header
}
)).data
Object.assign(this.meta, data.meta)
this.rows = _cloneDeep(data.data)
this.columns = this.fetchColumns()
}
catch(err) {
console.log(err)
}
},
fetchColumns() : string[] {
if(this.rows[0]){
return Object.keys(this.rows[0])
}
return []
},
setLimit(limit : number) {
this.limit = limit
this.fetchFromApi()
},
setSortBy(sortBy : string) {
this.sort_by = sortBy
this.fetchFromApi()
},
setSimpleSearch(simpleSearch : string) {
this.simple_search = simpleSearch
this.fetchFromApi()
},
setPage(page : number) {
this.page = page
this.fetchFromApi()
}
}
}) */
})

23
src/types/employees.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
type Employee = {
id: number,
firstName: string,
lastName: string,
email: string,
phone: string,
mobile: string,
role: string,
shorthand: string,
contractHours?: number,
username?: string,
password?: string,
passwordConfirm?: string,
isActive: boolean,
createdAt?: Date,
updatedAt?: Date
}
type EmployeeApiValidationError = {
rule: string,
field: keyof Employee,
message: string
}

View File

@@ -2,19 +2,6 @@ type ScheduleData = [
...ScheduleMonth[]
]
type Employee = {
id: number,
first_name: string,
last_name: string,
email: string,
phone: string,
mobile: string,
role: string,
shorthand: string,
contractHours?: number
username?: string
}
type ScheduleRow = {
dates : (Date | null)[],
employees : Employee[]

View File

@@ -1,256 +1,28 @@
<template>
<form class="text-start">
<VProfileControls class="mb-5" :isActive="editEmployee || createUser" @save="onUpdateEmployee" @toggleEdit="onToggleEdit" />
<div>
<EmployeeForm
:id="route.params.id"
<div class="row mb-5">
<div class="col pe-5">
<h4 class="">Persönliche Informationen</h4>
<label for="first-name" class="form-label">Vorname:</label>
<input
type="text"
v-model.trim="employee.firstName"
id="first-name"
class="form-control"
:class="classIsInvalid(v$.firstName)"
:disabled="!editEmployee"
>
<div
v-for="(error) in v$.firstName.$errors"
class="invalid-feedback"
id="firstNameFeedback">
{{error.$message}}
</div>
<label for="last-name" class="form-label">Nachname:</label>
<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="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>
<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>
<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="employee.email"
id="email"
class="form-control"
:class="classIsInvalid(v$.email)"
:disabled="!editEmployee"
>
<div v-for="(error) in v$.email.$errors" class="invalid-feedback" id="emailFeedback">
{{error.$message}}
</div>
</div>
</div>
<div class="row">
<div class="col pe-5">
<h4 class="">Vertragsinformationen:</h4>
<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 || !user.isAdmin"
/>
<div v-for="(error) in v$.contractHours.$errors" class="invalid-feedback" id="contractHoursFeedback">
{{error.$message}}
</div>
</div>
<div class="col ps-5 border-start">
<div class="form-check form-switch">
<input
type="checkbox"
role="switch"
class="form-check-input"
id="userEnabledSwitch"
:checked="createUser || state.userEnabled"
@click="onToggleCreateUser"
:disabled="state.userEnabled"
>
<label for="userEnabledSwitch" class="form-check-label h5">{{ labelUserEnabled }}</label>
</div>
<template v-if="state.userEnabled || createUser">
<label for="username" class="form-label">Benutzername:</label>
<input
type="text"
v-model.trim="employee.username"
id="username"
class="form-control"
:class="classIsInvalid(v$.username)"
:disabled="!createUser"
>
<div v-for="(error) in v$.username.$errors" class="invalid-feedback" id="usernameFeedback">
{{ error.$message }}
</div>
<label for="password" class="form-label">Neues Passwort:</label>
<input
type="password"
v-model="employee.password"
id="password"
class="form-control"
:class="classIsInvalid(v$.password)"
:disabled="!editEmployee && !createUser"
>
<div v-for="(error) in v$.password.$errors" id="passwordFeedback" class="invalid-feedback">
{{error.$message}}
</div>
<label for="password-repeat" class="form-label">Neues Passwort wiederholen:</label>
<input
type="password"
v-model="employee.passwordConfirm"
id="password-repeat"
class="form-control mb-3"
:class="classIsInvalid(v$.passwordConfirm)"
:disabled="!editEmployee && !createUser"
>
<div v-for="(error) in v$.passwordConfirm.$errors" class="invalid-feedback" id="passwordRepeatFeedback">
{{error.$message}}
</div>
</template>
</div>
</div>
<VProfileControls class="mt-5" :isActive="editEmployee || createUser" @save="onUpdateEmployee" @toggleEdit="onToggleEdit" />
</form>
/>
</div>
</template>
<script setup lang="ts">
import VProfileControls from '@/components/VProfileControls.vue';
import { onMounted, computed, ref, watch } from 'vue'
import { useEmployee } from '@/stores/employee'
import { useUser } from '@/stores/user'
import EmployeeForm from '@/components/Employees/EmployeeForm.vue'
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import useVuelidate from '@vuelidate/core'
import { required, email, between, decimal, sameAs, requiredIf } from '@vuelidate/validators'
import { IMaskComponent as MaskInput } from 'vue-imask'
const route = useRoute()
/*
const state = useEmployee()
const user = useUser()
const { employee } = storeToRefs(state)
const editEmployee = ref(false)
const strContractHours = ref('')
const rules = computed(() => ({
firstName: {
required: required
},
shorthand: {
required: required
},
email: {
email: email
},
contractHours: {
decimal,
betweenValue: between(0, 40)
},
username: {
requiredIf: requiredIf(() => createUser.value)
},
password: {
requiredIf: requiredIf(() => createUser.value)
},
passwordConfirm: {
sameAs: sameAs(employee.value.password)
}
}))
const v$ = useVuelidate(rules, employee)
const createUser = ref(false)
async function onUpdateEmployee() {
if(await v$.value.$validate()){
await state.persist()
onToggleEdit()
}
}
function onToggleCreateUser() {
createUser.value = !createUser.value
}
function onToggleEdit() {
if(createUser.value) {
editEmployee.value = false
createUser.value = false
}
else editEmployee.value = !editEmployee.value
v$.value.$reset()
state.reset()
}
watch(() => [route.params, route.name], ([newParam, newName], [oldParam, oldName]) => {
if(newName === oldName && newParam?.toString() !== oldParam?.toString()) {
state.fetchFromApi(route.params.id)
createUser.value = false
}
})
function classIsInvalid(object : any) : string {
return object.$dirty && object.$invalid ? 'is-invalid' : ''
}
onMounted(() => {
state.fetchFromApi(route.params.id)
})
const labelUserEnabled = computed(() => {
return state.userEnabled ? 'Benutzerinformationen' : 'Kein Benutzer vorhanden'
})
}) */
</script>

View File

@@ -0,0 +1,17 @@
<template>
<div>
<EmployeeForm />
</div>
</template>
<script setup lang="ts">
import EmployeeForm from '@/components/Employees/EmployeeForm.vue';
</script>
<style lang="scss" scoped>
</style>

View File

@@ -95,10 +95,6 @@ watch(input, () => {
v$.value.$reset()
})
userStore.$subscribe((mutation, state) => {
// if(state.isLoggedIn) router.push({name: 'Home'})
})
async function onClick() {
if(await v$.value.$validate()) {
await userStore.login(input.username, input.password, router)