Fixes from template
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "alphane-api",
|
"name": "calebburke-api",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "alphane-api",
|
"name": "calebburke-api",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "alphane-api",
|
"name": "calebburke-api",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Alphane Backend",
|
"description": "calebburke.dev Backend",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && tsc-alias",
|
"build": "tsc && tsc-alias",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import express, { Request, Response } from "express"
|
import express, { Request, Response } from "express"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
|
import { isArray } from "lodash"
|
||||||
|
|
||||||
import { NODE_ENV } from "@/config"
|
import { NODE_ENV } from "@/config"
|
||||||
import dbMigrationClient from "@/db/db-migration-client"
|
import dbMigrationClient from "@/db/db-migration-client"
|
||||||
@@ -35,7 +36,8 @@ export class Migrator {
|
|||||||
|
|
||||||
this.migrationRouter.get("/seed/:environment", async (req: Request, res: Response) => {
|
this.migrationRouter.get("/seed/:environment", async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
await this.seedUp(req.params.environment)
|
const environment = isArray(req.params.environment) ? req.params.environment[0] : req.params.environment
|
||||||
|
await this.seedUp(environment)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ x-default-environment: &default-environment
|
|||||||
TZ: "UTC"
|
TZ: "UTC"
|
||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
DB_HOST: db
|
DB_HOST: db
|
||||||
DB_USERNAME: &default-db-username alphane
|
DB_USERNAME: &default-db-username calebburke
|
||||||
DB_DATABASE: alphane_development
|
DB_DATABASE: calebburke_development
|
||||||
DB_PASSWORD: &default-db-password DevPwd99!
|
DB_PASSWORD: &default-db-password DevPwd99!
|
||||||
DB_PORT: &default-db-port 5432
|
DB_PORT: &default-db-port 5432
|
||||||
DB_TRUST_SERVER_CERTIFICATE: "true"
|
DB_TRUST_SERVER_CERTIFICATE: "true"
|
||||||
@@ -12,7 +12,7 @@ x-default-environment: &default-environment
|
|||||||
DB_HEALTH_CHECK_RETRIES: 3
|
DB_HEALTH_CHECK_RETRIES: 3
|
||||||
DB_HEALTH_CHECK_START_PERIOD_SECONDS: 5
|
DB_HEALTH_CHECK_START_PERIOD_SECONDS: 5
|
||||||
FRONTEND_URL: "http://localhost:8080"
|
FRONTEND_URL: "http://localhost:8080"
|
||||||
VITE_APPLICATION_NAME: "ALPHANE"
|
VITE_APPLICATION_NAME: "CALEB BURKE DEV"
|
||||||
VITE_API_BASE_URL: "http://localhost:3000"
|
VITE_API_BASE_URL: "http://localhost:3000"
|
||||||
VITE_AUTH0_CLIENT_ID: "TRlKzdNBynpo9tU1RSmnF0p8d3IEam4J"
|
VITE_AUTH0_CLIENT_ID: "TRlKzdNBynpo9tU1RSmnF0p8d3IEam4J"
|
||||||
VITE_AUTH0_AUDIENCE: "alphane-api"
|
VITE_AUTH0_AUDIENCE: "alphane-api"
|
||||||
@@ -25,7 +25,6 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./api
|
context: ./api
|
||||||
dockerfile: development.Dockerfile
|
dockerfile: development.Dockerfile
|
||||||
image: wrap-api:latest
|
|
||||||
env_file:
|
env_file:
|
||||||
- ./api/.env.development
|
- ./api/.env.development
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "alphane-web",
|
"name": "calebburke-web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "alphane-web",
|
"name": "calebburke-web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth0/auth0-vue": "^2.5.0",
|
"@auth0/auth0-vue": "^2.5.0",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "alphane-web",
|
"name": "calebburke-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 198 KiB |
@@ -1,8 +0,0 @@
|
|||||||
<svg width="109" height="44" viewBox="0 0 109 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M99 26.9C101.3 26.9 102.6 28.4 102.6 31.2V42.7H108.4V29.3C108.4 24.5 105.8 21.6 101.4 21.6C98.3 21.6 96.6 23.3 95.4 24.9L95.3 25V22H89.5V42.7H95.3V31.1C95.2 28.4 96.6 26.9 99 26.9Z" fill="black"/>
|
|
||||||
<path d="M81.6 32.5C81.6 34 81.1 35.5 80.2 36.5C79.2 37.6 77.8 38.2 76.2 38.2C74.6 38.2 73.2 37.6 72.2 36.5C71.2 35.4 70.7 34 70.7 32.4V32.3C70.7 30.8 71.2 29.3 72.1 28.3C73.1 27.2 74.5 26.6 76.1 26.6C77.7 26.6 79.1 27.2 80.1 28.3C81 29.4 81.6 30.9 81.6 32.5ZM76.1 21.6C73 21.6 70.2 22.7 68.1 24.8C66 26.8 64.9 29.5 64.9 32.4V32.5C64.9 35.4 66 38.1 68.1 40.1C70.2 42.1 73 43.2 76.1 43.2C79.2 43.2 82 42.1 84.1 40C86.2 38 87.3 35.3 87.3 32.4V32.3C87.3 29.4 86.2 26.7 84.1 24.7C82.1 22.7 79.2 21.6 76.1 21.6Z" fill="black"/>
|
|
||||||
<path d="M59.8 42.7H66.4L58.3 30.1L66.1 22H59.2L52.3 29.7V15.1H46.5V42.7H52.3V36.4L54.4 34.2L59.8 42.7Z" fill="black"/>
|
|
||||||
<path d="M31.6 43.1C34.7 43.1 36.4 41.4 37.6 39.8L37.7 39.7V42.7H43.5V22H37.7V33.5C37.7 36.2 36.3 37.8 34 37.8C31.7 37.8 30.4 36.3 30.4 33.5V22H24.6V35.4C24.6 40.2 27.2 43.1 31.6 43.1Z" fill="black"/>
|
|
||||||
<path d="M20.7 15.7L14 26.3V26.2L7.5 15.7H0.699997L11.1 31.7V42.7H16.9V31.6L27.3 15.7H20.7Z" fill="black"/>
|
|
||||||
<path d="M76.1 0.800003L74 7.4L69.5 2.1L70.2 9L64 5.9L67.2 12L60.3 11.4L65.6 15.9L59 18L64.7 19.8L69.6 14.5L71.6 16.7L76.2 11.8L80.7 16.7L82.7 14.5L87.6 19.7L93.3 17.9L86.7 15.8L92 11.3L85.1 12L88.2 5.8L82.1 9L82.7 2.1L78.2 7.4L76.1 0.800003Z" fill="#F2A900"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -46,7 +46,7 @@
|
|||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { useDisplay } from "vuetify"
|
import { useDisplay } from "vuetify"
|
||||||
|
|
||||||
import HeaderActionsCard from "@/components/shared/cards/HeaderActionsCard.vue"
|
import HeaderActionsCard from "@/components/common/HeaderActionsCard.vue"
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
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"
|
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,12 @@ 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 +99,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
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
headline="Whoops, 403"
|
headline="Whoops, 403"
|
||||||
title="Access Forbidden"
|
title="Access Forbidden"
|
||||||
text="You do not have permission to access that page"
|
text="You do not have permission to access that page"
|
||||||
:image="SplashImage"
|
:image="AppLogoSplash"
|
||||||
>
|
>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col class="d-flex justify-center">
|
<v-col class="d-flex justify-center">
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { useAuth0 } from "@auth0/auth0-vue"
|
||||||
|
|
||||||
import { APPLICATION_NAME } from "@/config"
|
import { APPLICATION_NAME } from "@/config"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
headline="Whoops, 500"
|
headline="Whoops, 500"
|
||||||
title="Internal Server Error"
|
title="Internal Server Error"
|
||||||
:text="'Oops! The server encountered an unexpected error. Please\u00a0contact\u00a0support.'"
|
:text="'Oops! The server encountered an unexpected error. Please\u00a0contact\u00a0support.'"
|
||||||
:image="SplashImage"
|
:image="AppLogoSplash"
|
||||||
>
|
>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col class="d-flex justify-center">
|
<v-col class="d-flex justify-center">
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { useAuth0 } from "@auth0/auth0-vue"
|
||||||
|
|
||||||
import { APPLICATION_NAME } from "@/config"
|
import { APPLICATION_NAME } from "@/config"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
headline="Whoops, 404"
|
headline="Whoops, 404"
|
||||||
title="Page Not Found"
|
title="Page Not Found"
|
||||||
text="Oops! The page you're looking doesn't exist."
|
text="Oops! The page you're looking doesn't exist."
|
||||||
:image="SplashImage"
|
:image="AppLogoSplash"
|
||||||
>
|
>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col class="d-flex justify-center">
|
<v-col class="d-flex justify-center">
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { useAuth0 } from "@auth0/auth0-vue"
|
||||||
|
|
||||||
import { APPLICATION_NAME } from "@/config"
|
import { APPLICATION_NAME } from "@/config"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
headline="Whoops, 401"
|
headline="Whoops, 401"
|
||||||
title="Unauthorized"
|
title="Unauthorized"
|
||||||
text="If you think this is an error, please contact support. Alternatively, try logging out and signing back in."
|
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-row>
|
||||||
<v-col class="d-flex justify-center">
|
<v-col class="d-flex justify-center">
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { useAuth0 } from "@auth0/auth0-vue"
|
||||||
|
|
||||||
import { APPLICATION_NAME } from "@/config"
|
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(
|
watch(
|
||||||
() => unref(id),
|
() => unref(id),
|
||||||
async (newId) => {
|
async (newId) => {
|
||||||
@@ -106,7 +81,6 @@ export function useUser(id: Ref<number | null | undefined>) {
|
|||||||
fetch,
|
fetch,
|
||||||
refresh: fetch,
|
refresh: fetch,
|
||||||
save,
|
save,
|
||||||
directorySync,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||