refactor(forms): use arktype schemas for form validation

Replaces imperative validate() blocks in TransactionCreate, signup,
and AccountNew with schema-driven validation. Drops the dead
"(!currency && !date && payFor.length === 0)" branch in
TransactionCreate that was hiding behind the generic error.
This commit is contained in:
Julien Calixte
2026-06-01 22:17:56 +02:00
parent 0fc7118fd4
commit 7c3d34d02e
3 changed files with 29 additions and 50 deletions

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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 () => {