diff --git a/src/messages/en.ts b/src/messages/en.ts index 61bf7e4..d87def5 100644 --- a/src/messages/en.ts +++ b/src/messages/en.ts @@ -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.' } } diff --git a/src/messages/fr.ts b/src/messages/fr.ts index be53fbe..7262c79 100644 --- a/src/messages/fr.ts +++ b/src/messages/fr.ts @@ -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.' } } diff --git a/src/schemas/account.ts b/src/schemas/account.ts new file mode 100644 index 0000000..73b430c --- /dev/null +++ b/src/schemas/account.ts @@ -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() +}) diff --git a/src/schemas/transaction.ts b/src/schemas/transaction.ts new file mode 100644 index 0000000..1b3f1a4 --- /dev/null +++ b/src/schemas/transaction.ts @@ -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() +}) diff --git a/src/schemas/user.ts b/src/schemas/user.ts new file mode 100644 index 0000000..10af7cb --- /dev/null +++ b/src/schemas/user.ts @@ -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() +}) diff --git a/src/utils/validate.ts b/src/utils/validate.ts new file mode 100644 index 0000000..b48e38c --- /dev/null +++ b/src/utils/validate.ts @@ -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 +export type ParamsByPath = Record + +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() + 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 +}