Files
VPS 4 Builder 40bed96515 feat(newwhats): agenda inteligente — especialidades, situações, dúvidas, checklist do dono e pendências
Satélite da Secretária IA:
- /contexto (dentistas+especialidades+situações+cautelas) e /duvida (encaminhar
  ao humano sem agendar); agenda_pedidos ganha tipo=duvida + resumo.
- Situações da clínica: tabela agenda_situacoes + config (dono) + aba na UI.
- Checklist do dono (checklist-defs.js + agenda_checklist): perguntas Sim/Não
  conferidas contra o banco (dentistas/pacientes) → pendências de config +
  cautelas p/ a IA não chutar.
- Área de PENDÊNCIAS DO DONO (PendenciasDono.tsx): engloba config + fila
  operacional (horários/dúvidas); botão+badge no /wa-secretaria (só o dono).
- Fila humana (PedidosPendentes): cards de dúvida + resolver; /resolver.
- Botão DESLIGAR global da Secretária no ThinNav (secPowerApi, visível a todos).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:40:51 +02:00

119 lines
6.0 KiB
JavaScript

// Schema (idempotente) das tabelas de horário/feriado usadas pela Secretária.
// Chamado uma vez no registerNewwhats. Espelha os tipos de `dentistas_horarios`.
//
// clinicas_horarios — horário de FUNCIONAMENTO da clínica (config do DONO).
// feriados — EXCEÇÕES da clínica (fechamento/ponte/horário especial);
// os feriados NACIONAIS são computados (ver feriados.js).
export async function ensureAgendaSchema(pool) {
try {
await pool.query(`
CREATE TABLE IF NOT EXISTS clinicas_horarios (
id varchar PRIMARY KEY,
clinica_id varchar NOT NULL,
dia_semana integer NOT NULL, -- 0=domingo … 6=sábado
hora_inicio time without time zone NOT NULL,
hora_fim time without time zone NOT NULL,
ativo smallint DEFAULT 1
)`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_clinicas_horarios_clinica ON clinicas_horarios (clinica_id)`);
await pool.query(`
CREATE TABLE IF NOT EXISTS feriados (
id varchar PRIMARY KEY,
clinica_id varchar NOT NULL,
data date NOT NULL,
nome varchar NOT NULL,
fecha smallint DEFAULT 1, -- 1=fechado o dia; 0=abre em horário especial
hora_inicio time without time zone, -- horário especial (quando fecha=0)
hora_fim time without time zone,
recorrente smallint DEFAULT 0, -- 1=repete todo ano (mesmo dia/mês)
created_at timestamptz DEFAULT now()
)`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_feriados_clinica_data ON feriados (clinica_id, data)`);
// ── dentistas_folgas — folga/férias/indisponibilidade por DATA do dentista.
// O dentista (ou o dono) marca dias em que NÃO atende; o /slots pula.
await pool.query(`
CREATE TABLE IF NOT EXISTS dentistas_folgas (
id varchar PRIMARY KEY,
dentista_id varchar NOT NULL,
clinica_id varchar,
data_inicio date NOT NULL,
data_fim date NOT NULL, -- inclusive; = data_inicio p/ 1 dia
motivo varchar,
created_at timestamptz DEFAULT now()
)`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_dentistas_folgas_dent ON dentistas_folgas (dentista_id, data_inicio, data_fim)`);
// ── B (salas): agendamentos.sala_id — permite agendar num quarto alugado.
// Aditivo/nullable: NÃO afeta o fluxo existente (clínica) quando nulo.
await pool.query(`ALTER TABLE agendamentos ADD COLUMN IF NOT EXISTS sala_id varchar`);
// ── Janela ESTENDIDA do dentista (zona cinza): a IA agenda livre em
// [hora_inicio, hora_fim]; entre a estendida e a livre exige confirmação
// humana. Nulo = sem zona cinza (só a janela livre).
await pool.query(`ALTER TABLE dentistas_horarios ADD COLUMN IF NOT EXISTS hora_inicio_flex time without time zone`);
await pool.query(`ALTER TABLE dentistas_horarios ADD COLUMN IF NOT EXISTS hora_fim_flex time without time zone`);
// ── agenda_pedidos — pedidos da ZONA CINZA aguardando a secretária humana.
// A IA cria (status 'pendente') e diz "vou confirmar"; a humana confirma
// (vira agendamento) ou recusa.
await pool.query(`
CREATE TABLE IF NOT EXISTS agenda_pedidos (
id varchar PRIMARY KEY,
clinica_id varchar NOT NULL,
dentista_id varchar,
sala_id varchar,
data date NOT NULL,
hora_inicio time without time zone NOT NULL,
hora_fim time without time zone,
paciente_nome varchar,
paciente_telefone varchar,
procedimento varchar,
origem varchar DEFAULT 'secretaria_ia',
status varchar DEFAULT 'pendente', -- pendente | confirmado | recusado
agendamento_id varchar, -- preenchido ao confirmar
created_at timestamptz DEFAULT now(),
resolved_at timestamptz,
resolved_by varchar
)`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_agenda_pedidos_clinica_status ON agenda_pedidos (clinica_id, status, created_at)`);
// Pedido também serve para DÚVIDAS (sem horário): tipo + resumo; data/hora nullable.
await pool.query(`ALTER TABLE agenda_pedidos ADD COLUMN IF NOT EXISTS tipo varchar DEFAULT 'horario'`); // horario | duvida
await pool.query(`ALTER TABLE agenda_pedidos ADD COLUMN IF NOT EXISTS resumo text`);
await pool.query(`ALTER TABLE agenda_pedidos ALTER COLUMN data DROP NOT NULL`).catch(() => {});
await pool.query(`ALTER TABLE agenda_pedidos ALTER COLUMN hora_inicio DROP NOT NULL`).catch(() => {});
// ── agenda_situacoes — "área de situações": regras da clínica que a Secretária
// IA deve seguir (ex.: "plano X não cobre canal posterior — ofereça avaliação").
await pool.query(`
CREATE TABLE IF NOT EXISTS agenda_situacoes (
id varchar PRIMARY KEY,
clinica_id varchar NOT NULL,
texto text NOT NULL,
ativo smallint DEFAULT 1,
ordem integer DEFAULT 0,
created_at timestamptz DEFAULT now()
)`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_agenda_situacoes_clinica ON agenda_situacoes (clinica_id)`);
// ── agenda_checklist — respostas Sim/Não do DONO sobre a clínica (ver
// checklist-defs.js). Conferidas contra o banco → geram pendências de config
// e deixam a Secretária cautelosa. resposta: 1=sim, 0=não, NULL=não respondido.
await pool.query(`
CREATE TABLE IF NOT EXISTS agenda_checklist (
clinica_id varchar NOT NULL,
chave varchar NOT NULL,
resposta smallint,
updated_at timestamptz DEFAULT now(),
PRIMARY KEY (clinica_id, chave)
)`);
return true;
} catch (e) {
// Não derruba o boot do satélite se o banco estiver indisponível no momento.
console.error('[newwhats] ensureAgendaSchema falhou:', e.message);
return false;
}
}