Fixes from template

This commit is contained in:
2026-06-25 00:38:47 -07:00
parent d134b480a0
commit bf1d82fa19
24 changed files with 162 additions and 323 deletions
@@ -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
+2 -2
View File
@@ -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"
+2 -2
View File
@@ -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"
+2 -2
View File
@@ -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"
-71
View File
@@ -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
-26
View File
@@ -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,
}
}