feat(areas): fundação multi-área (Odontologia/Biomedicina/Prótese) — Fase 1
- tabela referência 'areas' (slug, nome, cor, ícone) seedada com 3 áreas - coluna 'area' em especialidades e procedimentos + backfill idempotente - junções multi-área: clinica_areas e usuario_areas (backfill por papel/catálogo) - POST especialidades/procedimentos default de área pelo papel (roleToArea) + marca clinica_areas - seedBiomedicina grava area='biomedicina'; GET /api/areas - Fase 2 (próxima): seletor/badge de área no catálogo, filtro por área no marketplace, UI multi-área de perfil/clínica Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+51
-3
@@ -349,6 +349,13 @@ async function pushGoogleEvent(action, ag) {
|
||||
} catch (e) { console.error('[google-push]', e.message); }
|
||||
}
|
||||
|
||||
// Área padrão a partir do papel do usuário (default ao criar especialidade/procedimento).
|
||||
function roleToArea(role) {
|
||||
if (role === 'biomedico') return 'biomedicina';
|
||||
if (role === 'protetico') return 'protese';
|
||||
return 'odontologia';
|
||||
}
|
||||
|
||||
// Catálogo padrão de Biomedicina Estética (especialidades + procedimentos com valor sugerido).
|
||||
const BIOMED_ESPECIALIDADES = [
|
||||
'BIOMEDICINA ESTÉTICA', 'HARMONIZAÇÃO OROFACIAL', 'ESTÉTICA FACIAL', 'ESTÉTICA CORPORAL', 'PROCEDIMENTOS INJETÁVEIS',
|
||||
@@ -375,16 +382,18 @@ async function seedBiomedicina(clinicaId) {
|
||||
const espId = {};
|
||||
for (let i = 0; i < BIOMED_ESPECIALIDADES.length; i++) {
|
||||
const id = `esp_${Date.now()}_${i}_${Math.random().toString(36).slice(2, 6)}`;
|
||||
await pool.query('INSERT INTO especialidades (id, nome, clinica_id, ordem, ativo) VALUES ($1,$2,$3,$4,true)', [id, BIOMED_ESPECIALIDADES[i], clinicaId, i]);
|
||||
await pool.query("INSERT INTO especialidades (id, nome, clinica_id, ordem, ativo, area) VALUES ($1,$2,$3,$4,true,'biomedicina')", [id, BIOMED_ESPECIALIDADES[i], clinicaId, i]);
|
||||
espId[BIOMED_ESPECIALIDADES[i]] = id;
|
||||
}
|
||||
for (let i = 0; i < BIOMED_PROCEDIMENTOS.length; i++) {
|
||||
const pr = BIOMED_PROCEDIMENTOS[i];
|
||||
const id = `proc_${Date.now()}_${i}_${Math.random().toString(36).slice(2, 6)}`;
|
||||
await pool.query(
|
||||
'INSERT INTO procedimentos (id, nome, especialidadeid, especialidadenome, valorparticular, clinica_id, ordem, ativo) VALUES ($1,$2,$3,$4,$5,$6,$7,true)',
|
||||
"INSERT INTO procedimentos (id, nome, especialidadeid, especialidadenome, valorparticular, clinica_id, ordem, ativo, area) VALUES ($1,$2,$3,$4,$5,$6,$7,true,'biomedicina')",
|
||||
[id, pr.nome, espId[pr.esp] || null, pr.esp, pr.valor, clinicaId, i]);
|
||||
}
|
||||
// marca a clínica como área de biomedicina
|
||||
await pool.query("INSERT INTO clinica_areas (clinica_id, area) VALUES ($1,'biomedicina') ON CONFLICT DO NOTHING", [clinicaId]);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2645,10 +2654,12 @@ app.post('/api/especialidades', authGuard, async (req, res) => {
|
||||
const { rows: userRows } = await pool.query('SELECT role FROM usuarios WHERE id = $1', [req.authUser.userId]);
|
||||
const isSuperAdmin = userRows[0]?.role === 'superadmin';
|
||||
const clinicaId = isSuperAdmin ? null : (req.body.clinica_id || req.query.clinicaId || null);
|
||||
const area = req.body.area || roleToArea(userRows[0]?.role);
|
||||
const { rows: counts } = await pool.query('SELECT COUNT(*) as count FROM especialidades');
|
||||
const ordem = parseInt(counts[0].count);
|
||||
const ins = buildInsert('especialidades', { ...req.body, clinica_id: clinicaId, ordem });
|
||||
const ins = buildInsert('especialidades', { ...req.body, clinica_id: clinicaId, ordem, area });
|
||||
await pool.query(ins.text, ins.values);
|
||||
if (clinicaId && area) await pool.query('INSERT INTO clinica_areas (clinica_id, area) VALUES ($1,$2) ON CONFLICT DO NOTHING', [clinicaId, area]);
|
||||
res.json({ success: true });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
@@ -2779,10 +2790,12 @@ app.post('/api/procedimentos', authGuard, async (req, res) => {
|
||||
clinica_id: clinicaId,
|
||||
codigoInterno: codigoInternoVal,
|
||||
ordem: ordemValue,
|
||||
area: proc.area || roleToArea(userRows[0]?.role),
|
||||
tipo_regiao: proc.tipo_regiao || 'GERAL',
|
||||
exige_face: proc.exige_face ? 1 : 0
|
||||
};
|
||||
if (!payload.id) payload.id = Math.random().toString(36).substring(2, 10).toUpperCase();
|
||||
if (clinicaId && payload.area) await pool.query('INSERT INTO clinica_areas (clinica_id, area) VALUES ($1,$2) ON CONFLICT DO NOTHING', [clinicaId, payload.area]);
|
||||
|
||||
await withTransaction(async (client) => {
|
||||
const ins = buildInsert('procedimentos', payload);
|
||||
@@ -4057,6 +4070,33 @@ async function runMigrations() {
|
||||
`ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS numero TEXT`,
|
||||
`ALTER TABLE especialidades ADD COLUMN IF NOT EXISTS ativo BOOLEAN DEFAULT true`,
|
||||
`ALTER TABLE procedimentos ADD COLUMN IF NOT EXISTS ativo BOOLEAN DEFAULT true`,
|
||||
// ── Áreas de atuação (Odontologia / Biomedicina / Prótese) ──────────
|
||||
`CREATE TABLE IF NOT EXISTS areas (
|
||||
slug TEXT PRIMARY KEY, nome TEXT NOT NULL, cor TEXT, icone TEXT, ordem INT DEFAULT 0
|
||||
)`,
|
||||
`INSERT INTO areas (slug, nome, cor, icone, ordem) VALUES
|
||||
('odontologia','Odontologia','#2563eb','Stethoscope',1),
|
||||
('biomedicina','Biomedicina Estética','#db2777','Sparkles',2),
|
||||
('protese','Prótese','#7c3aed','FlaskConical',3)
|
||||
ON CONFLICT (slug) DO NOTHING`,
|
||||
`ALTER TABLE especialidades ADD COLUMN IF NOT EXISTS area TEXT`,
|
||||
`ALTER TABLE procedimentos ADD COLUMN IF NOT EXISTS area TEXT`,
|
||||
// Multi-área por clínica e por usuário (uma unidade pode ter odonto + biomedicina).
|
||||
`CREATE TABLE IF NOT EXISTS clinica_areas (clinica_id TEXT NOT NULL, area TEXT NOT NULL, PRIMARY KEY (clinica_id, area))`,
|
||||
`CREATE TABLE IF NOT EXISTS usuario_areas (usuario_id TEXT NOT NULL, area TEXT NOT NULL, PRIMARY KEY (usuario_id, area))`,
|
||||
// Backfill idempotente do catálogo (só onde area ainda é NULL).
|
||||
`UPDATE especialidades SET area = 'biomedicina' WHERE area IS NULL AND nome IN ('BIOMEDICINA ESTÉTICA','ESTÉTICA FACIAL','ESTÉTICA CORPORAL','PROCEDIMENTOS INJETÁVEIS')`,
|
||||
`UPDATE especialidades SET area = 'odontologia' WHERE area IS NULL`,
|
||||
`UPDATE procedimentos SET area = 'biomedicina' WHERE area IS NULL AND nome IN ('TOXINA BOTULÍNICA (BOTOX)','PREENCHIMENTO COM ÁCIDO HIALURÔNICO','BIOESTIMULADOR DE COLÁGENO','SKINBOOSTER','PEELING QUÍMICO','MICROAGULHAMENTO','LIMPEZA DE PELE','FIOS DE SUSTENTAÇÃO (PDO)','HARMONIZAÇÃO FACIAL','ESCLEROTERAPIA (VASINHOS)','INTRADERMOTERAPIA / MESOTERAPIA','LIPÓLISE ENZIMÁTICA')`,
|
||||
`UPDATE procedimentos SET area = 'odontologia' WHERE area IS NULL`,
|
||||
// Backfill das junções: usuário pela role; clínica pelas áreas do catálogo.
|
||||
`INSERT INTO usuario_areas (usuario_id, area)
|
||||
SELECT id, CASE role WHEN 'biomedico' THEN 'biomedicina' WHEN 'protetico' THEN 'protese' ELSE 'odontologia' END
|
||||
FROM usuarios WHERE role IN ('dentista','biomedico','protetico','donoclinica','donoconsultorio','funcionario','admin')
|
||||
ON CONFLICT DO NOTHING`,
|
||||
`INSERT INTO clinica_areas (clinica_id, area)
|
||||
SELECT DISTINCT clinica_id, area FROM especialidades WHERE clinica_id IS NOT NULL AND area IS NOT NULL
|
||||
ON CONFLICT DO NOTHING`,
|
||||
`ALTER TABLE clinicas ADD COLUMN IF NOT EXISTS geo geography(Point,4326)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_salas_geo ON salas USING GIST (geo)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_usuarios_geo ON usuarios USING GIST (geo)`,
|
||||
@@ -4216,6 +4256,14 @@ app.post('/api/me/plugins', authGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// Lista as áreas de atuação (referência).
|
||||
app.get('/api/areas', authGuard, async (req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query('SELECT slug, nome, cor, icone, ordem FROM areas ORDER BY ordem');
|
||||
res.json(rows);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// Semeia o catálogo de biomedicina estética na clínica/consultório do usuário (idempotente).
|
||||
// Usado no 1º acesso do biomédico ao catálogo (cobre contas criadas antes do auto-seed).
|
||||
app.post('/api/me/seed-biomedicina', authGuard, async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user