| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- <script>
- import { onMount } from 'svelte';
- import { Settings, Phone, Bell, HelpCircle, ShieldCheck, LoaderCircle, RefreshCw, ExternalLink } from 'lucide-svelte';
- import SlaConfigManager from '$lib/features/sla/SlaConfigManager.svelte';
- import {
- listWhatsappAccounts,
- createWhatsappHostedLink,
- reconnectWhatsappAccount
- } from '$lib/features/integrations/unipile/api.js';
- // Settings sections
- const staticSettingsSections = [
- {
- title: 'Notificações',
- icon: Bell,
- items: [
- {
- title: 'Alertas de Sentimento',
- description: 'Receba alertas sobre mudanças no sentimento dos clientes',
- status: 'enabled',
- action: 'configurar'
- },
- {
- title: 'Relatórios Diários',
- description: 'Receba relatórios automáticos por e-mail',
- status: 'disabled',
- action: 'ativar'
- }
- ]
- },
- {
- title: 'Suporte',
- icon: HelpCircle,
- items: [
- {
- title: 'Central de Ajuda',
- description: 'Acesse nossa documentação e tutoriais',
- status: 'available',
- action: 'acessar'
- },
- {
- title: 'Contato',
- description: 'Fale com nossa equipe de suporte',
- status: 'available',
- action: 'abrir chamado'
- }
- ]
- }
- ];
- const MAX_WHATSAPP_POLL_ATTEMPTS = 12;
- let whatsappAccounts = $state([]);
- let whatsappLoading = $state(true);
- let whatsappLoadError = $state('');
- let whatsappActionError = $state('');
- let whatsappOpening = $state(false);
- let whatsappRefreshing = $state(false);
- let whatsappPolling = $state(false);
- let lastHostedAuthUrl = $state('');
- let whatsappPollInterval = $state(null);
- let whatsappPollTimeout = $state(null);
- let whatsappPollAttempts = $state(0);
- const primaryWhatsappAccount = $derived(
- whatsappAccounts.find((account) => account?.isConnected) ?? whatsappAccounts[0] ?? null
- );
- const settingsSections = $derived([
- {
- title: 'Integrações',
- icon: Phone,
- // "WhatsApp dos Vendedores" (suporte multiconta: um número por vendedor) ainda não
- // foi implementado — o botão "configurar" não fazia nada e o "3 de 5" era texto fixo.
- // Comentado até o cliente decidir aderir. Para reativar: adicione o objeto abaixo de
- // volta ao array `items` e implemente o ramo 'whatsapp-sellers' em handleAction().
- // {
- // key: 'whatsapp-sellers',
- // title: 'WhatsApp dos Vendedores',
- // description: 'Adicione os números de WhatsApp da equipe de vendas',
- // status: 'partial',
- // action: 'configurar',
- // count: '3 de 5 configurados'
- // }
- items: [buildWhatsappItem()]
- },
- ...staticSettingsSections
- ]);
- function handleAction(item) {
- // Handle different actions based on item
- if (item.key === 'whatsapp-business') {
- void handleWhatsappPrimaryAction();
- return;
- }
-
- // For WhatsApp integration, we could open a modal or navigate to a specific page
- if (item.key === 'whatsapp-business') {
- // TODO: Open WhatsApp configuration modal
- } else if (item.key === 'whatsapp-sellers') {
- // TODO: Open sellers WhatsApp configuration
- }
- }
- function handleSecondaryAction(item) {
- if (item.key === 'whatsapp-business') {
- void refreshWhatsappAccounts();
- }
- }
- function getWhatsappStatus() {
- if (whatsappLoading) return 'loading';
- if (whatsappOpening || whatsappPolling) return 'connecting';
- if (whatsappLoadError || whatsappActionError) return 'error';
- if (primaryWhatsappAccount?.isConnected) return 'connected';
- if (primaryWhatsappAccount) return 'partial';
- return 'disabled';
- }
- function buildWhatsappItem() {
- const connectedAccounts = whatsappAccounts.filter((account) => account?.isConnected);
- const status = getWhatsappStatus();
- const hasAccount = primaryWhatsappAccount !== null;
- const action = hasAccount ? 'reconectar' : 'conectar';
- const description = whatsappLoading
- ? 'Consultando o status da integração WhatsApp.'
- : primaryWhatsappAccount?.isConnected
- ? 'Sua conta do operador está conectada e pronta para receber e enviar mensagens da empresa pelo backend.'
- : hasAccount
- ? 'A conta do seu operador existe, mas ainda não está conectada. Gere um novo link para concluir ou repetir o Hosted Auth.'
- : 'Nenhuma conta WhatsApp foi conectada para este operador ainda. Gere um link, conclua o Hosted Auth e volte para atualizar o status.';
- const count = whatsappLoading
- ? ''
- : connectedAccounts.length > 0
- ? `${connectedAccounts.length} conta${connectedAccounts.length > 1 ? 's' : ''} conectada${connectedAccounts.length > 1 ? 's' : ''} para este operador`
- : whatsappAccounts.length > 0
- ? `${whatsappAccounts.length} conta${whatsappAccounts.length > 1 ? 's' : ''} cadastrada${whatsappAccounts.length > 1 ? 's' : ''} para este operador`
- : '';
- const meta = primaryWhatsappAccount
- ? [
- primaryWhatsappAccount.accountName ? `Conta: ${primaryWhatsappAccount.accountName}` : '',
- primaryWhatsappAccount.accountId ? `Account ID: ${primaryWhatsappAccount.accountId}` : '',
- primaryWhatsappAccount.status ? `Status: ${primaryWhatsappAccount.status}` : '',
- primaryWhatsappAccount.lastSyncAt ? `Última sincronização: ${formatDateTime(primaryWhatsappAccount.lastSyncAt)}` : ''
- ]
- .filter(Boolean)
- .join(' • ')
- : '';
- const helper = whatsappOpening || whatsappPolling
- ? 'Complete a autenticação na janela do Unipile. Esta tela está verificando automaticamente se a conta do seu operador entrou no backend.'
- : lastHostedAuthUrl !== ''
- ? 'Se você fechou a janela do Unipile, use “abrir link” para retomar o Hosted Auth ou “atualizar” para consultar o backend. As mensagens continuarão chegando vinculadas à empresa.'
- : 'Depois de concluir o Hosted Auth, use “atualizar” para confirmar se o callback registrou a integração do seu operador. As mensagens continuarão chegando vinculadas à empresa.';
- return {
- key: 'whatsapp-business',
- title: 'WhatsApp Business',
- description,
- status,
- action: whatsappOpening ? 'abrindo...' : action,
- secondaryAction: whatsappRefreshing ? 'atualizando...' : 'atualizar',
- tertiaryAction: lastHostedAuthUrl !== '' ? 'abrir link' : '',
- count,
- meta,
- error: whatsappLoadError || whatsappActionError || primaryWhatsappAccount?.lastError || '',
- helper,
- disablePrimary: whatsappLoading || whatsappOpening,
- disableSecondary: whatsappLoading || whatsappRefreshing,
- disableTertiary: whatsappOpening
- };
- }
- async function loadWhatsappAccounts({ silent = false } = {}) {
- if (!silent) {
- whatsappLoading = true;
- whatsappLoadError = '';
- }
- try {
- const accounts = await listWhatsappAccounts();
- whatsappAccounts = accounts;
- if (accounts.some((account) => account?.isConnected)) {
- stopWhatsappPolling();
- }
- return accounts;
- } catch (err) {
- const message = err?.message ?? 'Falha ao carregar as integrações do WhatsApp.';
- if (silent) {
- whatsappActionError = message;
- return whatsappAccounts;
- }
- whatsappLoadError = message;
- whatsappAccounts = [];
- return [];
- } finally {
- if (!silent) {
- whatsappLoading = false;
- }
- }
- }
- async function refreshWhatsappAccounts() {
- if (whatsappRefreshing) {
- return whatsappAccounts;
- }
- whatsappRefreshing = true;
- whatsappActionError = '';
- try {
- return await loadWhatsappAccounts({ silent: true });
- } finally {
- whatsappRefreshing = false;
- }
- }
- function stopWhatsappPolling() {
- if (whatsappPollTimeout !== null) {
- window.clearTimeout(whatsappPollTimeout);
- whatsappPollTimeout = null;
- }
- if (whatsappPollInterval !== null) {
- window.clearInterval(whatsappPollInterval);
- whatsappPollInterval = null;
- }
- whatsappPolling = false;
- whatsappPollAttempts = 0;
- }
- function startWhatsappPolling() {
- stopWhatsappPolling();
- whatsappPolling = true;
- whatsappPollAttempts = 0;
- const poll = async () => {
- whatsappPollAttempts += 1;
- const accounts = await refreshWhatsappAccounts();
- if (accounts.some((account) => account?.isConnected) || whatsappPollAttempts >= MAX_WHATSAPP_POLL_ATTEMPTS) {
- stopWhatsappPolling();
- }
- };
- whatsappPollTimeout = window.setTimeout(() => {
- void poll();
- }, 2000);
- whatsappPollInterval = window.setInterval(() => {
- void poll();
- }, 5000);
- }
- async function handleWhatsappPrimaryAction() {
- if (whatsappOpening) return;
- whatsappActionError = '';
- whatsappOpening = true;
- let popup = null;
- try {
- popup = window.open('', '_blank');
- const url = primaryWhatsappAccount?.id
- ? await reconnectWhatsappAccount(primaryWhatsappAccount.id)
- : await createWhatsappHostedLink();
- if (!url) {
- throw new Error('A API não retornou a URL de conexão do WhatsApp.');
- }
- lastHostedAuthUrl = url;
- if (popup && !popup.closed) {
- popup.location.replace(url);
- } else {
- const opened = window.open(url, '_blank');
- if (!opened) {
- throw new Error('Não foi possível abrir a janela do Unipile. Verifique o bloqueador de pop-up.');
- }
- }
- startWhatsappPolling();
- await refreshWhatsappAccounts();
- } catch (err) {
- if (popup && !popup.closed) {
- popup.close();
- }
- whatsappActionError = err?.message ?? 'Falha ao iniciar a conexão do WhatsApp.';
- } finally {
- whatsappOpening = false;
- }
- }
- function openLastHostedAuthLink() {
- if (!lastHostedAuthUrl) return;
- window.open(lastHostedAuthUrl, '_blank');
- }
- function formatDateTime(value) {
- if (!value) return '';
- const parsed = new Date(value);
- if (Number.isNaN(parsed.getTime())) {
- return value;
- }
- return new Intl.DateTimeFormat('pt-BR', {
- dateStyle: 'short',
- timeStyle: 'short'
- }).format(parsed);
- }
- onMount(() => {
- void loadWhatsappAccounts();
- const onFocus = () => {
- if (lastHostedAuthUrl !== '' || whatsappPolling) {
- void refreshWhatsappAccounts();
- }
- };
- window.addEventListener('focus', onFocus);
- return () => {
- window.removeEventListener('focus', onFocus);
- stopWhatsappPolling();
- };
- });
- function getStatusBadge(status) {
- const statusConfig = {
- loading: { text: 'Carregando', class: 'bg-slate-100 text-slate-800 dark:bg-slate-900/30 dark:text-slate-400' },
- connecting: { text: 'Conectando', class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400' },
- connected: { text: 'Conectado', class: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' },
- partial: { text: 'Parcial', class: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400' },
- error: { text: 'Erro', class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' },
- enabled: { text: 'Ativo', class: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' },
- disabled: { text: 'Inativo', class: 'bg-slate-100 text-slate-800 dark:bg-slate-900/30 dark:text-slate-400' },
- configured: { text: 'Configurado', class: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' },
- available: { text: 'Disponível', class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400' },
- dark: { text: 'Escuro', class: 'bg-slate-100 text-slate-800 dark:bg-slate-900/30 dark:text-slate-400' },
- 'pt-BR': { text: 'Português', class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400' }
- };
- return statusConfig[status] || statusConfig.available;
- }
- </script>
- <div class="mx-auto max-w-[1600px] space-y-6">
- <!-- Header -->
- <div class="rounded-xl border border-slate-200 bg-white p-5 md:p-6 shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30]">
- <div class="flex items-center gap-3">
- <div class="h-8 w-8 shrink-0 rounded-lg bg-indigo-500/20 text-indigo-500 flex items-center justify-center">
- <Settings size={20} strokeWidth={2.5} />
- </div>
- <h1 class="text-xl font-bold text-slate-900 dark:text-white">Configurações</h1>
- </div>
- <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
- Gerencie as configurações do sistema, integrações e preferências da sua conta
- </p>
- </div>
- <!-- Settings Sections -->
- <div class="grid grid-cols-1 gap-6">
- {#each settingsSections as section}
- <div class="rounded-xl border border-slate-200 bg-white shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30]">
- <!-- Section Header -->
- <div class="border-b border-slate-200 p-5 dark:border-slate-800">
- <div class="flex items-center gap-3">
- <div class="h-8 w-8 shrink-0 rounded-lg bg-slate-100 text-slate-600 flex items-center justify-center dark:bg-slate-800 dark:text-slate-400">
- <section.icon size={18} strokeWidth={2} />
- </div>
- <h2 class="text-lg font-semibold text-slate-900 dark:text-white">{section.title}</h2>
- </div>
- </div>
- <!-- Section Items -->
- <div class="divide-y divide-slate-200 dark:divide-slate-800">
- {#each section.items as item}
- <div class="p-5 hover:bg-slate-50 transition-colors duration-150 dark:hover:bg-slate-800/50">
- <div class="flex items-start justify-between gap-4">
- <div class="flex-1 min-w-0">
- <div class="flex items-center gap-3">
- <h3 class="text-sm font-medium text-slate-900 dark:text-white">{item.title}</h3>
- <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium {getStatusBadge(item.status).class}">
- {getStatusBadge(item.status).text}
- </span>
- </div>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">{item.description}</p>
- {#if item.count}
- <p class="mt-1 text-xs text-amber-600 dark:text-amber-400">{item.count}</p>
- {/if}
- {#if item.meta}
- <p class="mt-2 text-xs text-slate-500 dark:text-slate-400">{item.meta}</p>
- {/if}
- {#if item.error}
- <p class="mt-2 text-xs text-red-600 dark:text-red-400">{item.error}</p>
- {/if}
- {#if item.helper}
- <p class="mt-2 text-xs text-slate-500 dark:text-slate-400">{item.helper}</p>
- {/if}
- </div>
- <div class="ml-4 flex shrink-0 flex-col items-end gap-2">
- {#if item.secondaryAction}
- <button
- onclick={() => handleSecondaryAction(item)}
- disabled={item.disableSecondary}
- class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-slate-300 text-slate-700 hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-60 transition-colors duration-150 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
- >
- {#if item.disableSecondary}
- <LoaderCircle size={14} class="animate-spin" />
- {:else}
- <RefreshCw size={14} />
- {/if}
- {item.secondaryAction}
- </button>
- {/if}
- <button
- onclick={() => handleAction(item)}
- disabled={item.disablePrimary}
- class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md bg-indigo-600 text-white hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-60 transition-colors duration-150"
- >
- {#if item.disablePrimary}
- <LoaderCircle size={14} class="animate-spin" />
- {/if}
- {item.action}
- </button>
- {#if item.tertiaryAction}
- <button
- onclick={openLastHostedAuthLink}
- disabled={item.disableTertiary}
- class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md text-indigo-700 hover:bg-indigo-50 disabled:cursor-not-allowed disabled:opacity-60 transition-colors duration-150 dark:text-indigo-300 dark:hover:bg-indigo-500/10"
- >
- <ExternalLink size={14} />
- {item.tertiaryAction}
- </button>
- {/if}
- </div>
- </div>
- </div>
- {/each}
- </div>
- </div>
- {/each}
- </div>
- <!-- SLA / Tempo de Resposta -->
- <div
- class="rounded-xl border border-slate-200 bg-white shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30]"
- >
- <div class="border-b border-slate-200 p-5 dark:border-slate-800">
- <div class="flex items-center gap-3">
- <div
- class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400"
- >
- <ShieldCheck size={18} strokeWidth={2} />
- </div>
- <div>
- <h2 class="text-lg font-semibold text-slate-900 dark:text-white">SLA por Departamento</h2>
- <p class="text-sm text-slate-500 dark:text-slate-400">
- Tempos de primeira resposta e resolução, com status em tempo real
- </p>
- </div>
- </div>
- </div>
- <div class="p-5">
- <SlaConfigManager />
- </div>
- </div>
- </div>
|