feat(secretaria): horários de funcionamento + feriados na agenda/Secretária
A Secretária passa a saber quando a clínica está aberta e a NÃO oferecer horário/agendar em dia fechado (domingo, fora do horário, feriado). - feriados.js: feriados nacionais BR (fixos + móveis via Páscoa) + resolver com exceções da clínica. - agenda-schema.js: tabelas clinicas_horarios (horário do dono) e feriados (exceções/fechamentos), idempotentes. - agenda-config.js: CRUD com ownership (horário/feriado da clínica = dono; horário do dentista = ele mesmo ou o dono), em /api/nw/agenda-config. - agenda-bridge.js: GET /status (aberto hoje?, motivo, próximo aberto, grade, feriados) + /slots agora = clínica ∩ dentista − feriado. - HorariosConfig.tsx + botão no AgendaSettingsModal: telas de configuração (abas Clínica e Dentistas). Testado em dev: a Secretária ofereceu "amanhã" ao pedirem "hoje" num domingo, com os horários configurados da clínica. Obs.: index.js também traz trabalho acumulado do satélite (proxy de mídia via Wasabi/storage) que estava no working tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+26
-10
@@ -6,7 +6,7 @@
|
||||
// registerNewwhats(app, pool, { superadminGuard })
|
||||
import { createRoutes } from './routes.js'
|
||||
import { createWebhookReceiver } from './webhook-receiver.js'
|
||||
import { createRestProxy, provisionNewwhatsAccount } from './proxy.js'
|
||||
import { createRestProxy, createAccountsRoute, provisionNewwhatsAccount } from './proxy.js'
|
||||
import { getConfig, isConfigured, loadConfigFromDb, saveConfigToDb } from './config.js'
|
||||
import { Readable } from 'node:stream'
|
||||
|
||||
@@ -24,6 +24,8 @@ export async function provisionNewwhatsAccountEager(pool, email) {
|
||||
} catch { return false }
|
||||
}
|
||||
import { createAgendaBridge } from './agenda-bridge.js'
|
||||
import { createAgendaConfig } from './agenda-config.js'
|
||||
import { ensureAgendaSchema } from './agenda-schema.js'
|
||||
|
||||
// Valida uma chave contra o motor. Envia um email-sonda (a chave mestra exige
|
||||
// x-nw-email). 200 = ok com dados; 404 = chave válida mas email-sonda inexistente
|
||||
@@ -48,26 +50,39 @@ export function registerNewwhats(app, pool, opts = {}) {
|
||||
// Auth própria por segredo compartilhado (x-nw-agenda-secret). Server-to-server.
|
||||
app.use('/api/nw/agenda', createAgendaBridge(pool));
|
||||
|
||||
// ── Proxy de MÍDIA: /api/nw/media/* → motor /media/* ───────────────────────
|
||||
// A mídia do WhatsApp vive no motor (servida do Wasabi/local, pública). O
|
||||
// satélite a repassa mantendo same-origin no <img>/<video> (sem exigir header
|
||||
// de auth no browser), com suporte a Range para áudio/vídeo.
|
||||
// ── Schema de horários/feriados (idempotente) + config (dono/dentista) ─────
|
||||
// clinicas_horarios + feriados; usados pela agenda-bridge (Secretária) e pelas
|
||||
// telas de configuração do scoreodonto.
|
||||
ensureAgendaSchema(pool).catch((e) => console.error('[newwhats] schema agenda:', e?.message));
|
||||
app.use('/api/nw/agenda-config', createAgendaConfig(pool));
|
||||
|
||||
// ── Proxy de MÍDIA: /api/nw/media/* → motor ────────────────────────────────
|
||||
// A mídia/avatar vivem no motor (Wasabi ou local). O Wasabi é servido por
|
||||
// /api/storage/view/* (plugin UnifiedStorageProvider, que cobre Wasabi + fallback
|
||||
// local); a mídia local ANTIGA fica no /media estático. Tentamos o storage do
|
||||
// plugin primeiro e caímos no /media se der 404. Same-origin no <img>/<video>.
|
||||
app.use('/api/nw/media', async (req, res) => {
|
||||
if (req.method !== 'GET') { res.status(405).end(); return; }
|
||||
const cfg = getConfig();
|
||||
if (!cfg.motorUrl) { res.status(503).end(); return; }
|
||||
const rest = req.path.replace(/^\//, '');
|
||||
const url = `${cfg.motorUrl.replace(/\/$/, '')}/media/${rest}`;
|
||||
const base = cfg.motorUrl.replace(/\/$/, '');
|
||||
const headers = {};
|
||||
if (req.headers.range) headers.range = req.headers.range;
|
||||
const candidates = [`${base}/api/storage/view/${rest}`, `${base}/media/${rest}`];
|
||||
try {
|
||||
const headers = {};
|
||||
if (req.headers.range) headers.range = req.headers.range;
|
||||
const r = await fetch(url, { headers });
|
||||
let r = null;
|
||||
for (const url of candidates) {
|
||||
r = await fetch(url, { headers });
|
||||
if (r.status !== 404) break; // achou (ou erro que não é 404) → usa esse
|
||||
}
|
||||
res.status(r.status);
|
||||
for (const h of ['content-type', 'content-length', 'content-range', 'accept-ranges', 'cache-control', 'content-disposition']) {
|
||||
const v = r.headers.get(h);
|
||||
if (v) res.setHeader(h, v);
|
||||
}
|
||||
if (!res.getHeader('cache-control')) res.setHeader('cache-control', 'private, max-age=3600');
|
||||
// Mídia é imutável (path único por arquivo) → cache longo no browser.
|
||||
if (!res.getHeader('cache-control')) res.setHeader('cache-control', 'public, max-age=604800, immutable');
|
||||
if (r.body) Readable.fromWeb(r.body).pipe(res);
|
||||
else res.end();
|
||||
} catch (e) {
|
||||
@@ -160,6 +175,7 @@ export function registerNewwhats(app, pool, opts = {}) {
|
||||
// ── Operação ────────────────────────────────────────────────────────────────
|
||||
app.use('/nw', createRoutes(pool)); // Secretária do motor consulta a clínica
|
||||
app.use(createWebhookReceiver(pool)); // webhook motor → satélite
|
||||
app.get('/api/nw/accounts', createAccountsRoute(pool)); // seletor multi-conta da inbox (próprias + liberadas) — ANTES do catch-all
|
||||
app.use('/api/nw/v1', createRestProxy(pool)); // proxy do inbox → motor (express 5: use, não wildcard) + enforcement de ownership
|
||||
|
||||
console.log('[newwhats] integração registrada (/nw, /api/webhook/newwhats, /api/nw/v1/*, /api/nw/config)');
|
||||
|
||||
Reference in New Issue
Block a user