feat: standardize contact formatting
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<div class="flex items-center gap-2 text-gray-600">
|
<div class="flex items-center gap-2 text-gray-600">
|
||||||
<IconLucidePhone class="w-4 h-4 text-secondary" aria-hidden="true" />
|
<IconLucidePhone class="w-4 h-4 text-secondary" aria-hidden="true" />
|
||||||
<span>{{ site.contactPhone }}</span>
|
<span>{{ formattedContactPhone }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-start gap-2 text-gray-600">
|
<div class="flex items-start gap-2 text-gray-600">
|
||||||
@@ -53,6 +53,7 @@ import IconLucideFactory from '~icons/lucide/factory'
|
|||||||
import IconLucideMapPin from '~icons/lucide/map-pin'
|
import IconLucideMapPin from '~icons/lucide/map-pin'
|
||||||
import IconLucidePhone from '~icons/lucide/phone'
|
import IconLucidePhone from '~icons/lucide/phone'
|
||||||
import IconLucideUser from '~icons/lucide/user'
|
import IconLucideUser from '~icons/lucide/user'
|
||||||
|
import { formatPhone } from '~/utils/formatters/phone'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
site: {
|
site: {
|
||||||
@@ -64,4 +65,9 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['edit', 'delete'])
|
const emit = defineEmits(['edit', 'delete'])
|
||||||
|
|
||||||
const machineCount = computed(() => props.site?.machines?.length || 0)
|
const machineCount = computed(() => props.site?.machines?.length || 0)
|
||||||
|
const formattedContactPhone = computed(() => {
|
||||||
|
const value = props.site?.contactPhone ?? ''
|
||||||
|
const formatted = formatPhone(value)
|
||||||
|
return formatted || value || '—'
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<tr v-for="constructeur in filteredConstructeurs" :key="constructeur.id" class="text-sm">
|
<tr v-for="constructeur in filteredConstructeurs" :key="constructeur.id" class="text-sm">
|
||||||
<td>{{ constructeur.name }}</td>
|
<td>{{ constructeur.name }}</td>
|
||||||
<td>{{ constructeur.email || '—' }}</td>
|
<td>{{ constructeur.email || '—' }}</td>
|
||||||
<td>{{ constructeur.phone || '—' }}</td>
|
<td>{{ formatPhoneDisplay(constructeur.phone) }}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<button class="btn btn-ghost btn-xs" @click="openEditModal(constructeur)">
|
<button class="btn btn-ghost btn-xs" @click="openEditModal(constructeur)">
|
||||||
@@ -122,6 +122,7 @@ import FieldEmail from '~/components/form/FieldEmail.vue'
|
|||||||
import FieldPhone from '~/components/form/FieldPhone.vue'
|
import FieldPhone from '~/components/form/FieldPhone.vue'
|
||||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
import { formatPhone } from '~/utils/formatters/phone'
|
||||||
import IconLucidePlus from '~icons/lucide/plus'
|
import IconLucidePlus from '~icons/lucide/plus'
|
||||||
|
|
||||||
const { constructeurs, loading, searchConstructeurs, createConstructeur, updateConstructeur, deleteConstructeur, loadConstructeurs } = useConstructeurs()
|
const { constructeurs, loading, searchConstructeurs, createConstructeur, updateConstructeur, deleteConstructeur, loadConstructeurs } = useConstructeurs()
|
||||||
@@ -150,6 +151,14 @@ const debouncedSearch = debounce(async () => {
|
|||||||
await searchConstructeurs(searchTerm.value)
|
await searchConstructeurs(searchTerm.value)
|
||||||
}, 300)
|
}, 300)
|
||||||
|
|
||||||
|
const formatPhoneDisplay = (value) => {
|
||||||
|
const formatted = formatPhone(value)
|
||||||
|
if (formatted) {
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
return value || '—'
|
||||||
|
}
|
||||||
|
|
||||||
function debounce (fn, delay) {
|
function debounce (fn, delay) {
|
||||||
let timeout
|
let timeout
|
||||||
return (...args) => {
|
return (...args) => {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
class="w-4 h-4 text-secondary"
|
class="w-4 h-4 text-secondary"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span>{{ site.contactPhone }}</span>
|
<span>{{ formatPhoneDisplay(site.contactPhone) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<IconLucideMapPinned
|
<IconLucideMapPinned
|
||||||
@@ -465,6 +465,7 @@ import IconLucideMapPinned from '~icons/lucide/map-pinned'
|
|||||||
import IconLucideChevronDown from '~icons/lucide/chevron-down'
|
import IconLucideChevronDown from '~icons/lucide/chevron-down'
|
||||||
import IconLucideSettings2 from '~icons/lucide/settings-2'
|
import IconLucideSettings2 from '~icons/lucide/settings-2'
|
||||||
import IconLucideTag from '~icons/lucide/tag'
|
import IconLucideTag from '~icons/lucide/tag'
|
||||||
|
import { formatPhone } from '~/utils/formatters/phone'
|
||||||
|
|
||||||
const { sites, loading, loadSites, createSite } = useSites()
|
const { sites, loading, loadSites, createSite } = useSites()
|
||||||
const { machineTypes, loadMachineTypes } = useMachineTypesApi()
|
const { machineTypes, loadMachineTypes } = useMachineTypesApi()
|
||||||
@@ -516,6 +517,14 @@ const totalMachines = computed(() => {
|
|||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const formatPhoneDisplay = (value) => {
|
||||||
|
const formatted = formatPhone(value)
|
||||||
|
if (formatted) {
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
return value || '—'
|
||||||
|
}
|
||||||
|
|
||||||
const filteredSites = computed(() => {
|
const filteredSites = computed(() => {
|
||||||
let filtered = sites.value
|
let filtered = sites.value
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { formatPhone } from '~/utils/formatters/phone';
|
||||||
|
|
||||||
export interface ConstructeurSummary {
|
export interface ConstructeurSummary {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
@@ -92,8 +94,16 @@ export const resolveConstructeurs = (
|
|||||||
|
|
||||||
export const formatConstructeurContact = (
|
export const formatConstructeurContact = (
|
||||||
constructeur?: ConstructeurSummary | null,
|
constructeur?: ConstructeurSummary | null,
|
||||||
): string =>
|
): string => {
|
||||||
[constructeur?.email, constructeur?.phone].filter(Boolean).join(' • ');
|
if (!constructeur) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedPhone = formatPhone(constructeur.phone);
|
||||||
|
const phone = formattedPhone || constructeur.phone || null;
|
||||||
|
|
||||||
|
return [constructeur.email, phone].filter(Boolean).join(' • ');
|
||||||
|
};
|
||||||
|
|
||||||
export const buildConstructeurRequestPayload = <T extends Record<string, any>>(
|
export const buildConstructeurRequestPayload = <T extends Record<string, any>>(
|
||||||
payload: T,
|
payload: T,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { normalizeEmail } from '~/utils/formatters/email'
|
import { normalizeEmail } from '~/utils/formatters/email'
|
||||||
|
|
||||||
export const EMAIL_INPUT_PATTERN = '[^\s@]+'
|
|
||||||
|
|
||||||
const EMAIL_VALIDATION_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
const EMAIL_VALIDATION_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
|
||||||
|
export const EMAIL_INPUT_PATTERN = EMAIL_VALIDATION_PATTERN.source
|
||||||
|
|
||||||
export type EmailValidationResult = {
|
export type EmailValidationResult = {
|
||||||
valid: boolean
|
valid: boolean
|
||||||
error?: string
|
error?: string
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { normalizePhone } from '~/utils/formatters/phone'
|
import { normalizePhone } from '~/utils/formatters/phone'
|
||||||
|
|
||||||
/** Pattern used for the HTML input `pattern` attribute on phone fields. */
|
/** 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}$/
|
const PHONE_VALIDATION_PATTERN = /^\+?\d{7,15}$/
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ const PHONE_CHAR_PATTERN = /[^+\d]/g
|
|||||||
* Normalises a phone number by trimming whitespace, removing spacing/separators and
|
* Normalises a phone number by trimming whitespace, removing spacing/separators and
|
||||||
* converting international prefixes written with `00` to their `+` variant.
|
* converting international prefixes written with `00` to their `+` variant.
|
||||||
*/
|
*/
|
||||||
export const normalizePhone = (rawValue: string): string => {
|
export const normalizePhone = (rawValue: string | null | undefined): string => {
|
||||||
const trimmed = (rawValue || '').trim()
|
const trimmed = typeof rawValue === 'string' ? rawValue.trim() : ''
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -26,30 +26,57 @@ export const normalizePhone = (rawValue: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a phone number by grouping digits by two while keeping any international
|
* Formats a phone number by grouping digits by two and joining them with dots while
|
||||||
* prefix. The function remains tolerant to partially entered numbers.
|
* 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)
|
const normalized = normalizePhone(rawValue)
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return ''
|
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 hasInternationalPrefix = normalized.startsWith('+')
|
||||||
const prefix = hasInternationalPrefix ? normalized.slice(0, 1) : ''
|
const prefix = hasInternationalPrefix ? normalized.slice(0, 1) : ''
|
||||||
const digits = hasInternationalPrefix ? normalized.slice(1) : normalized
|
const digits = hasInternationalPrefix ? normalized.slice(1) : normalized
|
||||||
|
|
||||||
const groups = digits.match(/.{1,2}/g) ?? []
|
const groups = digits.match(/\d{1,2}/g) ?? []
|
||||||
const grouped = groups.join(' ')
|
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 ·.
|
* 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.
|
* 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)
|
const normalized = normalizePhone(rawValue)
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
uniqueConstructeurIds,
|
uniqueConstructeurIds,
|
||||||
resolveConstructeurs,
|
resolveConstructeurs,
|
||||||
|
formatConstructeurContact,
|
||||||
} from '~/shared/constructeurUtils'
|
} from '~/shared/constructeurUtils'
|
||||||
|
|
||||||
const formatSize = (size) => {
|
const formatSize = (size) => {
|
||||||
@@ -202,10 +203,11 @@ const normalizeCustomFields = (values = []) => {
|
|||||||
|
|
||||||
const normalizeConstructeur = (constructeur) => {
|
const normalizeConstructeur = (constructeur) => {
|
||||||
if (!constructeur) { return null }
|
if (!constructeur) { return null }
|
||||||
|
const contact = formatConstructeurContact(constructeur)
|
||||||
return {
|
return {
|
||||||
id: constructeur.id || null,
|
id: constructeur.id || null,
|
||||||
name: constructeur.name || '—',
|
name: constructeur.name || '—',
|
||||||
contact: [constructeur.email, constructeur.phone].filter(Boolean).join(' • ') || '—'
|
contact: contact || '—'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user