Files
pagodera/lib/utils/formatters.ts
T
2025-11-11 03:08:36 -03:00

494 lines
16 KiB
TypeScript

/**
* Funções utilitárias para formatação de dados
*/
/**
* Formata número de telefone brasileiro
* Exemplo: 11988887777 -> (11) 98888-7777
*/
export function formatPhone(value: string): string {
const cleaned = value.replace(/\D/g, "");
if (cleaned.length <= 2) {
return cleaned;
}
if (cleaned.length <= 6) {
return `(${cleaned.slice(0, 2)}) ${cleaned.slice(2)}`;
}
if (cleaned.length <= 10) {
return `(${cleaned.slice(0, 2)}) ${cleaned.slice(2, 6)}-${cleaned.slice(6)}`;
}
// Telefone com 11 dígitos (celular)
return `(${cleaned.slice(0, 2)}) ${cleaned.slice(2, 7)}-${cleaned.slice(7, 11)}`;
}
/**
* Formata número de telefone específico por país
* @param phoneNumber - Número do telefone (apenas dígitos, sem código de país)
* @param countryCode - Código ISO do país (ex: "BR", "US", "GB")
*/
function formatPhoneByCountry(
phoneNumber: string,
countryCode: string,
): string {
const len = phoneNumber.length;
switch (countryCode) {
// Brasil: (XX) XXXXX-XXXX ou (XX) XXXX-XXXX
case "BR":
if (len <= 2) return phoneNumber;
if (len <= 6)
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2)}`;
if (len <= 10)
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2, 6)}-${phoneNumber.slice(6)}`;
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2, 7)}-${phoneNumber.slice(7, 11)}`;
// EUA/Canadá: (XXX) XXX-XXXX
case "US":
case "CA":
if (len <= 3) return phoneNumber;
if (len <= 6)
return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3)}`;
return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6, 10)}`;
// México: (XX) XXXX XXXX
case "MX":
if (len <= 2) return phoneNumber;
if (len <= 6)
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2)}`;
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2, 6)} ${phoneNumber.slice(6, 10)}`;
// Argentina: (XX) XXXX-XXXX
case "AR":
if (len <= 2) return phoneNumber;
if (len <= 6)
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2)}`;
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2, 6)}-${phoneNumber.slice(6, 10)}`;
// Chile: X XXXX XXXX
case "CL":
if (len <= 1) return phoneNumber;
if (len <= 5) return `${phoneNumber.slice(0, 1)} ${phoneNumber.slice(1)}`;
return `${phoneNumber.slice(0, 1)} ${phoneNumber.slice(1, 5)} ${phoneNumber.slice(5, 9)}`;
// Colômbia: XXX XXX XXXX
case "CO":
if (len <= 3) return phoneNumber;
if (len <= 6) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6, 10)}`;
// Peru: XXX XXX XXX
case "PE":
if (len <= 3) return phoneNumber;
if (len <= 6) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6, 9)}`;
// Portugal: XXX XXX XXX
case "PT":
if (len <= 3) return phoneNumber;
if (len <= 6) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6, 9)}`;
// Espanha: XXX XXX XXX
case "ES":
if (len <= 3) return phoneNumber;
if (len <= 6) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6, 9)}`;
// Reino Unido: XXXX XXX XXXX
case "GB":
if (len <= 4) return phoneNumber;
if (len <= 7) return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4)}`;
return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4, 7)} ${phoneNumber.slice(7, 11)}`;
// França: XX XX XX XX XX
case "FR": {
if (len <= 2) return phoneNumber;
const groups: string[] = [];
for (let i = 0; i < len; i += 2) {
if (i + 2 <= len) {
groups.push(phoneNumber.slice(i, i + 2));
} else {
groups.push(phoneNumber.slice(i));
}
}
return groups.join(" ");
}
// Alemanha: XXXX XXXXXXX
case "DE":
if (len <= 4) return phoneNumber;
return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4)}`;
// Itália: XXX XXX XXXX
case "IT":
if (len <= 3) return phoneNumber;
if (len <= 6) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6, 10)}`;
// Holanda: XX XXXXXXXX
case "NL":
if (len <= 2) return phoneNumber;
return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2)}`;
// Bélgica: XXXX XX XX XX
case "BE":
if (len <= 4) return phoneNumber;
if (len <= 6) return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4)}`;
if (len <= 8)
return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4, 6)} ${phoneNumber.slice(6)}`;
return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4, 6)} ${phoneNumber.slice(6, 8)} ${phoneNumber.slice(8)}`;
// Suíça: XX XXX XX XX
case "CH":
if (len <= 2) return phoneNumber;
if (len <= 5) return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2)}`;
if (len <= 7)
return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2, 5)} ${phoneNumber.slice(5)}`;
return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2, 5)} ${phoneNumber.slice(5, 7)} ${phoneNumber.slice(7)}`;
// Áustria: XXXX XXXX
case "AT":
if (len <= 4) return phoneNumber;
return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4, 8)}`;
// Polônia: XXX XXX XXX
case "PL":
if (len <= 3) return phoneNumber;
if (len <= 6) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6, 9)}`;
// Rússia: XXX XXX-XX-XX
case "RU":
if (len <= 3) return phoneNumber;
if (len <= 6) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
if (len <= 8)
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6)}`;
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6, 8)}-${phoneNumber.slice(8)}`;
// China: XXX XXXX XXXX
case "CN":
if (len <= 3) return phoneNumber;
if (len <= 7) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 7)} ${phoneNumber.slice(7, 11)}`;
// Japão: XX-XXXX-XXXX
case "JP":
if (len <= 2) return phoneNumber;
if (len <= 6) return `${phoneNumber.slice(0, 2)}-${phoneNumber.slice(2)}`;
return `${phoneNumber.slice(0, 2)}-${phoneNumber.slice(2, 6)}-${phoneNumber.slice(6, 10)}`;
// Coreia do Sul: XXX-XXXX-XXXX
case "KR":
if (len <= 3) return phoneNumber;
if (len <= 7) return `${phoneNumber.slice(0, 3)}-${phoneNumber.slice(3)}`;
return `${phoneNumber.slice(0, 3)}-${phoneNumber.slice(3, 7)}-${phoneNumber.slice(7, 11)}`;
// Índia: XXXX XXX XXX
case "IN":
if (len <= 4) return phoneNumber;
if (len <= 7) return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4)}`;
return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4, 7)} ${phoneNumber.slice(7, 10)}`;
// Austrália: XXXX XXX XXX
case "AU":
if (len <= 4) return phoneNumber;
if (len <= 7) return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4)}`;
return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4, 7)} ${phoneNumber.slice(7, 10)}`;
// Nova Zelândia: XXXX XXX XXX
case "NZ":
if (len <= 4) return phoneNumber;
if (len <= 7) return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4)}`;
return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4, 7)} ${phoneNumber.slice(7, 10)}`;
// África do Sul: XX XXX XXXX
case "ZA":
if (len <= 2) return phoneNumber;
if (len <= 5) return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2)}`;
return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2, 5)} ${phoneNumber.slice(5, 9)}`;
// Turquia: XXX XXX XX XX
case "TR":
if (len <= 3) return phoneNumber;
if (len <= 6) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
if (len <= 8)
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6)}`;
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6, 8)} ${phoneNumber.slice(8)}`;
// Arábia Saudita: XX XXX XXXX
case "SA":
if (len <= 2) return phoneNumber;
if (len <= 5) return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2)}`;
return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2, 5)} ${phoneNumber.slice(5, 9)}`;
// Emirados Árabes: XX XXX XXXX
case "AE":
if (len <= 2) return phoneNumber;
if (len <= 5) return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2)}`;
return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2, 5)} ${phoneNumber.slice(5, 9)}`;
// Israel: XX-XXX-XXXX
case "IL":
if (len <= 2) return phoneNumber;
if (len <= 5) return `${phoneNumber.slice(0, 2)}-${phoneNumber.slice(2)}`;
return `${phoneNumber.slice(0, 2)}-${phoneNumber.slice(2, 5)}-${phoneNumber.slice(5, 9)}`;
// Uruguai: XXXX XXXX
case "UY":
if (len <= 4) return phoneNumber;
return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4, 8)}`;
// Paraguai: XXX XXX XXX
case "PY":
if (len <= 3) return phoneNumber;
if (len <= 6) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6, 9)}`;
// Bolívia: XXXX XXXX
case "BO":
if (len <= 4) return phoneNumber;
return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4, 8)}`;
// Venezuela: XXXX-XXXXXXX
case "VE":
if (len <= 4) return phoneNumber;
return `${phoneNumber.slice(0, 4)}-${phoneNumber.slice(4)}`;
// Equador: XX XXX XXXX
case "EC":
if (len <= 2) return phoneNumber;
if (len <= 5) return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2)}`;
return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2, 5)} ${phoneNumber.slice(5, 9)}`;
// Egito: XX XXXX XXXX
case "EG":
if (len <= 2) return phoneNumber;
if (len <= 6) return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2)}`;
return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2, 6)} ${phoneNumber.slice(6, 10)}`;
// Formatação genérica padrão (espaços a cada 3-4 dígitos)
default: {
if (len <= 4) return phoneNumber;
if (len <= 7) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
if (len <= 10)
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6)}`;
// Para números maiores, agrupa em grupos de 3-4 dígitos
const groups: string[] = [];
let remaining = phoneNumber;
while (remaining.length > 4) {
groups.push(remaining.slice(0, 3));
remaining = remaining.slice(3);
}
if (remaining.length > 0) {
groups.push(remaining);
}
return groups.join(" ");
}
}
}
/**
* Gera um placeholder de telefone baseado no formato do país
* @param countryCode - Código ISO do país (ex: "BR", "US", "GB")
* @returns Placeholder formatado (ex: "(11) 98888-7777")
*/
export function getPhonePlaceholder(countryCode: string): string {
// Números exemplo para cada país (apenas para mostrar o formato)
const examples: Record<string, string> = {
BR: "(11) 98888-7777",
US: "(415) 555-1234",
CA: "(416) 555-1234",
MX: "(55) 1234 5678",
AR: "(11) 1234-5678",
CL: "9 1234 5678",
CO: "300 123 4567",
PE: "987 654 321",
PT: "912 345 678",
ES: "612 345 678",
GB: "7123 456 789",
FR: "61 23 45 67 89",
DE: "3012 3456789",
IT: "321 234 5678",
NL: "61 23456789",
BE: "1234 56 78 90",
CH: "21 321 45 67",
AT: "1234 5678",
PL: "123 456 789",
RU: "495 123-45-67",
CN: "138 1234 5678",
JP: "90-1234-5678",
KR: "010-1234-5678",
IN: "9876 543 210",
AU: "0412 345 678",
NZ: "021 234 5678",
ZA: "82 123 4567",
TR: "532 123 45 67",
SA: "50 123 4567",
AE: "50 123 4567",
IL: "50-123-4567",
UY: "1234 5678",
PY: "981 234 567",
BO: "7123 4567",
VE: "2126-1234567",
EC: "99 123 4567",
EG: "10 1234 5678",
};
return examples[countryCode] || "123 456 7890";
}
/**
* Formata número de telefone internacional com código de país
* @param value - Número do telefone (com ou sem código de país)
* @param dialCode - Código de discagem internacional (ex: "+55")
* @param countryCode - Código ISO do país (ex: "BR")
*/
export function formatInternationalPhone(
value: string,
dialCode: string,
countryCode?: string,
): string {
const cleaned = value.replace(/\D/g, "");
// Remove o código de país se já estiver presente
let phoneNumber = cleaned;
const dialCodeDigits = dialCode.replace(/\D/g, "");
if (phoneNumber.startsWith(dialCodeDigits)) {
phoneNumber = phoneNumber.slice(dialCodeDigits.length);
}
// Se não temos código de país, usar formatação genérica
if (!countryCode) {
return formatPhoneByCountry(phoneNumber, "DEFAULT");
}
// Aplicar formatação específica do país
return formatPhoneByCountry(phoneNumber, countryCode);
}
/**
* Remove formatação do telefone
* Exemplo: (11) 98888-7777 -> 11988887777
*/
export function unformatPhone(value: string): string {
return value.replace(/\D/g, "");
}
/**
* Combina código de país com número de telefone
* @param dialCode - Código de discagem (ex: "+55")
* @param phoneNumber - Número do telefone (sem código)
*/
export function combinePhoneWithDialCode(
dialCode: string,
phoneNumber: string,
): string {
const cleanedPhone = unformatPhone(phoneNumber);
const cleanedDialCode = dialCode.replace(/\D/g, "");
// Remove código de país se já estiver presente
if (cleanedPhone.startsWith(cleanedDialCode)) {
return `${dialCode}${cleanedPhone.slice(cleanedDialCode.length)}`;
}
return `${dialCode}${cleanedPhone}`;
}
/**
* Calcula a força da senha
* Retorna: 'weak' | 'medium' | 'strong'
*/
export function checkPasswordStrength(
password: string,
): "weak" | "medium" | "strong" {
if (password.length < 8) {
return "weak";
}
let strength = 0;
// Verificar requisitos básicos
if (password.length >= 8) strength++;
if (password.length >= 12) strength++;
if (/[a-z]/.test(password)) strength++;
if (/[A-Z]/.test(password)) strength++;
if (/[0-9]/.test(password)) strength++;
if (/[^a-zA-Z0-9]/.test(password)) strength++; // Caracteres especiais
if (strength <= 3) {
return "weak";
} else if (strength <= 5) {
return "medium";
} else {
return "strong";
}
}
/**
* Valida se a senha atende aos requisitos mínimos
*/
export function validatePasswordRequirements(password: string): {
isValid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (password.length < 8) {
errors.push("Senha deve ter no mínimo 8 caracteres");
}
if (!/[A-Z]/.test(password)) {
errors.push("Senha deve conter pelo menos 1 letra maiúscula");
}
if (!/[a-z]/.test(password)) {
errors.push("Senha deve conter pelo menos 1 letra minúscula");
}
if (!/[0-9]/.test(password)) {
errors.push("Senha deve conter pelo menos 1 número");
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* Mapeia o locale da aplicação (ex: "pt", "en") para um BCP 47 válido
* para APIs de formatação (ex: "pt-BR", "en-US").
*/
export function resolveBCP47Locale(locale?: string): string {
switch (locale) {
case "pt":
return "pt-BR";
case "en":
return "en-US";
default:
// Fallback consistente para evitar divergência SSR/cliente
return "pt-BR";
}
}
/**
* Formata números com locale consistente, evitando mismatch de hidratação
* entre SSR e cliente.
*/
export function formatNumber(value: number, locale?: string): string {
try {
return value.toLocaleString(resolveBCP47Locale(locale));
} catch {
return String(value);
}
}