GuilhermeTerramilk commited on
Commit
698e263
·
verified ·
1 Parent(s): c818ef1

Você é o DeepSite v2 encarregado de gerar um web app de produção para edição de dados armazenados em Google Sheets, tratando a planilha como um “banco de dados”. O sistema deve oferecer uma experiência de painel (admin + operadores), API segura e camadas de proteção para que usuários NUNCA acessem diretamente a planilha, apenas o site. ======================== [1] OBJETIVO E ESCOPO ======================== - Construir um site (SPA + API) para CRUD em Google Sheets, usando a planilha como storage primário. - NUNCA expor: ID da planilha, credenciais, nomes de abas, ranges, chaves de serviço, estrutura interna. - Suportar múltiplas abas/datasets (por ex.: "DIA", "LISTA CONTATOS(Backup)", "MunicípiosEndereços" e "HISTORICO"). - Garantir UX de painel: tabela com filtros, busca, paginação, edição inline/side-drawer, criação em lote, import/export CSV. - Integrar notificações e webhooks para um orquestrador (n8n). - Tolerante a conflitos (concorrência), com logs de auditoria e versionamento por linha. - **Foco logístico**: rotas, bloqueios por localidade/município, normalização de celulares BR com apóstrofo, agrupamentos por DATA/turno e “Fechamento 23:59 (TZ São Paulo)” com idempotência diária e limpeza pós-arquivamento. ======================== [2] REQUISITOS NÃO FUNCIONAIS ======================== - Segurança: OAuth (Google) ou e-mail+senha com 2FA, JWT httpOnly, CSRF protection, CORS restrito, reCAPTCHA no signup. - RBAC: "Admin", "Gestor", "Operador-Leitura", "Operador-Editar", "Somente-Relatórios". - Desempenho: cache de leitura (60–120s) com invalidation on write; paginação server-side; índices em memória por colunas-chave. - Confiabilidade: fila de escrita (retry exponencial até 3x); detecção de corrida por ETag/versão de linha ou hash de linha. - Observabilidade: logs estruturados, métricas (p95/p99), trilha de auditoria (quem, quando, antes/depois). - Acessibilidade: WCAG AA, navegação por teclado, labels/aria. - L10n: pt-BR por padrão; TZ fixa **America/Sao_Paulo** em todas as operações de data/hora. ======================== [3] ARQUITETURA ======================== - Frontend: SPA (React/Vue/Svelte) com roteamento protegido, tabela editável, validação em tempo real, modais e side-panels. - Backend: API Node/Express (ou equivalente) com endpoints REST: - GET /api/sheets/:dataset/rows - POST /api/sheets/:dataset/rows - PATCH /api/sheets/:dataset/rows/:id - DELETE /api/sheets/:dataset/rows/:id - POST /api/sheets/:dataset/bulk (criação/atualização em lote) - GET /api/audit?dataset=...&rowId=... - “dataset” mapeia nomes lógicos (ex.: "dia", "contatos", "municipios_enderecos", "historico") para abas internas via **config privada do servidor**. - Integração com Google Sheets via **Service Account** (JWT) + Sheets API v4 com **Named Ranges** estáveis. - Proibir chamadas diretas do browser ao Google; front só fala com nosso backend (proxy que oculta IDs e ranges). - Webhooks assíncronos: POST para {{N8N_WEBHOOK_URL}} em CREATE/UPDATE/DELETE, e em eventos especiais (fechamento, envio). - Backups e consistência: criar automaticamente a aba “HISTORICO” se não existir; validar cabeçalhos e criar colunas ausentes (modo “auto-heal” com alerta para Admin). ======================== [4] CONFIGURAÇÕES (ENV) ======================== - GOOGLE_SA_EMAIL=... - GOOGLE_SA_PRIVATE_KEY=...(escaped com \n) - GOOGLE_SHEET_ID={{OCULTO_NO_CLIENTE}} - DATASETS_JSON_PATH=./deepsite.datasets.json # mapeia dataset lógico -> aba e idColumn - N8N_WEBHOOK_URL=https://.../webhook/edicoes - JWT_SECRET=... - CORS_ORIGIN=https://app.empresa.com - CACHE_TTL_SECONDS=90 - DEFAULT_PAGE_SIZE=50; MAX_PAGE_SIZE=200 - RATE_LIMIT_WINDOW_MIN=15; RATE_LIMIT_MAX_REQ=100 ======================== [5] DATASETS, ESQUEMAS E REGRAS ======================== Datasets lógicos e abas padrão: - "dia" ↔ aba "DIA" (idColumn: "UID" estável; se ausente, criar coluna oculta "UID") - "contatos" ↔ aba "LISTA CONTATOS(Backup)" (idColumn: "UID" estável; se ausente, criar) - "municipios_enderecos" ↔ "MunicípiosEndereços" (idColumn: "UID") - "historico" ↔ aba "HISTORICO" (criar se não existir; idColumn: "UID") Campos principais e validações: - Dataset "dia": - row_number (int, readOnly), **UID** (string uuid, readOnly, obrigatório) - NOME DO CLIENTE (string, obrigatório, trim e capitalização) - CELULAR (string, obrigatório) → **armazenar como** `'<+55DDDNÚMERO>` (apóstrofo + +55 + 10/11 dígitos) - CIDADE, ENDEREÇO (string) - DATA (YYYY-MM-DD, TZ São Paulo; se vazio durante o fechamento, usar “hoje”) - AM | PM (enum: ["AM","PM"]) - ITEM 1..4 (strings); UI também exibe “ITENS” concatenado - OBSERVAÇÃO (string) - STATUS (enum: ["PENDENTE","OK","ENVIADO","ERRO TELEFONE"]) - ROTEIRO (string/código), AUT (bool), CIDADE AUT (bool), ROTEIRO AUT (bool) - AUTORIZADO_EM (datetime ISO), MENSAGEM_ENVIADA_EM (HH:mm), ADMIN_NOTIFICADO (bool), TENTATIVAS (int >=0), ERRO (string) - ARCHIVED_AT (datetime ISO; soft-delete) - Dataset "contatos": - row_number (int, readOnly), **UID** (uuid, readOnly, obrigatório) - NOME COMPLETO (obrigatório), NOME ABREVIADO, NOME MAIS ABREVIADO (opcional) - CELULAR (obrigatório) → **armazenar** `'<+55DDDNÚMERO>` - CIDADE, ENDEREÇO (string), CÓD ROTEIRO (string) - BLOQ. LOC (bool), BLOQ. MUNC (bool) → **bloqueios** de envio/roteirização e avisos na UI - ARCHIVED_AT (datetime ISO) - Dataset "municipios_enderecos": - row_number, UID, CIDADE (string), ENDEREÇO (string), CÓD. ROTEIRO (string opcional), ARCHIVED_AT - Dataset "historico" (gerado pelo fechamento): - UID_ORIGEM (uid da linha de “DIA”), SNAPSHOT_EM (datetime ISO), DATA, AM | PM, CLIENTE, CELULAR, CIDADE, ENDEREÇO, ITENS, OBSERVAÇÃO, STATUS_ANTES, STATUS_DEPOIS, ACTOR_EMAIL, OBS_SISTEMA **Normalização de telefone (backend):** - Aceita variações de entrada (com/sem +, com espaços, etc.) e normaliza para **`'<+55DDDNÚMERO>`**. - Se faltar DDD (8/9 dígitos): rejeitar com erro “ERRO TELEFONE” + sugestão de DDD local (UX). - Sanitizar para evitar CSV injection na exportação (prefixar com apóstrofo já resolve). **Chave estável por linha (UID):** - Baixar planilha → se não houver coluna “UID”, criar e preencher com uuid v4 (oculta/very hidden). - Todas as operações PATCH/DELETE usam **UID** como id. ======================== [6] UX / TELAS – FOCO LOGÍSTICO ======================== - Login/Recuperação/2FA; dashboard com cartões: total do dia, pendências, “erros de telefone”, “enviados hoje”. - Tabela “DIA”: - Filtros: DATA (intervalo), AM|PM, STATUS, CIDADE, ROTEIRO, busca por nome/telefone. - Edição inline (com validação); side-drawer com histórico de alterações da linha. - Autocomplete: ao digitar cliente/telefone, oferecer preenchimento a partir de “contatos”. - Ações por linha: Duplicar, **Arquivar** (soft delete), “Enviar p/ n8n”, “Marcar ‘OK’”, Excluir (somente Admin/Gestor). - **Bloqueios dinâmicos**: se BLOQ. LOC/BLOQ. MUNC ativo no contato correspondente, desabilitar “Enviar p/ n8n” e sinalizar tooltip. - Tabela “CONTATOS”: busca (nome normalizado, DDD, CÓD ROTEIRO); import CSV com mapeamento e **merge** por CELULAR/UID. - Modo “Lote”: upsert múltiplo com pré-validação resumida. - Export: CSV/Excel respeitando filtros. - **Gerador de Resumo (WhatsApp/Relatório)**: - Opções: - Agrupar por **DATA** e **AM|PM**. - Entre clientes da mesma data: **3 linhas em branco**. - Separar mudança de DATA com uma **linha de “_ _ _ _ _”** (sub-linhado contínuo). - Última linha do grupo **sem** “separador” extra (flex). - Incluir/omitir OBSERVAÇÃO por item. - Copiar com 1 clique (clipboard) e versão “limpa” sem formatação. ======================== [7] API – CONTRATOS ======================== - GET /api/sheets/:dataset/rows Query: page, pageSize, sortBy, sortDir, search, filters (JSON: {col:{op:"=", "in", "like", "between"}, value}), groupBy (opcional) Resposta: { rows:[...], page, total, versionTag } - POST /api/sheets/:dataset/rows Body: { data:{ ...campos... } } Passos: validar → normalizar → enfileirar escrita → retornar 201 com pré-estado → após commit: invalidar cache + emitir webhook. - PATCH /api/sheets/:dataset/rows/:id # id = UID Body: { data:{...}, ifVersion:"versionTag" } Se ifVersion divergente → 409 CONFLICT + { serverState, clientState, mergeHint }. - DELETE /api/sheets/:dataset/rows/:id Soft delete por padrão (set ARCHIVED_AT). Hard delete só Admin. - POST /api/sheets/:dataset/bulk Body: { upsertBy:"UID"|"CELULAR", rows:[{...}] } Retorno por item: { ok|erro, reason } - GET /api/audit?dataset=...&rowId=...&limit=50&offset=0 ======================== [8] INTEGRAÇÃO SHEETS – DETALHES TÉCNICOS ======================== - Service Account com acesso **apenas** ao GOOGLE_SHEET_ID. - valueInputOption="USER_ENTERED" para preservar apóstrofo do telefone. - Leitura: range baseado no cabeçalho → map de colunas por nome; autodetecção da última coluna com dados. - Escrita: localizar linha por UID; se UID não existir, criar no final (append). - “Named Ranges” por aba (HEADER, BODY) para estabilidade; re-bind automático se cabeçalho mudar. - **Cache**: key “sheet:{dataset}:{hash(filters, page, sort)}”; **invalidate** on write. ======================== [9] REGRAS DE NEGÓCIO LOGÍSTICAS ======================== - **Fechamento 23:59 (TZ São Paulo)**: - Só **AUT = TRUE** vai para “HISTORICO”. - DATA: se vazia na “DIA”, usar hoje (YYYY-MM-DD). - **Idempotência por dia**: não duplica no mesmo dia (chave = concat(DATA, AM|PM, CELULAR, CLIENTE, CIDADE, ENDEREÇO)). - Pós-arquivar (na origem “DIA”): limpar campos [ITEM 1..4, OBSERVAÇÃO, ERRO], desmarcar [AUT, CIDADE AUT, ROTEIRO AUT, ADMIN_NOTIFICADO], zerar [TENTATIVAS], limpar [AUTORIZADO_EM, MENSAGEM_ENVIADA_EM], e **STATUS="PENDENTE"**. - Emite webhook “fechamento.executado” com contagem {arquivados, pulados_por_idempotência, erros}. - **Bloqueios por contato**: - Se BLOQ. MUNC (município) ou BLOQ. LOC (localidade) ativos → bloquear envio/roteirização e sinalizar na UI. - **Lookup inteligente** (criação/edição em “DIA”): - Preenche CIDADE/ENDEREÇO/ROTEIRO a partir de “contatos” pelo CELULAR; se CELULAR não encontrado, sugere criar contato. - **STATUS** state machine: - PENDENTE → (validação ok) → OK → (mensagem enviada) → ENVIADO - PENDENTE → (telefone inválido) → ERRO TELEFONE - **Roteirização**: - Exibir campo “CÓD ROTEIRO” (quando houver); permitir filtros/ordenação por código; visão “por rota”. ======================== [10] SEGURANÇA E PRIVACIDADE ======================== - Nunca retornar ao front: nomes de abas, ID da planilha, ranges, credenciais. - Rate limit por IP/usuário; deteção de “burst writes” → backoff. - Auditoria 100% (antes/depois) com hash das mudanças; trilha por usuário/IP. - Sanitização: evitar CSV injection; exporta sempre com apóstrofo em campos de risco. ======================== [11] WEBHOOKS E N8N (EVENTOS) ======================== - Em CREATE/UPDATE/DELETE: { "event":"create|update|delete", "dataset":"dia|contatos|municipios_enderecos", "rowId":"UID", "diff":{ "before":{...}, "after":{...} }, "actor":{ "id":"...", "email":"..." }, "ts":"ISO" } - Eventos especiais: - "fechamento.executado" → { totalArquivados, totalIgnorados, dataExecucao } - "envio.mensagem" (quando usuário clica “Enviar p/ n8n”) → payload com linha atual + contexto de rota. - Reenfileirar webhook até 3 tentativas (retry/backoff). ======================== [12] CONCORRÊNCIA E CONFLITOS ======================== - Cada fetch retorna “versionTag” (hash por linha). - PATCH exige “ifVersion”; se divergir → 409 e UI de resolução de conflitos (reabrir, merge por campo, sobrescrever se Admin). - Lock otimista por 60s durante edição (toast se outro usuário abrir a mesma linha). ======================== [13] FLUXO DE ERROS (UX) ======================== - Validações inline (telefone, enum, datas); erros de rede com “Tentar novamente”. - Telefone inválido: sugerir máscara automática com DDD provável (base em CIDADE → DDD configurável). - Import CSV: relatório de erros por linha + arquivo de correção. ======================== [14] RELATÓRIOS E EXPORTS ======================== - Export CSV/Excel das visões filtradas. - Relatórios prontos: “Pendências por cidade”, “Erros de telefone por DDD”, “Entregas por data/turno”, “Tempo até ENVIADO”. ======================== [15] TESTES E QA ======================== - OpenAPI contratual para todos endpoints; testes e2e: login, filtros, edição, bulk, conflitos, fechamento 23:59. - Seeds de 50–100 linhas por dataset para QA. - Teste de corrida: 2 usuários editando mesma linha → 409. ======================== [16] DEPLOY ======================== - Build Docker; TLS, HSTS, CSP estrita; variáveis do [.env]. - Monitoração de logs/métricas/erros com alertas. ======================== [17] CHECKLIST DE ENTREGA ======================== Frontend: - Login/2FA, Tabela DIA, Tabela CONTATOS, Modo Lote, Histórico por linha, Gerador de Resumo (WhatsApp) Backend: - Endpoints REST, integração Sheets (SA+Named Ranges), Cache + Fila, RBAC, Auditoria, Webhooks Documentação: - .env.example, OpenAPI, Guia de papéis e permissões, Manual do “Fechamento 23:59” Testes/Scripts: - Seeds, teste de conflitos, job de fechamento com simulação ======================== [18] PARAMETRIZAÇÃO INICIAL (RESPONDA) ======================== - {{N8N_WEBHOOK_URL}}? - Confirmar mapeamentos lógicos → abas: - "dia" ↔ "DIA" - "contatos" ↔ "LISTA CONTATOS(Backup)" - "municipios_enderecos" ↔ "MunicípiosEndereços" - "historico" ↔ "HISTORICO" (auto-criar se faltar) - Campos obrigatórios adicionais? - DDD padrão sugerido por cidade? (para auto-mask) - Papéis por usuário (RBAC)? - TTL do cache (90s) e paginação (50/100)? - Initial Deployment

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +1103 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Logistica
3
- emoji: 📊
4
- colorFrom: yellow
5
- colorTo: indigo
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: logistica
3
+ emoji: 🐳
4
+ colorFrom: green
5
+ colorTo: green
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,1103 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DeepSite v2 - Painel de Controle</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ .drawer {
11
+ transition: transform 0.3s ease-in-out;
12
+ }
13
+ .drawer-open {
14
+ transform: translateX(0);
15
+ }
16
+ .drawer-closed {
17
+ transform: translateX(100%);
18
+ }
19
+ .fade-enter-active, .fade-leave-active {
20
+ transition: opacity 0.3s;
21
+ }
22
+ .fade-enter, .fade-leave-to {
23
+ opacity: 0;
24
+ }
25
+ .phone-input:focus + .phone-hint {
26
+ display: block;
27
+ }
28
+ [x-cloak] { display: none !important; }
29
+ </style>
30
+ </head>
31
+ <body class="bg-gray-50 font-sans antialiased" x-data="app()" x-init="init()" x-cloak>
32
+ <!-- Loading Overlay -->
33
+ <div x-show="isLoading" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
34
+ <div class="bg-white p-6 rounded-lg shadow-xl">
35
+ <div class="flex items-center space-x-2">
36
+ <svg class="animate-spin h-8 w-8 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
37
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
38
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
39
+ </svg>
40
+ <span class="text-lg font-medium">Carregando...</span>
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <!-- Login Screen -->
46
+ <div x-show="currentScreen === 'login'" class="min-h-screen flex items-center justify-center bg-gray-100 p-4">
47
+ <div class="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
48
+ <div class="bg-blue-600 py-4 px-6">
49
+ <h1 class="text-white text-2xl font-bold">DeepSite v2</h1>
50
+ <p class="text-blue-100">Painel de Controle Logístico</p>
51
+ </div>
52
+ <div class="p-6">
53
+ <div class="mb-4">
54
+ <label class="block text-gray-700 text-sm font-bold mb-2" for="login-email">
55
+ E-mail
56
+ </label>
57
+ <input x-model="login.email" id="login-email" type="email" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
58
+ </div>
59
+ <div class="mb-6">
60
+ <label class="block text-gray-700 text-sm font-bold mb-2" for="login-password">
61
+ Senha
62
+ </label>
63
+ <input x-model="login.password" id="login-password" type="password" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
64
+ </div>
65
+ <div class="mb-4">
66
+ <button @click="doLogin()" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition">
67
+ Entrar
68
+ </button>
69
+ </div>
70
+ <div class="text-center text-sm text-gray-600">
71
+ <a href="#" class="text-blue-600 hover:underline">Esqueceu sua senha?</a>
72
+ </div>
73
+ </div>
74
+ <div class="bg-gray-50 px-6 py-4 border-t border-gray-200">
75
+ <p class="text-xs text-gray-600 text-center">
76
+ © 2023 DeepSite v2. Todos os direitos reservados.
77
+ </p>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Main App Layout -->
83
+ <div x-show="currentScreen !== 'login'" class="flex h-screen overflow-hidden bg-gray-100">
84
+ <!-- Sidebar -->
85
+ <div class="hidden md:flex md:flex-shrink-0">
86
+ <div class="flex flex-col w-64 bg-gray-800">
87
+ <div class="h-0 flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
88
+ <div class="flex items-center flex-shrink-0 px-4">
89
+ <div class="text-white font-bold text-xl">DeepSite v2</div>
90
+ </div>
91
+ <nav class="mt-5 flex-1 px-2 space-y-1">
92
+ <template x-for="(item, index) in sidebarItems" :key="index">
93
+ <a @click="changeView(item.view)" href="#"
94
+ class="group flex items-center px-2 py-2 text-sm font-medium rounded-md transition"
95
+ :class="{
96
+ 'bg-gray-900 text-white': currentView === item.view,
97
+ 'text-gray-300 hover:bg-gray-700 hover:text-white': currentView !== item.view
98
+ }">
99
+ <i :class="item.icon" class="mr-3 flex-shrink-0 h-6 w-6"></i>
100
+ <span x-text="item.name"></span>
101
+ </a>
102
+ </template>
103
+ </nav>
104
+ </div>
105
+ <div class="flex-shrink-0 flex bg-gray-700 p-4">
106
+ <div class="flex items-center">
107
+ <div>
108
+ <div class="text-sm font-medium text-white" x-text="currentUser.name"></div>
109
+ <div class="text-xs font-medium text-gray-300" x-text="currentUser.role"></div>
110
+ </div>
111
+ </div>
112
+ <button @click="logout()" class="ml-auto flex-shrink-0 p-1 rounded-full text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white">
113
+ <i class="fas fa-sign-out-alt h-6 w-6"></i>
114
+ </button>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Mobile sidebar -->
120
+ <div x-show="mobileSidebarOpen" class="md:hidden fixed inset-0 z-40">
121
+ <div class="fixed inset-0 bg-gray-600 bg-opacity-75" @click="mobileSidebarOpen = false"></div>
122
+ <div class="relative flex flex-col w-full max-w-xs bg-gray-800">
123
+ <div class="absolute top-0 right-0 -mr-14 p-1">
124
+ <button @click="mobileSidebarOpen = false" class="flex items-center justify-center h-12 w-12 rounded-full focus:outline-none focus:ring-2 focus:ring-white">
125
+ <i class="fas fa-times text-white"></i>
126
+ </button>
127
+ </div>
128
+ <div class="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
129
+ <div class="flex-shrink-0 flex items-center px-4">
130
+ <div class="text-white font-bold text-xl">DeepSite v2</div>
131
+ </div>
132
+ <nav class="mt-5 px-2 space-y-1">
133
+ <template x-for="(item, index) in sidebarItems" :key="index">
134
+ <a @click="changeView(item.view); mobileSidebarOpen = false;" href="#"
135
+ class="group flex items-center px-2 py-2 text-base font-medium rounded-md transition"
136
+ :class="{
137
+ 'bg-gray-900 text-white': currentView === item.view,
138
+ 'text-gray-300 hover:bg-gray-700 hover:text-white': currentView !== item.view
139
+ }">
140
+ <i :class="item.icon" class="mr-4 flex-shrink-0 h-6 w-6"></i>
141
+ <span x-text="item.name"></span>
142
+ </a>
143
+ </template>
144
+ </nav>
145
+ </div>
146
+ <div class="flex-shrink-0 flex bg-gray-700 p-4">
147
+ <div class="flex items-center">
148
+ <div>
149
+ <div class="text-base font-medium text-white" x-text="currentUser.name"></div>
150
+ <div class="text-sm font-medium text-gray-300" x-text="currentUser.role"></div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <!-- Main Content -->
158
+ <div class="flex-1 overflow-auto focus:outline-none">
159
+ <!-- Top Navigation -->
160
+ <div class="bg-white shadow-sm">
161
+ <div class="px-4 sm:px-6 lg:px-8">
162
+ <div class="flex justify-between h-16">
163
+ <div class="flex">
164
+ <button @click="mobileSidebarOpen = true" class="md:hidden -ml-2 mr-2 flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500">
165
+ <i class="fas fa-bars h-6 w-6"></i>
166
+ </button>
167
+ <div class="flex-shrink-0 flex items-center">
168
+ <h1 class="text-lg font-medium text-gray-900" x-text="getViewTitle(currentView)"></h1>
169
+ </div>
170
+ </div>
171
+ <div class="ml-4 flex items-center md:ml-6">
172
+ <button @click="showNotifications = !showNotifications" class="p-1 rounded-full text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 relative">
173
+ <i class="fas fa-bell h-6 w-6"></i>
174
+ <span x-show="unreadNotifications > 0" class="absolute top-0 right-0 block h-2 w-2 rounded-full bg-red-500"></span>
175
+ </button>
176
+ <div x-show="showNotifications" @click.away="showNotifications = false" class="origin-top-right absolute right-0 mt-2 w-80 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
177
+ <div class="px-4 py-2 border-b border-gray-200">
178
+ <h3 class="text-sm font-medium text-gray-900">Notificações</h3>
179
+ </div>
180
+ <div class="divide-y divide-gray-200 max-h-96 overflow-y-auto">
181
+ <template x-for="(notification, index) in notifications" :key="index">
182
+ <a href="#" class="block px-4 py-3 text-sm hover:bg-gray-50">
183
+ <div class="flex items-center">
184
+ <div class="flex-shrink-0">
185
+ <i :class="notification.icon" class="h-5 w-5" :class="notification.read ? 'text-gray-400' : 'text-blue-500'"></i>
186
+ </div>
187
+ <div class="ml-3 flex-1">
188
+ <p class="text-sm font-medium text-gray-900" x-text="notification.title"></p>
189
+ <p class="text-xs text-gray-500" x-text="notification.message"></p>
190
+ <p class="text-xs text-gray-400 mt-1" x-text="notification.time"></p>
191
+ </div>
192
+ </div>
193
+ </a>
194
+ </template>
195
+ <div x-show="notifications.length === 0" class="px-4 py-3 text-center text-sm text-gray-500">
196
+ Nenhuma notificação
197
+ </div>
198
+ </div>
199
+ <div class="px-4 py-2 border-t border-gray-200 text-center">
200
+ <a href="#" class="text-xs font-medium text-blue-600 hover:text-blue-500">Ver todas</a>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+
208
+ <!-- Dashboard View -->
209
+ <div x-show="currentView === 'dashboard'" class="px-4 sm:px-6 lg:px-8 py-6">
210
+ <div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
211
+ <!-- Card 1 -->
212
+ <div class="bg-white overflow-hidden shadow rounded-lg">
213
+ <div class="px-4 py-5 sm:p-6">
214
+ <div class="flex items-center">
215
+ <div class="flex-shrink-0 bg-blue-500 rounded-md p-3">
216
+ <i class="fas fa-calendar-day text-white"></i>
217
+ </div>
218
+ <div class="ml-5 w-0 flex-1">
219
+ <dt class="text-sm font-medium text-gray-500 truncate">Total do Dia</dt>
220
+ <dd class="flex items-baseline">
221
+ <div class="text-2xl font-semibold text-gray-900" x-text="dashboardStats.totalToday"></div>
222
+ </dd>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ <!-- Card 2 -->
229
+ <div class="bg-white overflow-hidden shadow rounded-lg">
230
+ <div class="px-4 py-5 sm:p-6">
231
+ <div class="flex items-center">
232
+ <div class="flex-shrink-0 bg-yellow-500 rounded-md p-3">
233
+ <i class="fas fa-exclamation-circle text-white"></i>
234
+ </div>
235
+ <div class="ml-5 w-0 flex-1">
236
+ <dt class="text-sm font-medium text-gray-500 truncate">Pendências</dt>
237
+ <dd class="flex items-baseline">
238
+ <div class="text-2xl font-semibold text-gray-900" x-text="dashboardStats.pending"></div>
239
+ </dd>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </div>
244
+
245
+ <!-- Card 3 -->
246
+ <div class="bg-white overflow-hidden shadow rounded-lg">
247
+ <div class="px-4 py-5 sm:p-6">
248
+ <div class="flex items-center">
249
+ <div class="flex-shrink-0 bg-red-500 rounded-md p-3">
250
+ <i class="fas fa-phone-slash text-white"></i>
251
+ </div>
252
+ <div class="ml-5 w-0 flex-1">
253
+ <dt class="text-sm font-medium text-gray-500 truncate">Erros de Telefone</dt>
254
+ <dd class="flex items-baseline">
255
+ <div class="text-2xl font-semibold text-gray-900" x-text="dashboardStats.phoneErrors"></div>
256
+ </dd>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ </div>
261
+
262
+ <!-- Card 4 -->
263
+ <div class="bg-white overflow-hidden shadow rounded-lg">
264
+ <div class="px-4 py-5 sm:p-6">
265
+ <div class="flex items-center">
266
+ <div class="flex-shrink-0 bg-green-500 rounded-md p-3">
267
+ <i class="fas fa-check-circle text-white"></i>
268
+ </div>
269
+ <div class="ml-5 w-0 flex-1">
270
+ <dt class="text-sm font-medium text-gray-500 truncate">Enviados Hoje</dt>
271
+ <dd class="flex items-baseline">
272
+ <div class="text-2xl font-semibold text-gray-900" x-text="dashboardStats.sentToday"></div>
273
+ </dd>
274
+ </div>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ </div>
279
+
280
+ <!-- Recent Activity -->
281
+ <div class="mt-8">
282
+ <div class="bg-white shadow rounded-lg overflow-hidden">
283
+ <div class="px-4 py-5 sm:px-6 border-b border-gray-200">
284
+ <h3 class="text-lg leading-6 font-medium text-gray-900">Atividade Recente</h3>
285
+ </div>
286
+ <div class="divide-y divide-gray-200">
287
+ <template x-for="(activity, index) in recentActivities" :key="index">
288
+ <div class="px-4 py-4 sm:px-6">
289
+ <div class="flex items-center">
290
+ <div class="flex-shrink-0">
291
+ <i :class="activity.icon" class="h-5 w-5 text-blue-500"></i>
292
+ </div>
293
+ <div class="ml-3">
294
+ <p class="text-sm font-medium text-gray-900" x-text="activity.action"></p>
295
+ <p class="text-sm text-gray-500" x-text="activity.details"></p>
296
+ <p class="text-xs text-gray-400 mt-1" x-text="activity.time"></p>
297
+ </div>
298
+ </div>
299
+ </div>
300
+ </template>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ </div>
305
+
306
+ <!-- Dia View -->
307
+ <div x-show="currentView === 'dia'" class="px-4 sm:px-6 lg:px-8 py-6">
308
+ <div class="bg-white shadow rounded-lg overflow-hidden">
309
+ <!-- Filters and Actions -->
310
+ <div class="px-4 py-4 sm:px-6 border-b border-gray-200">
311
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
312
+ <div class="flex-1">
313
+ <div class="flex flex-col sm:flex-row gap-2">
314
+ <div class="relative">
315
+ <input x-model="diaFilters.search" type="text" placeholder="Buscar..." class="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full sm:w-64">
316
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
317
+ <i class="fas fa-search text-gray-400"></i>
318
+ </div>
319
+ </div>
320
+ <select x-model="diaFilters.status" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
321
+ <option value="">Todos status</option>
322
+ <option value="PENDENTE">Pendente</option>
323
+ <option value="OK">OK</option>
324
+ <option value="ENVIADO">Enviado</option>
325
+ <option value="ERRO TELEFONE">Erro Telefone</option>
326
+ </select>
327
+ <select x-model="diaFilters.turno" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
328
+ <option value="">Todos turnos</option>
329
+ <option value="AM">AM</option>
330
+ <option value="PM">PM</option>
331
+ </select>
332
+ </div>
333
+ </div>
334
+ <div class="flex space-x-2">
335
+ <button @click="openBulkEditModal()" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
336
+ <i class="fas fa-edit mr-2"></i> Edição em Lote
337
+ </button>
338
+ <button @click="openNewDiaModal()" class="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
339
+ <i class="fas fa-plus mr-2"></i> Novo
340
+ </button>
341
+ <button @click="exportToCSV('dia')" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
342
+ <i class="fas fa-file-export mr-2"></i> Exportar
343
+ </button>
344
+ </div>
345
+ </div>
346
+ </div>
347
+
348
+ <!-- Table -->
349
+ <div class="overflow-x-auto">
350
+ <table class="min-w-full divide-y divide-gray-200">
351
+ <thead class="bg-gray-50">
352
+ <tr>
353
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
354
+ Cliente
355
+ </th>
356
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
357
+ Celular
358
+ </th>
359
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
360
+ Cidade
361
+ </th>
362
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
363
+ Data
364
+ </th>
365
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
366
+ Turno
367
+ </th>
368
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
369
+ Status
370
+ </th>
371
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
372
+ Ações
373
+ </th>
374
+ </tr>
375
+ </thead>
376
+ <tbody class="bg-white divide-y divide-gray-200">
377
+ <template x-for="(item, index) in filteredDiaItems" :key="item.UID">
378
+ <tr>
379
+ <td class="px-6 py-4 whitespace-nowrap">
380
+ <div class="text-sm font-medium text-gray-900" x-text="item['NOME DO CLIENTE']"></div>
381
+ </td>
382
+ <td class="px-6 py-4 whitespace-nowrap">
383
+ <div class="text-sm text-gray-900" x-text="formatPhone(item['CELULAR'])"></div>
384
+ </td>
385
+ <td class="px-6 py-4 whitespace-nowrap">
386
+ <div class="text-sm text-gray-900" x-text="item['CIDADE']"></div>
387
+ </td>
388
+ <td class="px-6 py-4 whitespace-nowrap">
389
+ <div class="text-sm text-gray-900" x-text="item['DATA']"></div>
390
+ </td>
391
+ <td class="px-6 py-4 whitespace-nowrap">
392
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
393
+ :class="{'bg-blue-100 text-blue-800': item['AM | PM'] === 'AM', 'bg-purple-100 text-purple-800': item['AM | PM'] === 'PM'}">
394
+ <span x-text="item['AM | PM']"></span>
395
+ </span>
396
+ </td>
397
+ <td class="px-6 py-4 whitespace-nowrap">
398
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
399
+ :class="getStatusClass(item['STATUS'])">
400
+ <span x-text="item['STATUS']"></span>
401
+ </span>
402
+ </td>
403
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
404
+ <button @click="openEditDiaModal(item)" class="text-blue-600 hover:text-blue-900 mr-3">
405
+ <i class="fas fa-edit"></i>
406
+ </button>
407
+ <button @click="openHistoryDrawer(item)" class="text-gray-600 hover:text-gray-900 mr-3">
408
+ <i class="fas fa-history"></i>
409
+ </button>
410
+ <button @click="deleteItem('dia', item.UID)" class="text-red-600 hover:text-red-900">
411
+ <i class="fas fa-trash"></i>
412
+ </button>
413
+ </td>
414
+ </tr>
415
+ </template>
416
+ <tr x-show="filteredDiaItems.length === 0">
417
+ <td colspan="7" class="px-6 py-4 text-center text-sm text-gray-500">
418
+ Nenhum registro encontrado
419
+ </td>
420
+ </tr>
421
+ </tbody>
422
+ </table>
423
+ </div>
424
+
425
+ <!-- Pagination -->
426
+ <div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
427
+ <div class="flex-1 flex justify-between sm:hidden">
428
+ <button @click="diaPagination.currentPage--" :disabled="diaPagination.currentPage === 1" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
429
+ Anterior
430
+ </button>
431
+ <button @click="diaPagination.currentPage++" :disabled="diaPagination.currentPage * diaPagination.itemsPerPage >= diaPagination.totalItems" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
432
+ Próxima
433
+ </button>
434
+ </div>
435
+ <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
436
+ <div>
437
+ <p class="text-sm text-gray-700">
438
+ Mostrando
439
+ <span class="font-medium" x-text="(diaPagination.currentPage - 1) * diaPagination.itemsPerPage + 1"></span>
440
+ a
441
+ <span class="font-medium" x-text="Math.min(diaPagination.currentPage * diaPagination.itemsPerPage, diaPagination.totalItems)"></span>
442
+ de
443
+ <span class="font-medium" x-text="diaPagination.totalItems"></span>
444
+ resultados
445
+ </p>
446
+ </div>
447
+ <div>
448
+ <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
449
+ <button @click="diaPagination.currentPage--" :disabled="diaPagination.currentPage === 1" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
450
+ <span class="sr-only">Anterior</span>
451
+ <i class="fas fa-chevron-left"></i>
452
+ </button>
453
+ <template x-for="page in getPaginationPages(diaPagination)">
454
+ <button @click="diaPagination.currentPage = page"
455
+ :class="{'bg-blue-50 border-blue-500 text-blue-600': diaPagination.currentPage === page, 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50': diaPagination.currentPage !== page}"
456
+ class="relative inline-flex items-center px-4 py-2 border text-sm font-medium">
457
+ <span x-text="page"></span>
458
+ </button>
459
+ </template>
460
+ <button @click="diaPagination.currentPage++" :disabled="diaPagination.currentPage * diaPagination.itemsPerPage >= diaPagination.totalItems" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
461
+ <span class="sr-only">Próxima</span>
462
+ <i class="fas fa-chevron-right"></i>
463
+ </button>
464
+ </nav>
465
+ </div>
466
+ </div>
467
+ </div>
468
+ </div>
469
+ </div>
470
+
471
+ <!-- Contatos View -->
472
+ <div x-show="currentView === 'contatos'" class="px-4 sm:px-6 lg:px-8 py-6">
473
+ <div class="bg-white shadow rounded-lg overflow-hidden">
474
+ <!-- Filters and Actions -->
475
+ <div class="px-4 py-4 sm:px-6 border-b border-gray-200">
476
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
477
+ <div class="flex-1">
478
+ <div class="flex flex-col sm:flex-row gap-2">
479
+ <div class="relative">
480
+ <input x-model="contatosFilters.search" type="text" placeholder="Buscar..." class="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full sm:w-64">
481
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
482
+ <i class="fas fa-search text-gray-400"></i>
483
+ </div>
484
+ </div>
485
+ <select x-model="contatosFilters.cidade" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
486
+ <option value="">Todas cidades</option>
487
+ <template x-for="city in uniqueCities">
488
+ <option :value="city" x-text="city"></option>
489
+ </template>
490
+ </select>
491
+ </div>
492
+ </div>
493
+ <div class="flex space-x-2">
494
+ <button @click="openImportModal('contatos')" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
495
+ <i class="fas fa-file-import mr-2"></i> Importar
496
+ </button>
497
+ <button @click="openNewContatoModal()" class="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
498
+ <i class="fas fa-plus mr-2"></i> Novo
499
+ </button>
500
+ <button @click="exportToCSV('contatos')" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
501
+ <i class="fas fa-file-export mr-2"></i> Exportar
502
+ </button>
503
+ </div>
504
+ </div>
505
+ </div>
506
+
507
+ <!-- Table -->
508
+ <div class="overflow-x-auto">
509
+ <table class="min-w-full divide-y divide-gray-200">
510
+ <thead class="bg-gray-50">
511
+ <tr>
512
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
513
+ Nome
514
+ </th>
515
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
516
+ Celular
517
+ </th>
518
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
519
+ Cidade
520
+ </th>
521
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
522
+ Endereço
523
+ </th>
524
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
525
+ Cód. Roteiro
526
+ </th>
527
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
528
+ Bloqueios
529
+ </th>
530
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
531
+ Ações
532
+ </th>
533
+ </tr>
534
+ </thead>
535
+ <tbody class="bg-white divide-y divide-gray-200">
536
+ <template x-for="(item, index) in filteredContatosItems" :key="item.UID">
537
+ <tr>
538
+ <td class="px-6 py-4 whitespace-nowrap">
539
+ <div class="text-sm font-medium text-gray-900" x-text="item['NOME COMPLETO']"></div>
540
+ <div class="text-sm text-gray-500" x-text="item['NOME ABREVIADO']"></div>
541
+ </td>
542
+ <td class="px-6 py-4 whitespace-nowrap">
543
+ <div class="text-sm text-gray-900" x-text="formatPhone(item['CELULAR'])"></div>
544
+ </td>
545
+ <td class="px-6 py-4 whitespace-nowrap">
546
+ <div class="text-sm text-gray-900" x-text="item['CIDADE']"></div>
547
+ </td>
548
+ <td class="px-6 py-4">
549
+ <div class="text-sm text-gray-900" x-text="item['ENDEREÇO']"></div>
550
+ </td>
551
+ <td class="px-6 py-4 whitespace-nowrap">
552
+ <div class="text-sm text-gray-900" x-text="item['CÓD ROTEIRO']"></div>
553
+ </td>
554
+ <td class="px-6 py-4 whitespace-nowrap">
555
+ <template x-if="item['BLOQ. LOC'] || item['BLOQ. MUNC']">
556
+ <div class="flex space-x-1">
557
+ <template x-if="item['BLOQ. LOC']">
558
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
559
+ Localidade
560
+ </span>
561
+ </template>
562
+ <template x-if="item['BLOQ. MUNC']">
563
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
564
+ Município
565
+ </span>
566
+ </template>
567
+ </div>
568
+ </template>
569
+ </td>
570
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
571
+ <button @click="openEditContatoModal(item)" class="text-blue-600 hover:text-blue-900 mr-3">
572
+ <i class="fas fa-edit"></i>
573
+ </button>
574
+ <button @click="openHistoryDrawer(item)" class="text-gray-600 hover:text-gray-900 mr-3">
575
+ <i class="fas fa-history"></i>
576
+ </button>
577
+ <button @click="deleteItem('contatos', item.UID)" class="text-red-600 hover:text-red-900">
578
+ <i class="fas fa-trash"></i>
579
+ </button>
580
+ </td>
581
+ </tr>
582
+ </template>
583
+ <tr x-show="filteredContatosItems.length === 0">
584
+ <td colspan="7" class="px-6 py-4 text-center text-sm text-gray-500">
585
+ Nenhum registro encontrado
586
+ </td>
587
+ </tr>
588
+ </tbody>
589
+ </table>
590
+ </div>
591
+
592
+ <!-- Pagination -->
593
+ <div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
594
+ <div class="flex-1 flex justify-between sm:hidden">
595
+ <button @click="contatosPagination.currentPage--" :disabled="contatosPagination.currentPage === 1" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
596
+ Anterior
597
+ </button>
598
+ <button @click="contatosPagination.currentPage++" :disabled="contatosPagination.currentPage * contatosPagination.itemsPerPage >= contatosPagination.totalItems" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
599
+ Próxima
600
+ </button>
601
+ </div>
602
+ <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
603
+ <div>
604
+ <p class="text-sm text-gray-700">
605
+ Mostrando
606
+ <span class="font-medium" x-text="(contatosPagination.currentPage - 1) * contatosPagination.itemsPerPage + 1"></span>
607
+ a
608
+ <span class="font-medium" x-text="Math.min(contatosPagination.currentPage * contatosPagination.itemsPerPage, contatosPagination.totalItems)"></span>
609
+ de
610
+ <span class="font-medium" x-text="contatosPagination.totalItems"></span>
611
+ resultados
612
+ </p>
613
+ </div>
614
+ <div>
615
+ <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
616
+ <button @click="contatosPagination.currentPage--" :disabled="contatosPagination.currentPage === 1" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
617
+ <span class="sr-only">Anterior</span>
618
+ <i class="fas fa-chevron-left"></i>
619
+ </button>
620
+ <template x-for="page in getPaginationPages(contatosPagination)">
621
+ <button @click="contatosPagination.currentPage = page"
622
+ :class="{'bg-blue-50 border-blue-500 text-blue-600': contatosPagination.currentPage === page, 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50': contatosPagination.currentPage !== page}"
623
+ class="relative inline-flex items-center px-4 py-2 border text-sm font-medium">
624
+ <span x-text="page"></span>
625
+ </button>
626
+ </template>
627
+ <button @click="contatosPagination.currentPage++" :disabled="contatosPagination.currentPage * contatosPagination.itemsPerPage >= contatosPagination.totalItems" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
628
+ <span class="sr-only">Próxima</span>
629
+ <i class="fas fa-chevron-right"></i>
630
+ </button>
631
+ </nav>
632
+ </div>
633
+ </div>
634
+ </div>
635
+ </div>
636
+ </div>
637
+
638
+ <!-- Municipios Enderecos View -->
639
+ <div x-show="currentView === 'municipios_enderecos'" class="px-4 sm:px-6 lg:px-8 py-6">
640
+ <div class="bg-white shadow rounded-lg overflow-hidden">
641
+ <!-- Filters and Actions -->
642
+ <div class="px-4 py-4 sm:px-6 border-b border-gray-200">
643
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
644
+ <div class="flex-1">
645
+ <div class="flex flex-col sm:flex-row gap-2">
646
+ <div class="relative">
647
+ <input x-model="municipiosFilters.search" type="text" placeholder="Buscar..." class="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full sm:w-64">
648
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
649
+ <i class="fas fa-search text-gray-400"></i>
650
+ </div>
651
+ </div>
652
+ <select x-model="municipiosFilters.cidade" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
653
+ <option value="">Todas cidades</option>
654
+ <template x-for="city in uniqueMunicipiosCities">
655
+ <option :value="city" x-text="city"></option>
656
+ </template>
657
+ </select>
658
+ </div>
659
+ </div>
660
+ <div class="flex space-x-2">
661
+ <button @click="openImportModal('municipios_enderecos')" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
662
+ <i class="fas fa-file-import mr-2"></i> Importar
663
+ </button>
664
+ <button @click="openNewMunicipioModal()" class="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
665
+ <i class="fas fa-plus mr-2"></i> Novo
666
+ </button>
667
+ <button @click="exportToCSV('municipios_enderecos')" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
668
+ <i class="fas fa-file-export mr-2"></i> Exportar
669
+ </button>
670
+ </div>
671
+ </div>
672
+ </div>
673
+
674
+ <!-- Table -->
675
+ <div class="overflow-x-auto">
676
+ <table class="min-w-full divide-y divide-gray-200">
677
+ <thead class="bg-gray-50">
678
+ <tr>
679
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
680
+ Cidade
681
+ </th>
682
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
683
+ Endereço
684
+ </th>
685
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
686
+ Cód. Roteiro
687
+ </th>
688
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
689
+ Ações
690
+ </th>
691
+ </tr>
692
+ </thead>
693
+ <tbody class="bg-white divide-y divide-gray-200">
694
+ <template x-for="(item, index) in filteredMunicipiosItems" :key="item.UID">
695
+ <tr>
696
+ <td class="px-6 py-4 whitespace-nowrap">
697
+ <div class="text-sm font-medium text-gray-900" x-text="item['CIDADE']"></div>
698
+ </td>
699
+ <td class="px-6 py-4">
700
+ <div class="text-sm text-gray-900" x-text="item['ENDEREÇO']"></div>
701
+ </td>
702
+ <td class="px-6 py-4 whitespace-nowrap">
703
+ <div class="text-sm text-gray-900" x-text="item['CÓD. ROTEIRO']"></div>
704
+ </td>
705
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
706
+ <button @click="openEditMunicipioModal(item)" class="text-blue-600 hover:text-blue-900 mr-3">
707
+ <i class="fas fa-edit"></i>
708
+ </button>
709
+ <button @click="openHistoryDrawer(item)" class="text-gray-600 hover:text-gray-900 mr-3">
710
+ <i class="fas fa-history"></i>
711
+ </button>
712
+ <button @click="deleteItem('municipios_enderecos', item.UID)" class="text-red-600 hover:text-red-900">
713
+ <i class="fas fa-trash"></i>
714
+ </button>
715
+ </td>
716
+ </tr>
717
+ </template>
718
+ <tr x-show="filteredMunicipiosItems.length === 0">
719
+ <td colspan="4" class="px-6 py-4 text-center text-sm text-gray-500">
720
+ Nenhum registro encontrado
721
+ </td>
722
+ </tr>
723
+ </tbody>
724
+ </table>
725
+ </div>
726
+
727
+ <!-- Pagination -->
728
+ <div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
729
+ <div class="flex-1 flex justify-between sm:hidden">
730
+ <button @click="municipiosPagination.currentPage--" :disabled="municipiosPagination.currentPage === 1" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
731
+ Anterior
732
+ </button>
733
+ <button @click="municipiosPagination.currentPage++" :disabled="municipiosPagination.currentPage * municipiosPagination.itemsPerPage >= municipiosPagination.totalItems" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
734
+ Próxima
735
+ </button>
736
+ </div>
737
+ <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
738
+ <div>
739
+ <p class="text-sm text-gray-700">
740
+ Mostrando
741
+ <span class="font-medium" x-text="(municipiosPagination.currentPage - 1) * municipiosPagination.itemsPerPage + 1"></span>
742
+ a
743
+ <span class="font-medium" x-text="Math.min(municipiosPagination.currentPage * municipiosPagination.itemsPerPage, municipiosPagination.totalItems)"></span>
744
+ de
745
+ <span class="font-medium" x-text="municipiosPagination.totalItems"></span>
746
+ resultados
747
+ </p>
748
+ </div>
749
+ <div>
750
+ <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
751
+ <button @click="municipiosPagination.currentPage--" :disabled="municipiosPagination.currentPage === 1" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
752
+ <span class="sr-only">Anterior</span>
753
+ <i class="fas fa-chevron-left"></i>
754
+ </button>
755
+ <template x-for="page in getPaginationPages(municipiosPagination)">
756
+ <button @click="municipiosPagination.currentPage = page"
757
+ :class="{'bg-blue-50 border-blue-500 text-blue-600': municipiosPagination.currentPage === page, 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50': municipiosPagination.currentPage !== page}"
758
+ class="relative inline-flex items-center px-4 py-2 border text-sm font-medium">
759
+ <span x-text="page"></span>
760
+ </button>
761
+ </template>
762
+ <button @click="municipiosPagination.currentPage++" :disabled="municipiosPagination.currentPage * municipiosPagination.itemsPerPage >= municipiosPagination.totalItems" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
763
+ <span class="sr-only">Próxima</span>
764
+ <i class="fas fa-chevron-right"></i>
765
+ </button>
766
+ </nav>
767
+ </div>
768
+ </div>
769
+ </div>
770
+ </div>
771
+ </div>
772
+
773
+ <!-- Historico View -->
774
+ <div x-show="currentView === 'historico'" class="px-4 sm:px-6 lg:px-8 py-6">
775
+ <div class="bg-white shadow rounded-lg overflow-hidden">
776
+ <!-- Filters and Actions -->
777
+ <div class="px-4 py-4 sm:px-6 border-b border-gray-200">
778
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
779
+ <div class="flex-1">
780
+ <div class="flex flex-col sm:flex-row gap-2">
781
+ <div class="relative">
782
+ <input x-model="historicoFilters.search" type="text" placeholder="Buscar..." class="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full sm:w-64">
783
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
784
+ <i class="fas fa-search text-gray-400"></i>
785
+ </div>
786
+ </div>
787
+ <select x-model="historicoFilters.status" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
788
+ <option value="">Todos status</option>
789
+ <option value="PENDENTE">Pendente</option>
790
+ <option value="OK">OK</option>
791
+ <option value="ENVIADO">Enviado</option>
792
+ <option value="ERRO TELEFONE">Erro Telefone</option>
793
+ </select>
794
+ <select x-model="historicoFilters.turno" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
795
+ <option value="">Todos turnos</option>
796
+ <option value="AM">AM</option>
797
+ <option value="PM">PM</option>
798
+ </select>
799
+ </div>
800
+ </div>
801
+ <div class="flex space-x-2">
802
+ <button @click="exportToCSV('historico')" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
803
+ <i class="fas fa-file-export mr-2"></i> Exportar
804
+ </button>
805
+ </div>
806
+ </div>
807
+ </div>
808
+
809
+ <!-- Table -->
810
+ <div class="overflow-x-auto">
811
+ <table class="min-w-full divide-y divide-gray-200">
812
+ <thead class="bg-gray-50">
813
+ <tr>
814
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
815
+ Cliente
816
+ </th>
817
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
818
+ Data
819
+ </th>
820
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
821
+ Turno
822
+ </th>
823
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
824
+ Status
825
+ </th>
826
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
827
+ Alterado por
828
+ </th>
829
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
830
+ Data Alteração
831
+ </th>
832
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
833
+ Ações
834
+ </th>
835
+ </tr>
836
+ </thead>
837
+ <tbody class="bg-white divide-y divide-gray-200">
838
+ <template x-for="(item, index) in filteredHistoricoItems" :key="item.UID">
839
+ <tr>
840
+ <td class="px-6 py-4 whitespace-nowrap">
841
+ <div class="text-sm font-medium text-gray-900" x-text="item['CLIENTE']"></div>
842
+ </td>
843
+ <td class="px-6 py-4 whitespace-nowrap">
844
+ <div class="text-sm text-gray-900" x-text="item['DATA']"></div>
845
+ </td>
846
+ <td class="px-6 py-4 whitespace-nowrap">
847
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
848
+ :class="{'bg-blue-100 text-blue-800': item['AM | PM'] === 'AM', 'bg-purple-100 text-purple-800': item['AM | PM'] === 'PM'}">
849
+ <span x-text="item['AM | PM']"></span>
850
+ </span>
851
+ </td>
852
+ <td class="px-6 py-4 whitespace-nowrap">
853
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
854
+ :class="getStatusClass(item['STATUS_DEPOIS'])">
855
+ <span x-text="item['STATUS_DEPOIS']"></span>
856
+ </span>
857
+ </td>
858
+ <td class="px-6 py-4 whitespace-nowrap">
859
+ <div class="text-sm text-gray-900" x-text="item['ACTOR_EMAIL']"></div>
860
+ </td>
861
+ <td class="px-6 py-4 whitespace-nowrap">
862
+ <div class="text-sm text-gray-900" x-text="item['SNAPSHOT_EM']"></div>
863
+ </td>
864
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
865
+ <button @click="openHistoryDetailsModal(item)" class="text-blue-600 hover:text-blue-900">
866
+ <i class="fas fa-eye"></i>
867
+ </button>
868
+ </td>
869
+ </tr>
870
+ </template>
871
+ <tr x-show="filteredHistoricoItems.length === 0">
872
+ <td colspan="7" class="px-6 py-4 text-center text-sm text-gray-500">
873
+ Nenhum registro encontrado
874
+ </td>
875
+ </tr>
876
+ </tbody>
877
+ </table>
878
+ </div>
879
+
880
+ <!-- Pagination -->
881
+ <div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
882
+ <div class="flex-1 flex justify-between sm:hidden">
883
+ <button @click="historicoPagination.currentPage--" :disabled="historicoPagination.currentPage === 1" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
884
+ Anterior
885
+ </button>
886
+ <button @click="historicoPagination.currentPage++" :disabled="historicoPagination.currentPage * historicoPagination.itemsPerPage >= historicoPagination.totalItems" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
887
+ Próxima
888
+ </button>
889
+ </div>
890
+ <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
891
+ <div>
892
+ <p class="text-sm text-gray-700">
893
+ Mostrando
894
+ <span class="font-medium" x-text="(historicoPagination.currentPage - 1) * historicoPagination.itemsPerPage + 1"></span>
895
+ a
896
+ <span class="font-medium" x-text="Math.min(historicoPagination.currentPage * historicoPagination.itemsPerPage, historicoPagination.totalItems)"></span>
897
+ de
898
+ <span class="font-medium" x-text="historicoPagination.totalItems"></span>
899
+ resultados
900
+ </p>
901
+ </div>
902
+ <div>
903
+ <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
904
+ <button @click="historicoPagination.currentPage--" :disabled="historicoPagination.currentPage === 1" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
905
+ <span class="sr-only">Anterior</span>
906
+ <i class="fas fa-chevron-left"></i>
907
+ </button>
908
+ <template x-for="page in getPaginationPages(historicoPagination)">
909
+ <button @click="historicoPagination.currentPage = page"
910
+ :class="{'bg-blue-50 border-blue-500 text-blue-600': historicoPagination.currentPage === page, 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50': historicoPagination.currentPage !== page}"
911
+ class="relative inline-flex items-center px-4 py-2 border text-sm font-medium">
912
+ <span x-text="page"></span>
913
+ </button>
914
+ </template>
915
+ <button @click="historicoPagination.currentPage++" :disabled="historicoPagination.currentPage * historicoPagination.itemsPerPage >= historicoPagination.totalItems" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
916
+ <span class="sr-only">Próxima</span>
917
+ <i class="fas fa-chevron-right"></i>
918
+ </button>
919
+ </nav>
920
+ </div>
921
+ </div>
922
+ </div>
923
+ </div>
924
+ </div>
925
+
926
+ <!-- Relatorios View -->
927
+ <div x-show="currentView === 'relatorios'" class="px-4 sm:px-6 lg:px-8 py-6">
928
+ <div class="bg-white shadow rounded-lg overflow-hidden">
929
+ <div class="px-4 py-5 sm:px-6 border-b border-gray-200">
930
+ <h3 class="text-lg leading-6 font-medium text-gray-900">Relatórios</h3>
931
+ </div>
932
+ <div class="px-4 py-5 sm:p-6">
933
+ <div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
934
+ <!-- Report Card 1 -->
935
+ <div class="bg-white overflow-hidden shadow rounded-lg">
936
+ <div class="px-4 py-5 sm:p-6">
937
+ <h3 class="text-lg leading-6 font-medium text-gray-900">Pendências por Cidade</h3>
938
+ <div class="mt-4">
939
+ <div class="h-64 overflow-y-auto">
940
+ <template x-for="(item, index) in reports.pendingByCity" :key="index">
941
+ <div class="mb-2">
942
+ <div class="flex justify-between text-sm">
943
+ <span x-text="item.city"></span>
944
+ <span x-text="item.count" class="font-medium"></span>
945
+ </div>
946
+ <div class="w-full bg-gray-200 rounded-full h-2.5">
947
+ <div class="bg-yellow-500 h-2.5 rounded-full"
948
+ :style="`width: ${(item.count / Math.max(...reports.pendingByCity.map(i => i.count))) * 100}%`"></div>
949
+ </div>
950
+ </div>
951
+ </template>
952
+ </div>
953
+ </div>
954
+ <div class="mt-4">
955
+ <button @click="exportReport('pendingByCity')" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
956
+ <i class="fas fa-file-export mr-2"></i> Exportar
957
+ </button>
958
+ </div>
959
+ </div>
960
+ </div>
961
+
962
+ <!-- Report Card 2 -->
963
+ <div class="bg-white overflow-hidden shadow rounded-lg">
964
+ <div class="px-4 py-5 sm:p-6">
965
+ <h3 class="text-lg leading-6 font-medium text-gray-900">Erros de Telefone por DDD</h3>
966
+ <div class="mt-4">
967
+ <div class="h-64 overflow-y-auto">
968
+ <template x-for="(item, index) in reports.phoneErrorsByDDD" :key="index">
969
+ <div class="mb-2">
970
+ <div class="flex justify-between text-sm">
971
+ <span>DDD <span x-text="item.ddd"></span></span>
972
+ <span x-text="item.count" class="font-medium"></span>
973
+ </div>
974
+ <div class="w-full bg-gray-200 rounded-full h-2.5">
975
+ <div class="bg-red-500 h-2.5 rounded-full"
976
+ :style="`width: ${(item.count / Math.max(...reports.phoneErrorsByDDD.map(i => i.count))) * 100}%`"></div>
977
+ </div>
978
+ </div>
979
+ </template>
980
+ </div>
981
+ </div>
982
+ <div class="mt-4">
983
+ <button @click="exportReport('phoneErrorsByDDD')" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
984
+ <i class="fas fa-file-export mr-2"></i> Exportar
985
+ </button>
986
+ </div>
987
+ </div>
988
+ </div>
989
+
990
+ <!-- Report Card 3 -->
991
+ <div class="bg-white overflow-hidden shadow rounded-lg">
992
+ <div class="px-4 py-5 sm:p-6">
993
+ <h3 class="text-lg leading-6 font-medium text-gray-900">Entregas por Data/Turno</h3>
994
+ <div class="mt-4">
995
+ <div class="h-64 overflow-y-auto">
996
+ <template x-for="(item, index) in reports.deliveriesByDateTurn" :key="index">
997
+ <div class="mb-2">
998
+ <div class="flex justify-between text-sm">
999
+ <span x-text="item.date + ' - ' + item.turn"></span>
1000
+ <span x-text="item.count" class="font-medium"></span>
1001
+ </div>
1002
+ <div class="w-full bg-gray-200 rounded-full h-2.5">
1003
+ <div class="bg-green-500 h-2.5 rounded-full"
1004
+ :style="`width: ${(item.count / Math.max(...reports.deliveriesByDateTurn.map(i => i.count))) * 100}%`"></div>
1005
+ </div>
1006
+ </div>
1007
+ </template>
1008
+ </div>
1009
+ </div>
1010
+ <div class="mt-4">
1011
+ <button @click="exportReport('deliveriesByDateTurn')" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
1012
+ <i class="fas fa-file-export mr-2"></i> Exportar
1013
+ </button>
1014
+ </div>
1015
+ </div>
1016
+ </div>
1017
+
1018
+ <!-- Report Card 4 -->
1019
+ <div class="bg-white overflow-hidden shadow rounded-lg">
1020
+ <div class="px-4 py-5 sm:p-6">
1021
+ <h3 class="text-lg leading-6 font-medium text-gray-900">Tempo até ENVIADO</h3>
1022
+ <div class="mt-4">
1023
+ <div class="h-64 overflow-y-auto">
1024
+ <div class="mb-4">
1025
+ <div class="text-sm text-gray-500 mb-1">Média</div>
1026
+ <div class="text-xl font-medium" x-text="reports.timeToSent.average + ' horas'"></div>
1027
+ </div>
1028
+ <div class="mb-4">
1029
+ <div class="text-sm text-gray-500 mb-1">Mínimo</div>
1030
+ <div class="text-xl font-medium" x-text="reports.timeToSent.min + ' horas'"></div>
1031
+ </div>
1032
+ <div class="mb-4">
1033
+ <div class="text-sm text-gray-500 mb-1">Máximo</div>
1034
+ <div class="text-xl font-medium" x-text="reports.timeToSent.max + ' horas'"></div>
1035
+ </div>
1036
+ </div>
1037
+ </div>
1038
+ <div class="mt-4">
1039
+ <button @click="exportReport('timeToSent')" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
1040
+ <i class="fas fa-file-export mr-2"></i> Exportar
1041
+ </button>
1042
+ </div>
1043
+ </div>
1044
+ </div>
1045
+ </div>
1046
+ </div>
1047
+ </div>
1048
+ </div>
1049
+
1050
+ <!-- WhatsApp Resumo Generator -->
1051
+ <div x-show="currentView === 'whatsapp'" class="px-4 sm:px-6 lg:px-8 py-6">
1052
+ <div class="bg-white shadow rounded-lg overflow-hidden">
1053
+ <div class="px-4 py-5 sm:px-6 border-b border-gray-200">
1054
+ <h3 class="text-lg leading-6 font-medium text-gray-900">Gerador de Resumo (WhatsApp/Relatório)</h3>
1055
+ </div>
1056
+ <div class="px-4 py-5 sm:p-6">
1057
+ <div class="grid grid-cols-1 gap-6">
1058
+ <!-- Options -->
1059
+ <div class="bg-gray-50 p-4 rounded-lg">
1060
+ <h4 class="text-md font-medium text-gray-900 mb-3">Opções de Formatação</h4>
1061
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
1062
+ <div class="flex items-center">
1063
+ <input x-model="whatsappOptions.groupByDate" id="groupByDate" type="checkbox" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
1064
+ <label for="groupByDate" class="ml-2 block text-sm text-gray-900">Agrupar por DATA</label>
1065
+ </div>
1066
+ <div class="flex items-center">
1067
+ <input x-model="whatsappOptions.groupByTurn" id="groupByTurn" type="checkbox" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
1068
+ <label for="groupByTurn" class="ml-2 block text-sm text-gray-900">Agrupar por AM/PM</label>
1069
+ </div>
1070
+ <div class="flex items-center">
1071
+ <input x-model="whatsappOptions.blankLines" id="blankLines" type="checkbox" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
1072
+ <label for="blankLines" class="ml-2 block text-sm text-gray-900">3 linhas em branco entre grupos</label>
1073
+ </div>
1074
+ <div class="flex items-center">
1075
+ <input x-model="whatsappOptions.separatorLine" id="separatorLine" type="checkbox" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
1076
+ <label for="separatorLine" class="ml-2 block text-sm text-gray-900">Linha separadora (_____)</label>
1077
+ </div>
1078
+ <div class="flex items-center">
1079
+ <input x-model="whatsappOptions.includeObservation" id="includeObservation" type="checkbox" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
1080
+ <label for="includeObservation" class="ml-2 block text-sm text-gray-900">Incluir OBSERVAÇÃO</label>
1081
+ </div>
1082
+ </div>
1083
+ </div>
1084
+
1085
+ <!-- Preview -->
1086
+ <div>
1087
+ <div class="flex justify-between items-center mb-2">
1088
+ <h4 class="text-md font-medium text-gray-900">Pré-visualização</h4>
1089
+ <div class="flex space-x-2">
1090
+ <button @click="copyToClipboard()" class="inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
1091
+ <i class="fas fa-copy mr-2"></i> Copiar
1092
+ </button>
1093
+ <button @click="generateWhatsappSummary()" class="inline-flex items-center px-3 py-1 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
1094
+ <i class="fas fa-sync-alt mr-2"></i> Atualizar
1095
+ </button>
1096
+ </div>
1097
+ </div>
1098
+ <div class="bg-gray-50 p-4 rounded-lg border border-gray-200">
1099
+ <pre x-text="whatsappPreview" class="whitespace-pre-wrap font-sans text-sm"></pre>
1100
+ </div>
1101
+ </div
1102
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=GuilhermeTerramilk/logistica" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
1103
+ </html>