feat: version 4.13.0 -- Agregar medicos desde proveedores de SAGA (solo por API)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-29 17:15:12 -05:00
parent 0ee9ed65bf
commit d85a7196f0
21 changed files with 206 additions and 23 deletions
@@ -19,4 +19,6 @@ router.put('/sources/:purpose', h((req) => svc.saveSource(req.params.purpose, re
router.post('/sources/patients/sync', h((req) => svc.syncPatients({ full: !!req.body?.full, user: req.currentUser })));
router.get('/employees', h((req) => svc.listEmployees(req.currentUser, req.query)));
router.post('/employees/sync', h((req) => svc.syncEmployees(req.currentUser)));
router.get('/providers', h((req) => svc.searchProviders(req.query.search, req.currentUser)));
router.post('/providers/:code/import', h((req) => svc.importProvider(req.params.code, req.body || {}, req.currentUser), 201));
export default router;
+56 -2
View File
@@ -13,7 +13,8 @@ const exec = async (db, sql, params = []) => (await db.query(sql, params))[0];
const ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
const TARGETS = {
employees: { required: ['identification'], fields: ['identification', 'lastNames', 'firstNames', 'phone', 'phone2', 'birthDate', 'position', 'department', 'hireDate', 'leaveDate', 'active'] },
patients: { required: ['externalId', 'fullName'], fields: ['externalId', 'identification', 'fullName', 'phone', 'email'] }
patients: { required: ['externalId', 'fullName'], fields: ['externalId', 'identification', 'fullName', 'phone', 'email'] },
providers: { required: ['externalId', 'fullName'], fields: ['externalId', 'identification', 'fullName', 'phone', 'email'] }
};
const isSuperadmin = (user) => user.roleCode === 'superadmin';
@@ -163,7 +164,7 @@ export async function saveSource(purpose, raw, user) {
async function sourceClient(purpose) {
const src = await sourceRow(purpose);
if (!src) throw new HttpError(409, purpose === 'employees' ? 'No hay fuente de nómina configurada' : 'No hay fuente de pacientes configurada');
if (!src) throw new HttpError(409, { employees: 'No hay fuente de nómina configurada', patients: 'No hay fuente de pacientes configurada', providers: 'No hay fuente de proveedores configurada' }[purpose]);
const conn = await loadConnection(src.connection_id);
if (!conn.is_active) throw new HttpError(409, 'La conexión está desactivada');
return { src, conn: clientFor(conn) };
@@ -246,6 +247,59 @@ export async function linkEmployeeToUser(employeeId, userId, connection = pool)
if (!result.affectedRows) throw new HttpError(409, 'Ese empleado ya tiene usuario o no existe');
}
// --- Proveedores → empleados (médicos que facturan; admin y superadmin, a pedido) ---
const providerValues = (src, r) => {
const m = src.field_map;
const raw = clip(m.identification && r[m.identification], 40);
const email = clip(m.email && r[m.email], 160);
const phone = clip(m.phone && r[m.phone], 40);
return {
code: clip(r[m.externalId], 40),
// RUC de persona natural = cédula + "001".
identification: raw && /^\d{13}$/.test(raw) && raw.endsWith('001') ? raw.slice(0, 10) : raw,
fullName: clip(r[m.fullName], 120)?.replace(/^DRA?\.?\s+/i, '') || null,
phone: phone && !/^9+$/.test(phone) ? phone : null,
email: email && /^[^\s@,;]+@[^\s@,;]+\.[^\s@,;]+$/.test(email) ? email : null
};
};
export async function searchProviders(text, user) {
requireAdmin(user);
const { src, conn } = await sourceClient('providers');
const term = String(text || '').trim();
if (term.length < 3) return [];
const m = src.field_map;
const page = await connectorGet(conn, `/v1/sources/${encodeURIComponent(src.source_key)}/tables/${encodeURIComponent(src.table_name)}/rows`, {
q: term.slice(0, 60), qFields: [m.fullName, m.identification].filter(Boolean).join(','), fields: fieldsQuery(src), limit: 30, ...eqQuery(src.filter)
});
const list = page.rows.map((r) => providerValues(src, r)).filter((p) => p.code && p.fullName);
const idents = list.map((p) => p.identification).filter(Boolean);
const employees = idents.length ? new Map((await exec(pool, 'SELECT id, identification FROM employees WHERE identification IN (?)', [idents])).map((e) => [e.identification, e.id])) : new Map();
return list.map((p) => ({ ...p, employee_id: employees.get(p.identification) || null }));
}
export async function importProvider(code, raw, user) {
requireAdmin(user);
const { src, conn } = await sourceClient('providers');
const c = clip(code, 40);
const page = await connectorGet(conn, `/v1/sources/${encodeURIComponent(src.source_key)}/tables/${encodeURIComponent(src.table_name)}/rows`, {
[`eq.${src.field_map.externalId}`]: c, fields: fieldsQuery(src), limit: 1
});
if (!page.rows.length) throw new HttpError(404, 'El proveedor no existe en el sistema origen');
const p = providerValues(src, page.rows[0]);
if (!p.identification) throw new HttpError(400, 'El proveedor no tiene cédula/RUC');
// El orden de nombres en el origen no es fijo: apellidos y nombres los confirma quien agrega.
const last = clip(raw.lastNames, 120), first = clip(raw.firstNames, 120), position = clip(raw.position, 120);
if (!last || !first) throw new HttpError(400, 'Apellidos y nombres son obligatorios');
const [exists] = await exec(pool, 'SELECT id FROM employees WHERE identification=?', [p.identification]);
if (exists) throw new HttpError(409, 'Ya existe un empleado con esa cédula');
const result = await exec(pool, `INSERT INTO employees (identification,last_names,first_names,phone,email,position,is_active,origin,in_source,synced_at)
VALUES (?,?,?,?,?,?,1,'provider',0,?)`, [p.identification, last, first, p.phone, p.email, position, guayaquilNow()]);
await logAudit({ userId: user.id, action: 'import', entityType: 'employee', entityId: result.insertId, detail: { providerCode: c } });
return (await exec(pool, 'SELECT * FROM employees WHERE id=?', [result.insertId]))[0];
}
// --- Pacientes (clientes del origen) ---
const patientKey = (src) => `src${src.id}`;
@@ -10,14 +10,16 @@ import { pool } from '../config/db.js';
import { decrypt } from '../utils/crypto.js';
import { connectorGet } from '../modules/integrations/connector.js';
const SOURCE = 'clientes', TABLE = 'PROVEDOR';
const args = process.argv.slice(2);
const position = (args.find((a) => a.startsWith('--position=')) || '--position=MEDICO').split('=')[1];
const people = args.filter((a) => !a.startsWith('--')).map((a) => a.split('+'));
if (!people.length) { console.error('Uso: import-provider-employees.js [--position=X] COD[+COD] ...'); process.exit(1); }
const [conn] = (await pool.query("SELECT c.base_url, c.token_encrypted FROM external_sources s JOIN external_connections c ON c.id=s.connection_id WHERE s.purpose='patients' AND c.is_active=1"))[0];
if (!conn) { console.error('No hay conexión activa (Configuración → Conexiones externas)'); process.exit(1); }
// Usa la fuente "Proveedores" de Conexiones externas (mismos campos que la pantalla Empleados →
// Agregar desde proveedores); con PROVCOD/PROVNOM/PROVRUC/PROVTEL/PROVEMAIL como en SAGA.
const [conn] = (await pool.query("SELECT c.base_url, c.token_encrypted, s.source_key, s.table_name FROM external_sources s JOIN external_connections c ON c.id=s.connection_id WHERE s.purpose='providers' AND c.is_active=1"))[0];
if (!conn) { console.error('No hay fuente de proveedores activa (Configuración → Conexiones externas)'); process.exit(1); }
const SOURCE = conn.source_key, TABLE = conn.table_name;
const client = { baseUrl: conn.base_url, token: decrypt(conn.token_encrypted) };
const valid = (v) => v && !/^9+$/.test(v);