Merge pull request #28 from sockenklaus/sockenklaus/issue25
This commit is contained in:
327
src/components/Employees/EmployeeForm.vue
Normal file
327
src/components/Employees/EmployeeForm.vue
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<EmployeeFormControls
|
||||||
|
class="mb-5"
|
||||||
|
:isActive="editEmployee || createUser"
|
||||||
|
:isNew="isNew"
|
||||||
|
@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"
|
||||||
|
:isNew="isNew"
|
||||||
|
@save="submitForm"
|
||||||
|
@toggleEdit="toggleEdit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
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)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 // return helpers.withMessage("", () => true) doesn't work!!!!
|
||||||
|
}
|
||||||
|
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) // error because wrong return type in validateApiErrors()...
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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'
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const isNew = computed(() => {
|
||||||
|
return props.id ? false : true
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* 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){
|
||||||
|
if(await employeeStore.patchEmployee()){
|
||||||
|
toggleEdit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(await employeeStore.postEmployee()) {
|
||||||
|
router.push({name: "Employees/Index"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch State
|
||||||
|
*/
|
||||||
|
watch(employee, () => {
|
||||||
|
v$.value.$reset()
|
||||||
|
employeeStore.apiValidationErrors = []
|
||||||
|
},{
|
||||||
|
deep: true
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Component
|
||||||
|
*/
|
||||||
|
onMounted(() => {
|
||||||
|
if(props.id !== undefined) {
|
||||||
|
employeeStore.fetchFromApi(props.id)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
employeeStore.$reset()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="scss">
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import { toRef } from 'vue'
|
import { toRefs } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
const props = defineProps(['isActive'])
|
isActive: boolean,
|
||||||
|
isNew: boolean
|
||||||
|
}>()
|
||||||
const emit = defineEmits(['save', 'toggleEdit'])
|
const emit = defineEmits(['save', 'toggleEdit'])
|
||||||
|
|
||||||
const isActive = toRef(props, 'isActive')
|
const { isActive } = toRefs(props)
|
||||||
|
|
||||||
function onEdit() {
|
function onEdit() {
|
||||||
emit('toggleEdit')
|
emit('toggleEdit')
|
||||||
@@ -23,8 +25,9 @@ function onCancel() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<router-link type="button" :to="{name: 'Home'}" class="btn btn-outline-secondary">
|
<router-link type="button" :to="{name: 'Employees/Index'}" class="btn btn-outline-secondary">
|
||||||
<i class="bi bi-chevron-left"></i>
|
<i class="bi bi-chevron-left"></i>
|
||||||
Zurück
|
Zurück
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -35,7 +38,7 @@ function onCancel() {
|
|||||||
<i class="bi bi-save"></i>
|
<i class="bi bi-save"></i>
|
||||||
Mitarbeiter speichern
|
Mitarbeiter speichern
|
||||||
</button>
|
</button>
|
||||||
<button v-if="isActive" type="button" @click="onCancel" class="btn btn-outline-secondary ms-3">
|
<button v-if="isActive && !isNew" type="button" @click="onCancel" class="btn btn-outline-secondary ms-3">
|
||||||
<i class="bi bi-x-lg"></i>
|
<i class="bi bi-x-lg"></i>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
20
src/components/Employees/IndexControls.vue
Normal file
20
src/components/Employees/IndexControls.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-flex mb-4">
|
||||||
|
<router-link type="button" :to="{name: 'Home'}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-chevron-left"></i>
|
||||||
|
Zurück
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<router-link type="button" :to="{name: 'Employees/New'}" class="btn btn-success ms-auto">
|
||||||
|
<i class="bi bi-save"></i>
|
||||||
|
Neuer Mitarbeiter
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
</style>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
AddEmployeeModal(
|
AddEmployeeModal(
|
||||||
|
|
||||||
:searchData="store.state.rows"
|
:searchData="store.state.rows"
|
||||||
:searchFields="['first_name', 'last_name']"
|
:searchFields="['firstName', 'lastName']"
|
||||||
:searchRow="addEmployeeRow"
|
:searchRow="addEmployeeRow"
|
||||||
:modalId="modalId"
|
:modalId="modalId"
|
||||||
@emitResult="addEmployee($event)"
|
@emitResult="addEmployee($event)"
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
requiresAdmin: true,
|
requiresAdmin: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/employees/new',
|
||||||
|
name: 'Employees/New',
|
||||||
|
component: () => import('@/views/Employees/New.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
requiresAdmin: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/employees/:id',
|
path: '/employees/:id',
|
||||||
name: 'Employees/Details',
|
name: 'Employees/Details',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { defineStore, acceptHMRUpdate } from 'pinia'
|
|||||||
import axios from '@/axios'
|
import axios from '@/axios'
|
||||||
import { useUser } from './user'
|
import { useUser } from './user'
|
||||||
import { useNotifications } from './notifications'
|
import { useNotifications } from './notifications'
|
||||||
|
import Axios from 'axios'
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const notifications = useNotifications()
|
const notifications = useNotifications()
|
||||||
@@ -12,7 +13,7 @@ export const useEmployee = defineStore({
|
|||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
clean: {
|
clean: {
|
||||||
/** @type Employee */
|
/** @type { Employee } */
|
||||||
employee: {
|
employee: {
|
||||||
id: NaN,
|
id: NaN,
|
||||||
firstName: '',
|
firstName: '',
|
||||||
@@ -25,7 +26,8 @@ export const useEmployee = defineStore({
|
|||||||
isActive: false,
|
isActive: false,
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordConfirm: ''
|
passwordConfirm: '',
|
||||||
|
role: ''
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -42,8 +44,12 @@ export const useEmployee = defineStore({
|
|||||||
isActive: false,
|
isActive: false,
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordConfirm: ''
|
passwordConfirm: '',
|
||||||
|
role: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** @type { EmployeeApiValidationError[] } */
|
||||||
|
apiValidationErrors: Array<EmployeeApiValidationError>(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -77,8 +83,17 @@ export const useEmployee = defineStore({
|
|||||||
Object.assign(this.employee, this.clean.employee)
|
Object.assign(this.employee, this.clean.employee)
|
||||||
},
|
},
|
||||||
|
|
||||||
/** TODO: #23 Persist user if password is changed */
|
|
||||||
async persist() {
|
async persist() {
|
||||||
|
|
||||||
|
if(this.employee.id){
|
||||||
|
this.patchEmployee()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.postEmployee()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async patchEmployee() {
|
||||||
try {
|
try {
|
||||||
let result
|
let result
|
||||||
let payload = Object.assign({}, this.employee)
|
let payload = Object.assign({}, this.employee)
|
||||||
@@ -101,15 +116,52 @@ export const useEmployee = defineStore({
|
|||||||
|
|
||||||
Object.assign(this.clean.employee, this.employee)
|
Object.assign(this.clean.employee, this.employee)
|
||||||
notifications.add('success', result.statusText)
|
notifications.add('success', result.statusText)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
catch(error) {
|
catch(error) {
|
||||||
if(error instanceof Error) {
|
console.log("Patch Employee Error")
|
||||||
console.log(error)
|
if(Axios.isAxiosError(error)) {
|
||||||
|
let data = error.response?.data as { errors: EmployeeApiValidationError[] }
|
||||||
|
this.apiValidationErrors = [...data.errors]
|
||||||
|
}
|
||||||
|
else if(error instanceof Error) {
|
||||||
notifications.add('danger', error.message, -1)
|
notifications.add('danger', error.message, -1)
|
||||||
}
|
}
|
||||||
else console.log(error)
|
console.log(error)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
catch(error){
|
||||||
|
console.log("Post Employee Error")
|
||||||
|
if(Axios.isAxiosError(error)) {
|
||||||
|
let data = error.response?.data as { errors: EmployeeApiValidationError[] }
|
||||||
|
this.apiValidationErrors = [...data.errors]
|
||||||
|
}
|
||||||
|
else if(error instanceof Error) {
|
||||||
|
notifications.add('danger', error.message, -1)
|
||||||
|
}
|
||||||
|
console.log(error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default defineStore('employees', () => {
|
|||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
rows: Array<any>(),
|
rows: Array<Employee>(),
|
||||||
columns: Array<string>(),
|
columns: Array<string>(),
|
||||||
|
|
||||||
meta : {
|
meta : {
|
||||||
@@ -41,7 +41,7 @@ export default defineStore('employees', () => {
|
|||||||
}
|
}
|
||||||
)).data
|
)).data
|
||||||
|
|
||||||
Object.assign(state.meta, data.meta)
|
_assign(state.meta, data.meta)
|
||||||
state.rows = _cloneDeep(data.data)
|
state.rows = _cloneDeep(data.data)
|
||||||
|
|
||||||
_assign(state.columns, fetchColumns())
|
_assign(state.columns, fetchColumns())
|
||||||
@@ -91,89 +91,4 @@ export default defineStore('employees', () => {
|
|||||||
setPage
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) */
|
|
||||||
@@ -37,8 +37,6 @@ export const useSettings = defineStore({
|
|||||||
headers: user.header
|
headers: user.header
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(result)
|
|
||||||
|
|
||||||
result.data.forEach((element) => {
|
result.data.forEach((element) => {
|
||||||
this.settings[_camelCase(element.key)] = JSON.parse(element.value)
|
this.settings[_camelCase(element.key)] = JSON.parse(element.value)
|
||||||
})
|
})
|
||||||
@@ -67,7 +65,6 @@ export const useSettings = defineStore({
|
|||||||
settings.push({key: _kebabCase(key), value: JSON.stringify(value)})
|
settings.push({key: _kebabCase(key), value: JSON.stringify(value)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(settings)
|
|
||||||
const result = await axios.post('settings',
|
const result = await axios.post('settings',
|
||||||
{
|
{
|
||||||
settings
|
settings
|
||||||
@@ -75,7 +72,6 @@ export const useSettings = defineStore({
|
|||||||
{
|
{
|
||||||
headers: user.header
|
headers: user.header
|
||||||
})
|
})
|
||||||
console.log(result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
23
src/types/employees.d.ts
vendored
Normal file
23
src/types/employees.d.ts
vendored
Normal 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
|
||||||
|
}
|
||||||
13
src/types/schedule-data.d.ts
vendored
13
src/types/schedule-data.d.ts
vendored
@@ -2,19 +2,6 @@ type ScheduleData = [
|
|||||||
...ScheduleMonth[]
|
...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 = {
|
type ScheduleRow = {
|
||||||
dates : (Date | null)[],
|
dates : (Date | null)[],
|
||||||
employees : Employee[]
|
employees : Employee[]
|
||||||
|
|||||||
@@ -1,256 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<form class="text-start">
|
<div>
|
||||||
<VProfileControls class="mb-5" :isActive="editEmployee || createUser" @save="onUpdateEmployee" @toggleEdit="onToggleEdit" />
|
<EmployeeForm
|
||||||
|
:id="route.params.id"
|
||||||
|
|
||||||
<div class="row mb-5">
|
/>
|
||||||
<div class="col pe-5">
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import EmployeeForm from '@/components/Employees/EmployeeForm.vue'
|
||||||
import VProfileControls from '@/components/VProfileControls.vue';
|
|
||||||
import { onMounted, computed, ref, watch } from 'vue'
|
|
||||||
import { useEmployee } from '@/stores/employee'
|
|
||||||
import { useUser } from '@/stores/user'
|
|
||||||
import { useRoute } from 'vue-router';
|
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 route = useRoute()
|
||||||
|
/*
|
||||||
const state = useEmployee()
|
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]) => {
|
watch(() => [route.params, route.name], ([newParam, newName], [oldParam, oldName]) => {
|
||||||
if(newName === oldName && newParam?.toString() !== oldParam?.toString()) {
|
if(newName === oldName && newParam?.toString() !== oldParam?.toString()) {
|
||||||
state.fetchFromApi(route.params.id)
|
state.fetchFromApi(route.params.id)
|
||||||
createUser.value = false
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
@update-columns-selected="updateColumnsSelected"
|
@update-columns-selected="updateColumnsSelected"
|
||||||
/>
|
/>
|
||||||
|
<IndexControls />
|
||||||
|
|
||||||
<SimpleSearch
|
<SimpleSearch
|
||||||
:columns="settings.employeesIndexColumnsSelected"
|
:columns="settings.employeesIndexColumnsSelected"
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SimpleSearch from '@/components/Employees/SimpleSearch.vue';
|
import SimpleSearch from '@/components/Employees/SimpleSearch.vue';
|
||||||
import IndexSettingsModal from '@/components/Employees/IndexSettingsModal.vue';
|
import IndexSettingsModal from '@/components/Employees/IndexSettingsModal.vue';
|
||||||
|
import IndexControls from '@/components/Employees/IndexControls.vue';
|
||||||
import VPaginator from '@/components/VPaginator.vue';
|
import VPaginator from '@/components/VPaginator.vue';
|
||||||
import { onMounted, reactive, computed } from 'vue'
|
import { onMounted, reactive, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -77,6 +79,7 @@ const sort_by = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () =>{
|
onMounted(async () =>{
|
||||||
|
console.log("Employees Index onMounted")
|
||||||
await store.fetchFromApi()
|
await store.fetchFromApi()
|
||||||
await settingsStore.fetchFromApi()
|
await settingsStore.fetchFromApi()
|
||||||
})
|
})
|
||||||
|
|||||||
17
src/views/Employees/New.vue
Normal file
17
src/views/Employees/New.vue
Normal 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>
|
||||||
@@ -95,10 +95,6 @@ watch(input, () => {
|
|||||||
v$.value.$reset()
|
v$.value.$reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
userStore.$subscribe((mutation, state) => {
|
|
||||||
// if(state.isLoggedIn) router.push({name: 'Home'})
|
|
||||||
})
|
|
||||||
|
|
||||||
async function onClick() {
|
async function onClick() {
|
||||||
if(await v$.value.$validate()) {
|
if(await v$.value.$validate()) {
|
||||||
await userStore.login(input.username, input.password, router)
|
await userStore.login(input.username, input.password, router)
|
||||||
|
|||||||
Reference in New Issue
Block a user