Files
scoreodonto.com/frontend/views/newwhats/utils/inboxUtils.ts
T
VPS 4 Builder 3042ddca38 feat(newwhats): frontend do plugin (Inbox/Sessions/Secretária) em tela cheia
- /wa-inbox, /wa-sessions e /wa-secretaria rodam sem a sidebar do scoreodonto
  (isStandaloneView); cada tela tem seu "Voltar" (Secretária ganhou botão na ThinNav).
- SessionsView: banner "Você já possui acesso ao WhatsApp" quando há sessão ativa.
- avatares: usa a URL do CDN (contactAvatarUrl/instance.avatar) direto; Avatar
  exibe sem depender de version; self-heal no onError re-busca a foto atual.
- deps: framer-motion, immer. Dev override com HMR (docker-compose.dev.yml).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 20:54:24 +02:00

100 lines
3.9 KiB
TypeScript

const getAccessToken = () => {
if (typeof window !== 'undefined') return localStorage.getItem('token') || '';
return '';
};
export const normalizeJid = (jid: string) => {
if (!jid) return '';
const [user] = jid.split('@');
const [cleanUser] = user.split(':');
return cleanUser;
};
export const formatPhone = (phone: string, remoteJid?: string): string => {
const raw = String(phone || '').trim();
const jid = String(remoteJid || '');
const cleaned = raw.replace(/\D/g, '');
// ── @lid — identificador interno do WhatsApp: exibe como +número ──────────
if (jid.endsWith('@lid')) {
const id = (normalizeJid(jid) || cleaned).replace(/\D/g, '');
return id ? `+${id}` : raw;
}
// ── Brasil (55) — 12 ou 13 dígitos ───────────────────────────────────────
if ((cleaned.length === 12 || cleaned.length === 13) && cleaned.startsWith('55')) {
return cleaned.replace(/^(\d{2})(\d{2})(\d{4,5})(\d{4})$/, '+$1 $2 $3-$4');
}
// ── EUA / Canadá (1) — 11 dígitos ────────────────────────────────────────
if (cleaned.length === 11 && cleaned.startsWith('1')) {
return cleaned.replace(/^(\d{1})(\d{3})(\d{3})(\d{4})$/, '+$1 ($2) $3-$4');
}
// ── Outros países — formato E.164 genérico ────────────────────────────────
if (cleaned.length >= 7 && cleaned.length <= 15) {
return `+${cleaned}`;
}
return raw || cleaned;
};
export const getAvatarInitials = (chat: any) => {
const name = chat.displayName || chat.subject || chat.phone || '';
return name.slice(0, 2).toUpperCase();
};
export const getAvatarColor = (id: string): string => {
let hash = 0;
const cleanId = String(id || 'anon');
for (let i = 0; i < cleanId.length; i++) {
hash = cleanId.charCodeAt(i) + ((hash << 5) - hash);
}
const h = Math.abs(hash % 360);
// HSL: S=65%, L=45% for a vibrant, accessible look with white text
return `hsl(${h}, 65%, 45%)`;
};
export const getLabelColor = (index: number) => {
const colors = [
'bg-gray-500', 'bg-red-500', 'bg-orange-500', 'bg-yellow-500',
'bg-green-500', 'bg-teal-500', 'bg-blue-500', 'bg-indigo-500',
'bg-purple-500', 'bg-pink-500'
];
return colors[index % colors.length] || 'bg-gray-500';
};
export const getMediaUrl = (url: string | null | undefined): string | null => {
if (!url) return null;
if (url.startsWith('http')) return url;
const apiBase = ''; // satélite: same-origin (URLs http/Wasabi já retornam acima)
// Path local (/media/...) — servido pelo Express static do backend
if (url.startsWith('/media/')) return `${apiBase}${url}`;
// Path Wasabi — proxiado pela rota /api/storage/view/ do backend
const clean = url.startsWith('/') ? url.slice(1) : url;
return `${apiBase}/api/storage/view/${clean}`;
};
export const getAvatarProxyUrl = (chat: any, _type: 'instance' | 'contact' = 'contact'): string | null => {
// Satélite: não há proxy de avatar exposto na ext API — retorna null e o
// componente Avatar cai para as iniciais (fallback limpo, sem 404s).
return null;
};
export const parseSafeDate = (timestamp: any): Date => {
if (!timestamp) return new Date();
let d = new Date(timestamp);
// If invalid and a number (or numeric string)
if (isNaN(d.getTime())) {
const num = Number(timestamp);
if (!isNaN(num)) {
// Unix seconds (Whats APIs often use this) vs ms
d = new Date(num > 10000000000 ? num : num * 1000);
}
}
return isNaN(d.getTime()) ? new Date() : d;
};
export default function UtilsPage() { return null; }