diff --git a/app/components/sites/SiteCard.vue b/app/components/sites/SiteCard.vue index 9e13a9f..413d067 100644 --- a/app/components/sites/SiteCard.vue +++ b/app/components/sites/SiteCard.vue @@ -18,7 +18,7 @@
@@ -53,6 +53,7 @@ import IconLucideFactory from '~icons/lucide/factory' import IconLucideMapPin from '~icons/lucide/map-pin' import IconLucidePhone from '~icons/lucide/phone' import IconLucideUser from '~icons/lucide/user' +import { formatPhone } from '~/utils/formatters/phone' const props = defineProps({ site: { @@ -64,4 +65,9 @@ const props = defineProps({ const emit = defineEmits(['edit', 'delete']) const machineCount = computed(() => props.site?.machines?.length || 0) +const formattedContactPhone = computed(() => { + const value = props.site?.contactPhone ?? '' + const formatted = formatPhone(value) + return formatted || value || '—' +}) diff --git a/app/pages/constructeurs.vue b/app/pages/constructeurs.vue index bb68f54..f9c24a6 100644 --- a/app/pages/constructeurs.vue +++ b/app/pages/constructeurs.vue @@ -69,7 +69,7 @@ {{ constructeur.name }} {{ constructeur.email || '—' }} - {{ constructeur.phone || '—' }} + {{ formatPhoneDisplay(constructeur.phone) }}
{ }, 0) }) +const formatPhoneDisplay = (value) => { + const formatted = formatPhone(value) + if (formatted) { + return formatted + } + return value || '—' +} + const filteredSites = computed(() => { let filtered = sites.value diff --git a/app/shared/constructeurUtils.ts b/app/shared/constructeurUtils.ts index e9adb6b..02649ed 100644 --- a/app/shared/constructeurUtils.ts +++ b/app/shared/constructeurUtils.ts @@ -1,3 +1,5 @@ +import { formatPhone } from '~/utils/formatters/phone'; + export interface ConstructeurSummary { id: string; name?: string | null; @@ -92,8 +94,16 @@ export const resolveConstructeurs = ( export const formatConstructeurContact = ( constructeur?: ConstructeurSummary | null, -): string => - [constructeur?.email, constructeur?.phone].filter(Boolean).join(' • '); +): string => { + if (!constructeur) { + return ''; + } + + const formattedPhone = formatPhone(constructeur.phone); + const phone = formattedPhone || constructeur.phone || null; + + return [constructeur.email, phone].filter(Boolean).join(' • '); +}; export const buildConstructeurRequestPayload = >( payload: T, diff --git a/app/shared/validation/email.ts b/app/shared/validation/email.ts index 19b0f97..f2ea100 100644 --- a/app/shared/validation/email.ts +++ b/app/shared/validation/email.ts @@ -1,9 +1,9 @@ import { normalizeEmail } from '~/utils/formatters/email' -export const EMAIL_INPUT_PATTERN = '[^\s@]+' - const EMAIL_VALIDATION_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ +export const EMAIL_INPUT_PATTERN = EMAIL_VALIDATION_PATTERN.source + export type EmailValidationResult = { valid: boolean error?: string diff --git a/app/shared/validation/phone.ts b/app/shared/validation/phone.ts index a7577b2..b656b13 100644 --- a/app/shared/validation/phone.ts +++ b/app/shared/validation/phone.ts @@ -1,7 +1,7 @@ import { normalizePhone } from '~/utils/formatters/phone' /** Pattern used for the HTML input `pattern` attribute on phone fields. */ -export const PHONE_INPUT_PATTERN = '[0-9+ ]*' +export const PHONE_INPUT_PATTERN = '[0-9+ .]*' const PHONE_VALIDATION_PATTERN = /^\+?\d{7,15}$/ diff --git a/app/utils/formatters/phone.ts b/app/utils/formatters/phone.ts index 2eb4a27..fce3868 100644 --- a/app/utils/formatters/phone.ts +++ b/app/utils/formatters/phone.ts @@ -11,8 +11,8 @@ const PHONE_CHAR_PATTERN = /[^+\d]/g * Normalises a phone number by trimming whitespace, removing spacing/separators and * converting international prefixes written with `00` to their `+` variant. */ -export const normalizePhone = (rawValue: string): string => { - const trimmed = (rawValue || '').trim() +export const normalizePhone = (rawValue: string | null | undefined): string => { + const trimmed = typeof rawValue === 'string' ? rawValue.trim() : '' if (!trimmed) { return '' } @@ -26,30 +26,57 @@ export const normalizePhone = (rawValue: string): string => { } /** - * Formats a phone number by grouping digits by two while keeping any international - * prefix. The function remains tolerant to partially entered numbers. + * Formats a phone number by grouping digits by two and joining them with dots while + * keeping any international prefix. The function remains tolerant to partially + * entered numbers and returns an empty string for nullish inputs. */ -export const formatPhone = (rawValue: string): string => { +export const formatPhone = (rawValue: string | null | undefined): string => { + if (rawValue == null) { + return '' + } + const normalized = normalizePhone(rawValue) if (!normalized) { return '' } + if (normalized.startsWith('+33')) { + let nationalNumber = normalized.slice(3) + if (nationalNumber.startsWith('0')) { + nationalNumber = nationalNumber.slice(1) + } + + if (nationalNumber.length % 2 !== 0) { + nationalNumber = `0${nationalNumber}` + } + + const groups = nationalNumber.match(/\d{1,2}/g) ?? [] + if (groups.length === 0) { + return '+33' + } + + return ['+33', ...groups].join('.') + } + + if (/^0\d{9}$/.test(normalized)) { + return normalized + } + const hasInternationalPrefix = normalized.startsWith('+') const prefix = hasInternationalPrefix ? normalized.slice(0, 1) : '' const digits = hasInternationalPrefix ? normalized.slice(1) : normalized - const groups = digits.match(/.{1,2}/g) ?? [] - const grouped = groups.join(' ') + const groups = digits.match(/\d{1,2}/g) ?? [] + const grouped = groups.join('.') - return prefix ? `${prefix}${grouped ? ' ' : ''}${grouped}` : grouped + return prefix ? `${prefix}${grouped}` : grouped } /** * Masks a phone number for display purposes by replacing the middle digits with ·. * Useful for UI fragments where the full number should not be exposed. */ -export const maskPhone = (rawValue: string): string => { +export const maskPhone = (rawValue: string | null | undefined): string => { const normalized = normalizePhone(rawValue) if (!normalized) { return '' diff --git a/app/utils/printTemplates/machineReport.js b/app/utils/printTemplates/machineReport.js index 08f75ef..5e0077e 100644 --- a/app/utils/printTemplates/machineReport.js +++ b/app/utils/printTemplates/machineReport.js @@ -1,6 +1,7 @@ import { uniqueConstructeurIds, resolveConstructeurs, + formatConstructeurContact, } from '~/shared/constructeurUtils' const formatSize = (size) => { @@ -202,10 +203,11 @@ const normalizeCustomFields = (values = []) => { const normalizeConstructeur = (constructeur) => { if (!constructeur) { return null } + const contact = formatConstructeurContact(constructeur) return { id: constructeur.id || null, name: constructeur.name || '—', - contact: [constructeur.email, constructeur.phone].filter(Boolean).join(' • ') || '—' + contact: contact || '—' } }