21bebd3256
- Drawer de WhatsApp focado (WhatsChatDrawer) aberto do slot do agendamento, com dropdown de números do paciente + grupo familiar e fluxo de iniciar conversa - Auto-provisionamento de conta no motor: eager no cadastro/convite + self-heal no proxy (404 "conta não encontrada" → provisiona e refaz o request) - Ownership por sessão (wa_session_authz): dono do workspace (owner_id ou vínculo donoclinica/donoconsultorio) tem poder total; delega re-scan, excluir sessão, apagar conversa/mensagem, acesso ao Inbox e à Secretária — por conta - Enforcement no proxy: guardSessionAction (scan/rescan/delete sessão), guardAreaAccess (inbox/secretaria), guardInboxDestructive (apagar conversa/msg) - Endpoints /api/nw/authz (dono gerencia) e /api/nw/access (acesso agregado) - Assinatura do operador nas mensagens (*Nome*, desambiguação de homônimos → *Rui C.*) - UI: painel "Gerenciar acesso" com 5 toggles; menu e botões de apagar condicionados - Proxy de mídia /api/nw/media + re-download sob demanda Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
4.0 KiB
TypeScript
101 lines
4.0 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;
|
|
// URL absoluta (ex.: Wasabi/CDN público) → usa direto, como o motor exibe.
|
|
if (url.startsWith('http')) return url;
|
|
// Mídia do WhatsApp vive no MOTOR (path /media/<instância>/<arquivo>, servido
|
|
// do Wasabi/local). O satélite proxia em /api/nw/media/* (same-origin, sem auth
|
|
// no browser) — assim exibe igual ao motor, direto do Wasabi.
|
|
if (url.startsWith('/media/')) return `/api/nw/media/${url.slice('/media/'.length)}`;
|
|
const clean = url.startsWith('/') ? url.slice(1) : url;
|
|
return `/api/nw/media/${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; }
|