Merge pull request #28 from sockenklaus/sockenklaus/issue25

This commit is contained in:
sockenklaus
2021-11-28 14:16:04 +01:00
committed by GitHub
14 changed files with 480 additions and 360 deletions

View 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>

View File

@@ -1,12 +1,14 @@
<script setup lang="ts">
import { toRef } from 'vue'
import { toRefs } from 'vue'
const props = defineProps(['isActive'])
const props = defineProps<{
isActive: boolean,
isNew: boolean
}>()
const emit = defineEmits(['save', 'toggleEdit'])
const isActive = toRef(props, 'isActive')
const { isActive } = toRefs(props)
function onEdit() {
emit('toggleEdit')
@@ -23,8 +25,9 @@ function onCancel() {
</script>
<template>
<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>
Zurück
</router-link>
@@ -35,7 +38,7 @@ function onCancel() {
<i class="bi bi-save"></i>
Mitarbeiter speichern
</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>
Abbrechen
</button>

View 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>

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

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

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)
@@ -101,15 +116,52 @@ export const useEmployee = defineStore({
Object.assign(this.clean.employee, this.employee)
notifications.add('success', result.statusText)
return true
}
catch(error) {
if(error instanceof Error) {
console.log(error)
console.log("Patch 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)
}
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: {

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 : {
@@ -41,7 +41,7 @@ export default defineStore('employees', () => {
}
)).data
Object.assign(state.meta, data.meta)
_assign(state.meta, data.meta)
state.rows = _cloneDeep(data.data)
_assign(state.columns, fetchColumns())
@@ -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()
}
}
}) */
})

View File

@@ -37,8 +37,6 @@ export const useSettings = defineStore({
headers: user.header
})
console.log(result)
result.data.forEach((element) => {
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)})
}
}
console.log(settings)
const result = await axios.post('settings',
{
settings
@@ -75,7 +72,6 @@ export const useSettings = defineStore({
{
headers: user.header
})
console.log(result)
}
}
})

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

@@ -8,6 +8,7 @@
@update-columns-selected="updateColumnsSelected"
/>
<IndexControls />
<SimpleSearch
:columns="settings.employeesIndexColumnsSelected"
@@ -57,6 +58,7 @@
<script setup lang="ts">
import SimpleSearch from '@/components/Employees/SimpleSearch.vue';
import IndexSettingsModal from '@/components/Employees/IndexSettingsModal.vue';
import IndexControls from '@/components/Employees/IndexControls.vue';
import VPaginator from '@/components/VPaginator.vue';
import { onMounted, reactive, computed } from 'vue'
import { useRouter } from 'vue-router'
@@ -77,6 +79,7 @@ const sort_by = reactive({
})
onMounted(async () =>{
console.log("Employees Index onMounted")
await store.fetchFromApi()
await settingsStore.fetchFromApi()
})

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)