Frontend clean up
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<v-card
|
||||||
|
elevation="10"
|
||||||
|
hover
|
||||||
|
>
|
||||||
|
<v-card-item>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-avatar
|
||||||
|
:color="color"
|
||||||
|
size="56"
|
||||||
|
class="mr-4"
|
||||||
|
>
|
||||||
|
<slot name="icon"></slot>
|
||||||
|
</v-avatar>
|
||||||
|
<div>
|
||||||
|
<v-card-title class="text-h5">{{ title }}</v-card-title>
|
||||||
|
<v-card-subtitle>{{ subtitle }}</v-card-subtitle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex align-center justify-space-between">
|
||||||
|
<span class="text-h3 font-weight-bold">{{ count ?? " " }}</span>
|
||||||
|
<v-icon
|
||||||
|
size="large"
|
||||||
|
:color="color"
|
||||||
|
>
|
||||||
|
mdi-chevron-right
|
||||||
|
</v-icon>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-medium-emphasis mt-1">{{ countLabel }}</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
count?: number | null
|
||||||
|
countLabel?: string
|
||||||
|
color?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
subtitle: "",
|
||||||
|
count: null,
|
||||||
|
countLabel: "",
|
||||||
|
color: "primary",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<v-text-field
|
||||||
|
ref="textField"
|
||||||
|
:model-value="modelValue"
|
||||||
|
:label="label"
|
||||||
|
:rules="[...rules, valueMustBeUnique]"
|
||||||
|
@update:model-value="updateModelValue"
|
||||||
|
>
|
||||||
|
<template #append-inner>
|
||||||
|
<v-icon
|
||||||
|
v-if="isNil(isUniqueValue)"
|
||||||
|
:title="uniqueValidationMessage"
|
||||||
|
>mdi-progress-clock</v-icon
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
v-else-if="isCheckingUniqueness"
|
||||||
|
size="18"
|
||||||
|
width="2"
|
||||||
|
indeterminate
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
<v-icon
|
||||||
|
v-else-if="isUniqueValue === true"
|
||||||
|
:title="isUniqueMessage"
|
||||||
|
color="success"
|
||||||
|
>mdi-check-circle-outline</v-icon
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
v-else
|
||||||
|
color="warning"
|
||||||
|
:title="isNotUniqueMessage"
|
||||||
|
>mdi-alert-circle-outline</v-icon
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { debounce, isEmpty, isNil } from "lodash"
|
||||||
|
|
||||||
|
import { VTextField } from "vuetify/components"
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: string | null | undefined
|
||||||
|
label: string
|
||||||
|
checkAvailability: (value: string) => Promise<boolean>
|
||||||
|
rules?: VTextField["rules"]
|
||||||
|
uniqueValidationMessage?: string
|
||||||
|
isUniqueMessage?: string
|
||||||
|
isNotUniqueMessage?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
rules: () => [],
|
||||||
|
uniqueValidationMessage: "Value must be unique",
|
||||||
|
isUniqueMessage: "Value is unique",
|
||||||
|
isNotUniqueMessage: "Value is not unique",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isUniqueValue = ref<boolean | null>(null)
|
||||||
|
const isCheckingUniqueness = ref(false)
|
||||||
|
const textField = ref<InstanceType<typeof VTextField> | null>(null)
|
||||||
|
|
||||||
|
async function updateModelValue(value: string) {
|
||||||
|
emit("update:modelValue", value)
|
||||||
|
|
||||||
|
await debouncedCheckValueIsUnique(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkValueIsUnique(value: string | null | undefined) {
|
||||||
|
if (isNil(value) || isEmpty(value)) return null
|
||||||
|
|
||||||
|
isCheckingUniqueness.value = true
|
||||||
|
try {
|
||||||
|
const result = await props.checkAvailability(value)
|
||||||
|
isUniqueValue.value = result
|
||||||
|
await textField.value?.validate(false)
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
isCheckingUniqueness.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedCheckValueIsUnique = debounce(checkValueIsUnique, 500)
|
||||||
|
|
||||||
|
function valueMustBeUnique() {
|
||||||
|
if (isUniqueValue.value === true || isUniqueValue.value === null) return true
|
||||||
|
|
||||||
|
return props.uniqueValidationMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
validate: () => checkValueIsUnique(props.modelValue),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -10,31 +10,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
|
|
||||||
<v-btn
|
|
||||||
v-if="isSystemAdmin"
|
|
||||||
icon
|
|
||||||
variant="text"
|
|
||||||
class="custom-hover-primary mr-2"
|
|
||||||
size="small"
|
|
||||||
title="AI Assistant"
|
|
||||||
@click="$emit('toggle-ai-panel')"
|
|
||||||
>
|
|
||||||
<v-icon size="28">mdi-robot-outline</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<ProfileMenu />
|
<ProfileMenu />
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import useCurrentUser from "@/use/use-current-user"
|
|
||||||
|
|
||||||
import AppLogo from "@/components/common/AppLogo.vue"
|
import AppLogo from "@/components/common/AppLogo.vue"
|
||||||
import ProfileMenu from "@/components/layout/ProfileMenu.vue"
|
import ProfileMenu from "@/components/layout/ProfileMenu.vue"
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
"toggle-ai-panel": []
|
"toggle-ai-panel": []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { isSystemAdmin } = useCurrentUser<true>()
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<UniqueTextField
|
||||||
|
ref="uniqueTextField"
|
||||||
|
v-model="userEmail"
|
||||||
|
label="Email"
|
||||||
|
:check-availability="checkEmailAvailability"
|
||||||
|
unique-validation-message="User email must be unique"
|
||||||
|
is-unique-message="User email is available"
|
||||||
|
is-not-unique-message="User email is already taken"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
import usersApi from "@/api/users-api"
|
||||||
|
|
||||||
|
import UniqueTextField from "@/components/common/UniqueTextField.vue"
|
||||||
|
|
||||||
|
const userEmail = defineModel<string | null | undefined>({
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function checkEmailAvailability(email: string) {
|
||||||
|
const { users } = await usersApi.list({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return users.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueTextField = ref<InstanceType<typeof UniqueTextField> | null>(null)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<v-chip
|
||||||
|
:color="getRoleColor(role)"
|
||||||
|
class="my-0"
|
||||||
|
:text="roleText"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from "vue-i18n"
|
||||||
|
|
||||||
|
import { UserRoles } from "@/api/users-api"
|
||||||
|
|
||||||
|
const props = defineProps<{ role: string }>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const roleText = t(`user.roles.${props.role}`, props.role)
|
||||||
|
|
||||||
|
function getRoleColor(roleName: string) {
|
||||||
|
switch (roleName) {
|
||||||
|
case UserRoles.SYSTEM_ADMIN:
|
||||||
|
return "red"
|
||||||
|
default:
|
||||||
|
return "primary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -9,7 +9,14 @@
|
|||||||
@click:row="rowClicked"
|
@click:row="rowClicked"
|
||||||
@update:page="updatePage"
|
@update:page="updatePage"
|
||||||
>
|
>
|
||||||
<template #item.roles="{ item }"> {{ item.roles?.join(", ") }} affaafafa </template>
|
<template #item.roles="{ item }">
|
||||||
|
<UserRoleChip
|
||||||
|
v-for="role in item.roles"
|
||||||
|
:key="role"
|
||||||
|
:role="role"
|
||||||
|
class="mr-1"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template #item.updatedAt="{ value }">{{ formatDate(value) }}</template>
|
<template #item.updatedAt="{ value }">{{ formatDate(value) }}</template>
|
||||||
<template #item.createdAt="{ value }">{{ formatDate(value) }}</template>
|
<template #item.createdAt="{ value }">{{ formatDate(value) }}</template>
|
||||||
<template
|
<template
|
||||||
@@ -55,6 +62,8 @@ import useUsers, {
|
|||||||
} from "@/use/use-users"
|
} from "@/use/use-users"
|
||||||
import useRouteQueryPagination from "@/use/utils/use-route-query-pagination"
|
import useRouteQueryPagination from "@/use/utils/use-route-query-pagination"
|
||||||
|
|
||||||
|
import UserRoleChip from "@/components/users/UserRoleChip.vue"
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
headers?: { title: string; key: string }[]
|
headers?: { title: string; key: string }[]
|
||||||
|
|||||||
@@ -19,11 +19,6 @@
|
|||||||
<router-view />
|
<router-view />
|
||||||
</v-container>
|
</v-container>
|
||||||
</v-main>
|
</v-main>
|
||||||
|
|
||||||
<AiChatPanel
|
|
||||||
v-if="showAiPanel"
|
|
||||||
v-model="showAiPanel"
|
|
||||||
/>
|
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isSystemAdmin">
|
<div v-if="isSystemAdmin">
|
||||||
<AppCard :to="{ name: 'administration/DashboardPage' }">You are a system admin</AppCard>
|
<AppCard :to="{ name: 'administration/AdministrationDashboardPage' }"
|
||||||
|
>You are a system admin</AppCard
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,105 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-row>
|
<v-row v-if="isSystemAdmin">
|
||||||
<!-- Users Card -->
|
|
||||||
<v-col
|
<v-col
|
||||||
v-if="isSystemAdmin"
|
|
||||||
cols="12"
|
cols="12"
|
||||||
md="6"
|
md="6"
|
||||||
lg="4"
|
lg="4"
|
||||||
>
|
>
|
||||||
<v-card
|
<DashboardCard
|
||||||
elevation="10"
|
|
||||||
:to="{ name: 'administration/UsersPage' }"
|
:to="{ name: 'administration/UsersPage' }"
|
||||||
hover
|
title="Users"
|
||||||
>
|
subtitle="Manage users and permissions"
|
||||||
<v-card-item>
|
:count="usersCount"
|
||||||
<div class="d-flex align-center">
|
count-label="Total Users"
|
||||||
<v-avatar
|
color="green"
|
||||||
color="success"
|
|
||||||
size="56"
|
|
||||||
class="mr-4"
|
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
<v-icon
|
<v-icon
|
||||||
size="32"
|
size="32"
|
||||||
color="white"
|
color="white"
|
||||||
>
|
icon="mdi-account-group"
|
||||||
mdi-account-group
|
/>
|
||||||
</v-icon>
|
</template>
|
||||||
</v-avatar>
|
</DashboardCard>
|
||||||
<div>
|
|
||||||
<v-card-title class="text-h5">Users</v-card-title>
|
|
||||||
<v-card-subtitle>Manage users and permissions</v-card-subtitle>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-card-item>
|
|
||||||
|
|
||||||
<v-divider />
|
|
||||||
|
|
||||||
<v-card-text>
|
|
||||||
<div class="d-flex align-center justify-space-between">
|
|
||||||
<span class="text-h3 font-weight-bold">{{ usersCount }}</span>
|
|
||||||
<v-icon
|
|
||||||
size="large"
|
|
||||||
color="success"
|
|
||||||
>
|
|
||||||
mdi-chevron-right
|
|
||||||
</v-icon>
|
|
||||||
</div>
|
|
||||||
<div class="text-caption text-medium-emphasis mt-1">Total Users</div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<!-- Settings Card -->
|
|
||||||
<v-col
|
<v-col
|
||||||
v-if="isSystemAdmin"
|
|
||||||
cols="12"
|
cols="12"
|
||||||
md="6"
|
md="6"
|
||||||
lg="4"
|
lg="4"
|
||||||
>
|
>
|
||||||
<v-card
|
<DashboardCard
|
||||||
elevation="10"
|
|
||||||
:to="{ name: 'administration/SettingsPage' }"
|
:to="{ name: 'administration/SettingsPage' }"
|
||||||
hover
|
title="Settings"
|
||||||
>
|
subtitle="System configuration"
|
||||||
<v-card-item>
|
count-label="Config Settings"
|
||||||
<div class="d-flex align-center">
|
|
||||||
<v-avatar
|
|
||||||
color="info"
|
color="info"
|
||||||
size="56"
|
|
||||||
class="mr-4"
|
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
<v-icon
|
<v-icon
|
||||||
size="32"
|
size="32"
|
||||||
color="white"
|
color="white"
|
||||||
>
|
icon="mdi-cog"
|
||||||
mdi-cog
|
/>
|
||||||
</v-icon>
|
</template>
|
||||||
</v-avatar>
|
</DashboardCard>
|
||||||
<div>
|
|
||||||
<v-card-title class="text-h5">Settings</v-card-title>
|
|
||||||
<v-card-subtitle>System configuration</v-card-subtitle>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-card-item>
|
|
||||||
|
|
||||||
<v-divider />
|
|
||||||
|
|
||||||
<v-card-text>
|
|
||||||
<div class="d-flex align-center justify-space-between">
|
|
||||||
<span class="text-h3 font-weight-bold"> </span>
|
|
||||||
|
|
||||||
<v-icon
|
|
||||||
size="large"
|
|
||||||
color="info"
|
|
||||||
>
|
|
||||||
mdi-chevron-right
|
|
||||||
</v-icon>
|
|
||||||
</div>
|
|
||||||
<div class="text-caption text-medium-emphasis mt-1">Configure System</div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,6 +53,8 @@ import useBreadcrumbs from "@/use/use-breadcrumbs"
|
|||||||
import useUsers from "@/use/use-users"
|
import useUsers from "@/use/use-users"
|
||||||
import useCurrentUser from "@/use/use-current-user"
|
import useCurrentUser from "@/use/use-current-user"
|
||||||
|
|
||||||
|
import DashboardCard from "@/components/common/DashboardCard.vue"
|
||||||
|
|
||||||
const { isSystemAdmin } = useCurrentUser()
|
const { isSystemAdmin } = useCurrentUser()
|
||||||
|
|
||||||
const { totalCount: usersCount } = useUsers()
|
const { totalCount: usersCount } = useUsers()
|
||||||
|
|||||||
@@ -53,114 +53,8 @@
|
|||||||
validate-on="blur"
|
validate-on="blur"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
v-model="userAttributes.title"
|
|
||||||
label="Title"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
v-model="userAttributes.pilotLicense"
|
|
||||||
label="Pilot License"
|
|
||||||
placeholder="e.g., CPL-123456"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
v-model="userAttributes.ameLicense"
|
|
||||||
label="AME License"
|
|
||||||
placeholder="e.g., AME-789012"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<h4 class="mt-3">Images</h4>
|
|
||||||
<v-divider class="mt-1 mb-2" />
|
|
||||||
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-file-input
|
|
||||||
v-model="profileImageFile"
|
|
||||||
accept="image/*"
|
|
||||||
prepend-icon="mdi-camera"
|
|
||||||
label="Upload Profile Image"
|
|
||||||
clearable
|
|
||||||
@change="handleProfileImageUpload"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="profileImagePreview"
|
|
||||||
class="mt-4 d-flex justify-center"
|
|
||||||
>
|
|
||||||
<v-img
|
|
||||||
:src="profileImagePreview"
|
|
||||||
max-width="200"
|
|
||||||
max-height="200"
|
|
||||||
class="rounded elevation-2"
|
|
||||||
cover
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-file-input
|
|
||||||
v-model="signatureImageFile"
|
|
||||||
accept="image/*"
|
|
||||||
prepend-icon="mdi-draw"
|
|
||||||
label="Upload Signature"
|
|
||||||
clearable
|
|
||||||
@change="handleSignatureImageUpload"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="signatureImagePreview"
|
|
||||||
class="mt-4 d-flex justify-center"
|
|
||||||
>
|
|
||||||
<v-img
|
|
||||||
:src="signatureImagePreview"
|
|
||||||
max-width="300"
|
|
||||||
max-height="150"
|
|
||||||
class="rounded elevation-2"
|
|
||||||
contain
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<h4 class="mt-3">Notification Details</h4>
|
|
||||||
<v-divider class="mt-1 mb-2" />
|
|
||||||
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-switch
|
|
||||||
v-model="userAttributes.emailNotificationsEnabled"
|
|
||||||
label="Email notifications enabled?"
|
|
||||||
inset
|
|
||||||
center-affix
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<v-btn
|
<v-btn
|
||||||
@@ -192,12 +86,11 @@ import { useRouter } from "vue-router"
|
|||||||
import { isEmpty, isNil } from "lodash"
|
import { isEmpty, isNil } from "lodash"
|
||||||
|
|
||||||
import { required, minimum, email } from "@/utils/validators"
|
import { required, minimum, email } from "@/utils/validators"
|
||||||
import { resizeToStandard } from "@/utils/image-resizer"
|
|
||||||
import usersApi, { User } from "@/api/users-api"
|
import usersApi, { User } from "@/api/users-api"
|
||||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||||
import useSnack from "@/use/use-snack"
|
import useSnack from "@/use/use-snack"
|
||||||
|
|
||||||
import HeaderActionsFormCard from "@/components/shared/cards/HeaderActionsFormCard.vue"
|
import HeaderActionsFormCard from "@/components/common/HeaderActionsFormCard.vue"
|
||||||
import UserEmailUniqueTextField from "@/components/users/UserEmailUniqueTextField.vue"
|
import UserEmailUniqueTextField from "@/components/users/UserEmailUniqueTextField.vue"
|
||||||
|
|
||||||
const userAttributes = ref<Partial<User>>({
|
const userAttributes = ref<Partial<User>>({
|
||||||
@@ -205,88 +98,8 @@ const userAttributes = ref<Partial<User>>({
|
|||||||
displayName: "",
|
displayName: "",
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
title: null,
|
|
||||||
pilotLicense: null,
|
|
||||||
ameLicense: null,
|
|
||||||
emailNotificationsEnabled: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Image handling
|
|
||||||
const profileImageFile = ref<File[] | File | null>(null)
|
|
||||||
const signatureImageFile = ref<File[] | File | null>(null)
|
|
||||||
const profileImagePreview = ref<string | null>(null)
|
|
||||||
const signatureImagePreview = ref<string | null>(null)
|
|
||||||
|
|
||||||
async function handleProfileImageUpload() {
|
|
||||||
if (!profileImageFile.value) {
|
|
||||||
profileImagePreview.value = null
|
|
||||||
userAttributes.value.profileImage = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle both array and single file
|
|
||||||
const file = Array.isArray(profileImageFile.value)
|
|
||||||
? profileImageFile.value[0]
|
|
||||||
: profileImageFile.value
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
profileImagePreview.value = null
|
|
||||||
userAttributes.value.profileImage = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we have a valid File object
|
|
||||||
if (!(file instanceof File)) {
|
|
||||||
console.error("Invalid file type:", file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Resize image to 512x512 before upload
|
|
||||||
const base64DataUrl = await resizeToStandard(file)
|
|
||||||
profileImagePreview.value = base64DataUrl
|
|
||||||
userAttributes.value.profileImage = base64DataUrl
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error resizing profile image:", error)
|
|
||||||
snack.error("Failed to process profile image")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSignatureImageUpload() {
|
|
||||||
if (!signatureImageFile.value) {
|
|
||||||
signatureImagePreview.value = null
|
|
||||||
userAttributes.value.signatureImage = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle both array and single file
|
|
||||||
const file = Array.isArray(signatureImageFile.value)
|
|
||||||
? signatureImageFile.value[0]
|
|
||||||
: signatureImageFile.value
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
signatureImagePreview.value = null
|
|
||||||
userAttributes.value.signatureImage = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we have a valid File object
|
|
||||||
if (!(file instanceof File)) {
|
|
||||||
console.error("Invalid file type:", file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Resize image to 512x512 before upload
|
|
||||||
const base64DataUrl = await resizeToStandard(file)
|
|
||||||
signatureImagePreview.value = base64DataUrl
|
|
||||||
userAttributes.value.signatureImage = base64DataUrl
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error resizing signature image:", error)
|
|
||||||
snack.error("Failed to process signature image")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function autoFillDependentFields(focused: boolean) {
|
function autoFillDependentFields(focused: boolean) {
|
||||||
if (focused) return
|
if (focused) return
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user