Compare commits
7 Commits
029fbae471
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0099e51f94 | ||
|
|
901f7f4c02 | ||
|
|
7c3d34d02e | ||
|
|
0fc7118fd4 | ||
|
|
d61f391203 | ||
|
|
360244dc08 | ||
|
|
39accae46e |
1
.env
1
.env
@@ -1,3 +1,2 @@
|
||||
VITE_COUCHDB=https://couch.apoena.dev
|
||||
VITE_MAP_KEY=AnLDo_m_IGvMsQPLuBak9igWj3gNYvqBodj0esZZ7VfI1OkqVWvg04eTZ4U9R0Y2
|
||||
VITE_MAPBOX_KEY=pk.eyJ1IjoiamNhbGl4dGUiLCJhIjoiY2s3cmo1aHhlMDZ3dDNtc2Z0OXl3M3c5dSJ9.rkkIAZ4lKSJZssoFvH7Fpw
|
||||
|
||||
@@ -36,5 +36,4 @@ Create `.env` (gitignored) with:
|
||||
```
|
||||
VITE_COUCHDB=https://your-couchdb-host
|
||||
VITE_MAPBOX_KEY=pk.your-mapbox-token
|
||||
VITE_MAP_KEY=your-bing-maps-key
|
||||
```
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"dependencies": {
|
||||
"@tabler/icons-webfont": "^3.30.0",
|
||||
"@vueuse/core": "^12.0.0",
|
||||
"arktype": "^2.2.0",
|
||||
"axios": "^1.7.7",
|
||||
"date-fns": "^4.1.0",
|
||||
"events": "^3.3.0",
|
||||
|
||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
'@vueuse/core':
|
||||
specifier: ^12.0.0
|
||||
version: 12.8.2(typescript@5.9.3)
|
||||
arktype:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
axios:
|
||||
specifier: ^1.7.7
|
||||
version: 1.16.1
|
||||
@@ -153,6 +156,12 @@ packages:
|
||||
peerDependencies:
|
||||
ajv: '>=8'
|
||||
|
||||
'@ark/schema@0.56.0':
|
||||
resolution: {integrity: sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==}
|
||||
|
||||
'@ark/util@0.56.0':
|
||||
resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==}
|
||||
|
||||
'@babel/code-frame@7.29.7':
|
||||
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -2001,6 +2010,12 @@ packages:
|
||||
argsarray@0.0.1:
|
||||
resolution: {integrity: sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg==}
|
||||
|
||||
arkregex@0.0.5:
|
||||
resolution: {integrity: sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==}
|
||||
|
||||
arktype@2.2.0:
|
||||
resolution: {integrity: sha512-t54MZ7ti5BhOEvzEkgKnWvqj+UbDfWig+DHr5I34xatymPusKLS0lQpNJd8M6DzmIto2QGszHfNKoFIT8tMCZQ==}
|
||||
|
||||
array-buffer-byte-length@1.0.2:
|
||||
resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4718,6 +4733,12 @@ snapshots:
|
||||
jsonpointer: 5.0.1
|
||||
leven: 3.1.0
|
||||
|
||||
'@ark/schema@0.56.0':
|
||||
dependencies:
|
||||
'@ark/util': 0.56.0
|
||||
|
||||
'@ark/util@0.56.0': {}
|
||||
|
||||
'@babel/code-frame@7.29.7':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
@@ -6512,6 +6533,16 @@ snapshots:
|
||||
|
||||
argsarray@0.0.1: {}
|
||||
|
||||
arkregex@0.0.5:
|
||||
dependencies:
|
||||
'@ark/util': 0.56.0
|
||||
|
||||
arktype@2.2.0:
|
||||
dependencies:
|
||||
'@ark/schema': 0.56.0
|
||||
'@ark/util': 0.56.0
|
||||
arkregex: 0.0.5
|
||||
|
||||
array-buffer-byte-length@1.0.2:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue'
|
||||
import { useCurrencyInput } from 'vue-currency-input'
|
||||
import { useCurrencyInput, CurrencyDisplay } from 'vue-currency-input'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -23,7 +23,8 @@ const props = withDefaults(
|
||||
defineEmits<{ 'update:modelValue': [value: number | null] }>()
|
||||
|
||||
const { inputRef, setValue } = useCurrencyInput({
|
||||
currency: undefined as unknown as string,
|
||||
currency: 'USD',
|
||||
currencyDisplay: CurrencyDisplay.hidden,
|
||||
locale: props.locale,
|
||||
precision: props.precision,
|
||||
hideCurrencySymbolOnFocus: false,
|
||||
|
||||
@@ -14,11 +14,12 @@ import TransactionType from '@/enums/TransactionType'
|
||||
import TransactionTag, { TransactionTagLabel } from '@/enums/TransactionTag'
|
||||
import exchangeService from '@/services/ExchangeService'
|
||||
import transactionService from '@/services/TransactionService'
|
||||
import queueNotifService from '@/services/QueueNotifService'
|
||||
import mapService from '@/services/MapService'
|
||||
import { money } from '@/utils/format'
|
||||
import { findContrastColor } from '@/utils'
|
||||
import notif from '@/utils/notif'
|
||||
import { validateAndNotify } from '@/utils/validate'
|
||||
import { transactionFormSchema, MAX_TRANSACTION_AMOUNT } from '@/schemas/transaction'
|
||||
import FabButton from '@/components/FabButton.vue'
|
||||
import TransactionSplit from '@/components/TransactionSplit.vue'
|
||||
import TransactionTagUpdate from '@/components/TransactionTagUpdate.vue'
|
||||
@@ -46,7 +47,7 @@ const date = ref(today)
|
||||
const currency = ref<ICurrency | null>(null)
|
||||
const payBy = ref('')
|
||||
const payFor = reactive<ISplit[]>([])
|
||||
const maxAmount = 1_000_000
|
||||
const maxAmount = MAX_TRANSACTION_AMOUNT
|
||||
const displayLocationModal = ref(false)
|
||||
const isSetLocation = ref(false)
|
||||
const location = ref<ILocation | null>(null)
|
||||
@@ -120,23 +121,12 @@ const validLocation = async () => {
|
||||
const validate = (): boolean => {
|
||||
const el = document.querySelector(':focus') as HTMLInputElement | null
|
||||
el?.blur()
|
||||
if (!name.value || !amount.value || !payBy.value || (!currency.value && !date.value && payFor.length === 0)) {
|
||||
if (amount.value === 0) {
|
||||
queueNotifService.error('Le montant doit être supérieur à 0.')
|
||||
} else {
|
||||
queueNotifService.error('Tous les champs sont requis.')
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (isNaN(amount.value) || amount.value > maxAmount) {
|
||||
queueNotifService.error(`Veuillez saisir un montant numérique inférieur à ${money(maxAmount, currency.value)}.`)
|
||||
return false
|
||||
}
|
||||
if (amount.value < 0) {
|
||||
queueNotifService.error('Le montant doit être supérieur à 0.')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return validateAndNotify(
|
||||
transactionFormSchema,
|
||||
{ name: name.value, amount: amount.value, payBy: payBy.value },
|
||||
t,
|
||||
{ amount: { max: money(maxAmount, currency.value) } }
|
||||
)
|
||||
}
|
||||
|
||||
const submitTransaction = async (finish: () => void) => {
|
||||
|
||||
@@ -15,8 +15,6 @@ export default [
|
||||
{ name: 'Swiss franc', code: 'CHF' },
|
||||
{ name: 'Icelandic krona', code: 'ISK', symbol: 'kr' },
|
||||
{ name: 'Norwegian krone', code: 'NOK', symbol: 'øre' },
|
||||
{ name: 'Croatian kuna', code: 'HRK', symbol: 'kn' },
|
||||
{ name: 'Russian rouble', code: 'RUB' },
|
||||
{ name: 'Turkish lira', code: 'TRY' },
|
||||
{ name: 'Australian dollar', code: 'AUD', symbol: 'A$' },
|
||||
{ name: 'Brazilian real', code: 'BRL', symbol: 'R$' },
|
||||
|
||||
@@ -47,6 +47,20 @@ export default {
|
||||
},
|
||||
validation: {
|
||||
max_char:
|
||||
'no character available | 1 character max | {max} character maximum'
|
||||
'no character available | 1 character max | {max} character maximum',
|
||||
transaction_name_required: 'Transaction name is required.',
|
||||
amount_required: 'Amount is required.',
|
||||
amount_must_be_positive: 'Amount must be greater than 0.',
|
||||
amount_too_large: 'Amount must be less than {max}.',
|
||||
currency_required: 'Currency is required.',
|
||||
date_required: 'Date is required.',
|
||||
pay_by_required: 'Payer is required.',
|
||||
pay_for_required: 'At least one share must be assigned.',
|
||||
user_id_required: 'A username is required.',
|
||||
passwords_mismatch: 'Passwords must match.',
|
||||
account_name_required: 'Account name is required.',
|
||||
account_needs_one_user: 'At least one person must be added to the account.',
|
||||
aliases_must_be_unique: 'Aliases must be unique.',
|
||||
user_ids_must_be_unique: 'Usernames must be unique.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,20 @@ export default {
|
||||
pseudo: 'identifiant'
|
||||
},
|
||||
validation: {
|
||||
max_char: 'limite atteinte | 1 caractère maximum | {max} caractères maximum'
|
||||
max_char: 'limite atteinte | 1 caractère maximum | {max} caractères maximum',
|
||||
transaction_name_required: 'Le nom de la dépense est requis.',
|
||||
amount_required: 'Le montant est requis.',
|
||||
amount_must_be_positive: 'Le montant doit être supérieur à 0.',
|
||||
amount_too_large: 'Le montant doit être inférieur à {max}.',
|
||||
currency_required: 'La devise est requise.',
|
||||
date_required: 'La date est requise.',
|
||||
pay_by_required: 'Le payeur est requis.',
|
||||
pay_for_required: 'Au moins une part doit être attribuée.',
|
||||
user_id_required: 'Un pseudo est obligatoire.',
|
||||
passwords_mismatch: 'Les mots de passe doivent être identiques.',
|
||||
account_name_required: 'Le nom du compte est requis.',
|
||||
account_needs_one_user: 'Au moins une personne doit être ajoutée au compte.',
|
||||
aliases_must_be_unique: 'Les alias doivent être uniques.',
|
||||
user_ids_must_be_unique: 'Les identifiants doivent être uniques.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,11 @@
|
||||
export default interface ILocationQuery {
|
||||
authenticationResultCode: string
|
||||
brandLogoUri: string
|
||||
copyright: string
|
||||
resourceSets: [
|
||||
{
|
||||
estimatedTotal: number
|
||||
resources: [
|
||||
{
|
||||
__type: string
|
||||
bbox: number[]
|
||||
name: string
|
||||
point: {
|
||||
type: string
|
||||
coordinates: number[]
|
||||
}
|
||||
address: {
|
||||
addressLine: string
|
||||
adminDistrict: string
|
||||
adminDistrict2: string
|
||||
countryRegion: string
|
||||
formattedAddress: string
|
||||
locality: string
|
||||
postalCode: string
|
||||
}
|
||||
confidence: string
|
||||
entityType: string
|
||||
geocodePoints: [
|
||||
{
|
||||
type: string
|
||||
coordinates: number[]
|
||||
calculationMethod: string
|
||||
usageTypes: string[]
|
||||
}
|
||||
]
|
||||
matchCodes: string[]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
statusCode: number
|
||||
statusDescription: string
|
||||
traceId: string
|
||||
type: string
|
||||
features: Array<{
|
||||
id: string
|
||||
type: string
|
||||
place_type: string[]
|
||||
text: string
|
||||
place_name: string
|
||||
center: number[]
|
||||
}>
|
||||
}
|
||||
|
||||
20
src/schemas/account.ts
Normal file
20
src/schemas/account.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { type } from 'arktype'
|
||||
|
||||
export const accountFormSchema = type({
|
||||
name: 'unknown',
|
||||
aliases: 'unknown',
|
||||
userIds: 'unknown'
|
||||
}).narrow((data, ctx) => {
|
||||
if (typeof data.name !== 'string' || data.name.trim().length === 0) {
|
||||
ctx.reject({ message: 'validation.account_name_required', path: ['name'] })
|
||||
}
|
||||
if (!Array.isArray(data.aliases) || data.aliases.length === 0) {
|
||||
ctx.reject({ message: 'validation.account_needs_one_user', path: ['aliases'] })
|
||||
} else if (new Set(data.aliases).size !== data.aliases.length) {
|
||||
ctx.reject({ message: 'validation.aliases_must_be_unique', path: ['aliases'] })
|
||||
}
|
||||
if (Array.isArray(data.userIds) && new Set(data.userIds).size !== data.userIds.length) {
|
||||
ctx.reject({ message: 'validation.user_ids_must_be_unique', path: ['userIds'] })
|
||||
}
|
||||
return !ctx.hasError()
|
||||
})
|
||||
24
src/schemas/transaction.ts
Normal file
24
src/schemas/transaction.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { type } from 'arktype'
|
||||
|
||||
export const MAX_TRANSACTION_AMOUNT = 1_000_000
|
||||
|
||||
export const transactionFormSchema = type({
|
||||
name: 'unknown',
|
||||
amount: 'unknown',
|
||||
payBy: 'unknown'
|
||||
}).narrow((data, ctx) => {
|
||||
if (typeof data.name !== 'string' || data.name.trim().length === 0) {
|
||||
ctx.reject({ message: 'validation.transaction_name_required', path: ['name'] })
|
||||
}
|
||||
if (typeof data.amount !== 'number' || Number.isNaN(data.amount)) {
|
||||
ctx.reject({ message: 'validation.amount_required', path: ['amount'] })
|
||||
} else if (data.amount <= 0) {
|
||||
ctx.reject({ message: 'validation.amount_must_be_positive', path: ['amount'] })
|
||||
} else if (data.amount > MAX_TRANSACTION_AMOUNT) {
|
||||
ctx.reject({ message: 'validation.amount_too_large', path: ['amount'] })
|
||||
}
|
||||
if (typeof data.payBy !== 'string' || data.payBy.length === 0) {
|
||||
ctx.reject({ message: 'validation.pay_by_required', path: ['payBy'] })
|
||||
}
|
||||
return !ctx.hasError()
|
||||
})
|
||||
15
src/schemas/user.ts
Normal file
15
src/schemas/user.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { type } from 'arktype'
|
||||
|
||||
export const signupFormSchema = type({
|
||||
userId: 'unknown',
|
||||
password: 'unknown',
|
||||
confirmPassword: 'unknown'
|
||||
}).narrow((data, ctx) => {
|
||||
if (typeof data.userId !== 'string' || data.userId.trim().length === 0) {
|
||||
ctx.reject({ message: 'validation.user_id_required', path: ['userId'] })
|
||||
}
|
||||
if (data.password !== data.confirmPassword) {
|
||||
ctx.reject({ message: 'validation.passwords_mismatch', path: ['confirmPassword'] })
|
||||
}
|
||||
return !ctx.hasError()
|
||||
})
|
||||
@@ -11,11 +11,11 @@ PouchDb.plugin(PouchDbAuthentication)
|
||||
|
||||
const emit = debounce((ev: typeof SYNC, arr?: string[]) => bus.emit(ev, arr), 150)
|
||||
|
||||
const remoteConfig = {
|
||||
const remoteConfig: PouchDB.Configuration.RemoteDatabaseConfiguration = {
|
||||
skip_setup: true,
|
||||
ajax: {
|
||||
cache: true,
|
||||
withCredentials: true
|
||||
fetch: (url, opts) => {
|
||||
const init: RequestInit = { ...(opts ?? {}), credentials: 'include' }
|
||||
return PouchDb.fetch(url as RequestInfo, init)
|
||||
}
|
||||
}
|
||||
const LOCALE_DB = 'VAQUANT_LOCALE_DB'
|
||||
|
||||
@@ -3,7 +3,7 @@ import couchService from '@/services/CouchService'
|
||||
import formatDate from '@/utils/format-date'
|
||||
|
||||
class ExchangeService {
|
||||
private baseUrl: string = 'https://api.exchangeratesapi.io/'
|
||||
private baseUrl: string = 'https://api.frankfurter.dev/v1/'
|
||||
|
||||
/**
|
||||
* Get an exchange with defined date and desired currencies
|
||||
@@ -42,12 +42,16 @@ class ExchangeService {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const url = new URL(
|
||||
date ? `${this.baseUrl}${formattedDate}` : 'latest'
|
||||
)
|
||||
const url = new URL(`${this.baseUrl}${date ? formattedDate : 'latest'}`)
|
||||
if (base) {
|
||||
url.searchParams.append('base', base)
|
||||
}
|
||||
if (rates.length) {
|
||||
url.searchParams.append(
|
||||
'symbols',
|
||||
rates.filter((r) => r !== base).join(',')
|
||||
)
|
||||
}
|
||||
const response = await fetch(url.toString())
|
||||
if (response.ok) {
|
||||
exchange = await response.json()
|
||||
|
||||
@@ -2,7 +2,7 @@ import ILocation from '@/models/ILocation'
|
||||
import ILocationQuery from '@/models/ILocationQuery'
|
||||
|
||||
class MapService {
|
||||
private key: string = import.meta.env.VITE_MAP_KEY || ''
|
||||
private token: string = import.meta.env.VITE_MAPBOX_KEY || ''
|
||||
|
||||
public async getPosition(): Promise<ILocation | null> {
|
||||
const position = await this.getCurrentPosition()
|
||||
@@ -28,20 +28,11 @@ class MapService {
|
||||
return ''
|
||||
}
|
||||
const json: ILocationQuery = await result.json()
|
||||
if (
|
||||
!json ||
|
||||
!json.resourceSets ||
|
||||
!json.resourceSets.length ||
|
||||
!json.resourceSets[0].resources.length
|
||||
) {
|
||||
return ''
|
||||
}
|
||||
const address = json.resourceSets[0].resources[0].address
|
||||
return address ? address.locality : ''
|
||||
return json?.features?.[0]?.text ?? ''
|
||||
}
|
||||
|
||||
private url(latitude: number, longitude: number) {
|
||||
return `https://dev.virtualearth.net/REST/v1/Locations/${latitude},${longitude}?key=${this.key}`
|
||||
return `https://api.mapbox.com/geocoding/v5/mapbox.places/${longitude},${latitude}.json?types=place&limit=1&access_token=${this.token}`
|
||||
}
|
||||
|
||||
private getCurrentPosition(): Promise<GeolocationPosition | null> {
|
||||
|
||||
29
src/utils/validate.ts
Normal file
29
src/utils/validate.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ArkErrors } from 'arktype'
|
||||
import type { Type } from 'arktype'
|
||||
import type { Composer } from 'vue-i18n'
|
||||
import queueNotifService from '@/services/QueueNotifService'
|
||||
|
||||
type TranslateFn = Composer['t']
|
||||
type MessageParams = Record<string, unknown>
|
||||
export type ParamsByPath = Record<string, MessageParams>
|
||||
|
||||
export function validateAndNotify(
|
||||
schema: Type,
|
||||
data: unknown,
|
||||
t: TranslateFn,
|
||||
paramsByPath: ParamsByPath = {}
|
||||
): boolean {
|
||||
const result = schema(data)
|
||||
if (!(result instanceof ArkErrors)) return true
|
||||
|
||||
const seen = new Set<string>()
|
||||
for (const error of result) {
|
||||
const pathKey = error.path.join('.')
|
||||
// Skip duplicate field-level errors at the same path so the user gets one toast per field.
|
||||
if (seen.has(pathKey)) continue
|
||||
seen.add(pathKey)
|
||||
const params = paramsByPath[pathKey] ?? {}
|
||||
queueNotifService.error(t(error.message, params))
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import type IUser from '@/models/IUser'
|
||||
import userService from '@/services/UserService'
|
||||
import queueNotifService from '@/services/QueueNotifService'
|
||||
import { slug } from '@/utils'
|
||||
import { validateAndNotify } from '@/utils/validate'
|
||||
import { signupFormSchema } from '@/schemas/user'
|
||||
import LangChanger from '@/components/LangChanger.vue'
|
||||
import AccountList from '@/components/AccountList.vue'
|
||||
import ConfirmButton from '@/components/ConfirmButton.vue'
|
||||
@@ -51,17 +53,16 @@ const signin = async () => {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const validateRegistration = (): boolean => {
|
||||
if (!userId.value) {
|
||||
queueNotifService.error('Un pseudo est obligatoire.')
|
||||
return false
|
||||
}
|
||||
if (password.value !== confirmPassword.value) {
|
||||
queueNotifService.error('Les mots de passe doivent être identique.')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
const validateRegistration = (): boolean =>
|
||||
validateAndNotify(
|
||||
signupFormSchema,
|
||||
{
|
||||
userId: userId.value,
|
||||
password: password.value,
|
||||
confirmPassword: confirmPassword.value
|
||||
},
|
||||
t
|
||||
)
|
||||
|
||||
const register = async (confirmed: boolean) => {
|
||||
if (!validateRegistration()) return
|
||||
|
||||
@@ -11,6 +11,8 @@ import type IUser from '@/models/IUser'
|
||||
import accountService from '@/services/AccountService'
|
||||
import queueNotifService from '@/services/QueueNotifService'
|
||||
import { slug } from '@/utils'
|
||||
import { validateAndNotify } from '@/utils/validate'
|
||||
import { accountFormSchema } from '@/schemas/account'
|
||||
import ColorPicker from '@/components/ColorPicker.vue'
|
||||
import AccountUserNew from '@/components/AccountUserNew.vue'
|
||||
|
||||
@@ -55,27 +57,13 @@ watch(
|
||||
)
|
||||
|
||||
const validate = (allUsers: IUser[]): boolean => {
|
||||
if (!name.value) {
|
||||
queueNotifService.error('Le nom doit être rempli.')
|
||||
return false
|
||||
}
|
||||
const aliases = allUsers.map((u) => (u.alias ? u.alias.toLowerCase() : ''))
|
||||
if (aliases.length === 0) {
|
||||
queueNotifService.error('Au moins une personne doit être ajoutée au compte.')
|
||||
return false
|
||||
}
|
||||
if (aliases.length !== new Set(aliases).size) {
|
||||
queueNotifService.error('Les alias doivent être uniques.')
|
||||
return false
|
||||
}
|
||||
if (user.value) {
|
||||
const userIds = allUsers.map((u) => u.userId)
|
||||
if (userIds.length !== new Set(userIds).size) {
|
||||
queueNotifService.error('Les identifiants doivent être uniques.')
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
const userIds = user.value ? allUsers.map((u) => u.userId) : undefined
|
||||
return validateAndNotify(
|
||||
accountFormSchema,
|
||||
{ name: name.value, aliases, userIds },
|
||||
t
|
||||
)
|
||||
}
|
||||
|
||||
const submitAccount = async () => {
|
||||
|
||||
Reference in New Issue
Block a user