feat(web): add lib/phone.ts with formatPhone/normalizePhone/zaloHref helpers

- Create apps/web/lib/phone.ts with VN_PHONE_REGEX, normalizePhone,
  formatPhone, and zaloHref helpers
- Deduplicate phone regex: auth.ts and inquiry.ts now import VN_PHONE_REGEX
  from @/lib/phone instead of defining their own local patterns
- Replace raw .replace(/^0/, '84') in inquiry-detail-dialog.tsx and
  lead-detail-dialog.tsx with zaloHref(); use formatPhone() for display

Resolves GOO-209

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-24 12:01:14 +07:00
parent d7c5b1ca2c
commit b4bb05479e
6 changed files with 102 additions and 13 deletions

59
apps/web/lib/phone.ts Normal file
View File

@@ -0,0 +1,59 @@
/**
* Vietnamese phone number helpers.
*
* Regex covers the current VN numbering plan:
* 0[35789]x xxxxxxx — Viettel, Mobifone, Vinaphone, Gmobile, Indochina
*
* See: https://en.wikipedia.org/wiki/Telephone_numbers_in_Vietnam
*/
/** Matches a VN mobile number, with optional +84 or leading 0. */
export const VN_PHONE_REGEX =
/^(0|\+84)(3[2-9]|5[2689]|7[06-9]|8[1-9]|9[0-9])\d{7}$/;
/**
* Normalise a VN phone number to the E.164-ish form used by Zalo / APIs:
* strip leading 0 and prepend the country code (84).
*
* "0987654321" → "84987654321"
* "+84987654321" → "84987654321"
* "84987654321" → "84987654321" (already normalised — idempotent)
*/
export function normalizePhone(phone: string): string {
const cleaned = phone.trim();
if (cleaned.startsWith('+84')) return `84${cleaned.slice(3)}`;
if (cleaned.startsWith('84')) return cleaned;
if (cleaned.startsWith('0')) return `84${cleaned.slice(1)}`;
return cleaned;
}
/**
* Format a raw VN phone number for display.
* Handles 10-digit numbers (0xx xxxx xxxx).
*
* "0987654321" → "0987 654 321"
* Passthrough for anything that doesn't match.
*/
export function formatPhone(phone: string): string {
const cleaned = phone.trim().replace(/\s+/g, '');
// 10-digit local format: 0xxx yyy zzz
const tenDigit = cleaned.match(/^(0\d{3})(\d{3})(\d{3})$/);
if (tenDigit) return `${tenDigit[1]} ${tenDigit[2]} ${tenDigit[3]}`;
// +84 prefix → treat as 10-digit local after swapping prefix
const e164 = cleaned.match(/^\+84(\d{9})$/);
if (e164) return formatPhone(`0${e164[1]}`);
return phone;
}
/**
* Returns the https://zalo.me deep-link URL for a given phone number.
*
* Zalo expects the number without a leading zero, prefixed with 84.
* "0987654321" → "https://zalo.me/84987654321"
*/
export function zaloHref(phone: string): string {
return `https://zalo.me/${normalizePhone(phone)}`;
}

View File

@@ -1,6 +1,6 @@
import { z } from 'zod';
const phoneRegex = /^(0|\+84)(3[2-9]|5[2689]|7[06-9]|8[1-9]|9[0-9])\d{7}$/;
import { VN_PHONE_REGEX as phoneRegex } from '@/lib/phone';
export const loginSchema = z.object({
phone: z

View File

@@ -1,12 +1,6 @@
import { z } from 'zod';
/**
* Vietnamese phone number rule:
* - 911 digits, optional leading +84 or 0.
* We keep validation pragmatic: whitespace is stripped, then the remaining
* string must be 911 digits (country code / leading zero stripped).
*/
const PHONE_REGEX = /^(?:\+?84|0)?\d{9,11}$/;
import { VN_PHONE_REGEX as PHONE_REGEX } from '@/lib/phone';
export const inquiryFormSchema = z.object({
message: z