generated from alphane/template
Fixes from template
This commit is contained in:
@@ -46,7 +46,7 @@
|
||||
import { computed } from "vue"
|
||||
import { useDisplay } from "vuetify"
|
||||
|
||||
import HeaderActionsCard from "@/components/shared/cards/HeaderActionsCard.vue"
|
||||
import HeaderActionsCard from "@/components/common/HeaderActionsCard.vue"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -53,114 +53,8 @@
|
||||
validate-on="blur"
|
||||
/>
|
||||
</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-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>
|
||||
<div class="d-flex">
|
||||
<v-btn
|
||||
@@ -192,12 +86,12 @@ import { useRouter } from "vue-router"
|
||||
import { isEmpty, isNil } from "lodash"
|
||||
|
||||
import { required, minimum, email } from "@/utils/validators"
|
||||
import { resizeToStandard } from "@/utils/image-resizer"
|
||||
|
||||
import usersApi, { User } from "@/api/users-api"
|
||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||
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"
|
||||
|
||||
const userAttributes = ref<Partial<User>>({
|
||||
@@ -205,88 +99,8 @@ const userAttributes = ref<Partial<User>>({
|
||||
displayName: "",
|
||||
firstName: "",
|
||||
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) {
|
||||
if (focused) return
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
headline="Whoops, 403"
|
||||
title="Access Forbidden"
|
||||
text="You do not have permission to access that page"
|
||||
:image="SplashImage"
|
||||
:image="AppLogoSplash"
|
||||
>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
@@ -48,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SplashImage from "@/assets/SplashImage.png"
|
||||
import AppLogoSplash from "@/assets/app_logo_splash.png"
|
||||
import { useAuth0 } from "@auth0/auth0-vue"
|
||||
|
||||
import { APPLICATION_NAME } from "@/config"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
headline="Whoops, 500"
|
||||
title="Internal Server Error"
|
||||
:text="'Oops! The server encountered an unexpected error. Please\u00a0contact\u00a0support.'"
|
||||
:image="SplashImage"
|
||||
:image="AppLogoSplash"
|
||||
>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
@@ -48,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SplashImage from "@/assets/SplashImage.png"
|
||||
import AppLogoSplash from "@/assets/app_logo_splash.png"
|
||||
import { useAuth0 } from "@auth0/auth0-vue"
|
||||
|
||||
import { APPLICATION_NAME } from "@/config"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
headline="Whoops, 404"
|
||||
title="Page Not Found"
|
||||
text="Oops! The page you're looking doesn't exist."
|
||||
:image="SplashImage"
|
||||
:image="AppLogoSplash"
|
||||
>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
@@ -48,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SplashImage from "@/assets/SplashImage.png"
|
||||
import AppLogoSplash from "@/assets/app_logo_splash.png"
|
||||
import { useAuth0 } from "@auth0/auth0-vue"
|
||||
|
||||
import { APPLICATION_NAME } from "@/config"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
headline="Whoops, 401"
|
||||
title="Unauthorized"
|
||||
text="If you think this is an error, please contact support. Alternatively, try logging out and signing back in."
|
||||
:image="SplashImage"
|
||||
:image="AppLogoSplash"
|
||||
>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
@@ -48,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SplashImage from "@/assets/SplashImage.png"
|
||||
import AppLogoSplash from "@/assets/app_logo_splash.png"
|
||||
import { useAuth0 } from "@auth0/auth0-vue"
|
||||
|
||||
import { APPLICATION_NAME } from "@/config"
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { type Ref, reactive, toRefs, ref, unref, watch } from "vue"
|
||||
|
||||
import notificationsApi, {
|
||||
NotificationSourceTypes,
|
||||
type Notification,
|
||||
type NotificationWhereOptions,
|
||||
type NotificationFiltersOptions,
|
||||
} from "@/api/notifications-api"
|
||||
|
||||
export {
|
||||
NotificationSourceTypes,
|
||||
type Notification,
|
||||
type NotificationWhereOptions,
|
||||
type NotificationFiltersOptions,
|
||||
}
|
||||
|
||||
export function useNotifications(
|
||||
queryOptions: Ref<{
|
||||
where?: Record<string, unknown>
|
||||
page?: number
|
||||
perPage?: number
|
||||
}> = ref({}),
|
||||
{ skipWatchIf = () => false }: { skipWatchIf?: () => boolean } = {}
|
||||
) {
|
||||
const state = reactive<{
|
||||
notifications: Notification[]
|
||||
totalCount: number
|
||||
isLoading: boolean
|
||||
isErrored: boolean
|
||||
}>({
|
||||
notifications: [],
|
||||
totalCount: 0,
|
||||
isLoading: false,
|
||||
isErrored: false,
|
||||
})
|
||||
|
||||
async function fetch(): Promise<Notification[]> {
|
||||
state.isLoading = true
|
||||
try {
|
||||
const { notifications, totalCount } = await notificationsApi.list(unref(queryOptions))
|
||||
state.isErrored = false
|
||||
state.notifications = notifications
|
||||
state.totalCount = totalCount
|
||||
return notifications
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch notifications:", error)
|
||||
state.isErrored = true
|
||||
throw error
|
||||
} finally {
|
||||
state.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => unref(queryOptions),
|
||||
async () => {
|
||||
if (skipWatchIf()) return
|
||||
|
||||
await fetch()
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
fetch,
|
||||
refresh: fetch,
|
||||
}
|
||||
}
|
||||
|
||||
export default useNotifications
|
||||
@@ -66,31 +66,6 @@ export function useUser(id: Ref<number | null | undefined>) {
|
||||
}
|
||||
}
|
||||
|
||||
async function directorySync() {
|
||||
const staticId = unref(id)
|
||||
if (isNil(staticId)) {
|
||||
throw new Error("id is required")
|
||||
}
|
||||
|
||||
if (isNil(state.user)) {
|
||||
throw new Error("No user to save")
|
||||
}
|
||||
|
||||
state.isLoading = true
|
||||
try {
|
||||
const { user } = await usersApi.directorySync(staticId)
|
||||
state.isErrored = false
|
||||
state.user = user
|
||||
return user
|
||||
} catch (error) {
|
||||
console.error("Failed to sync user:", error)
|
||||
state.isErrored = true
|
||||
throw error
|
||||
} finally {
|
||||
state.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => unref(id),
|
||||
async (newId) => {
|
||||
@@ -106,7 +81,6 @@ export function useUser(id: Ref<number | null | undefined>) {
|
||||
fetch,
|
||||
refresh: fetch,
|
||||
save,
|
||||
directorySync,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user