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:
@@ -60,6 +60,9 @@ CREATE TABLE IF NOT EXISTS employees (
|
||||
CONSTRAINT fk_employee_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Tercera fuente: proveedores (médicos que facturan), para buscarlos y agregarlos como empleados.
|
||||
ALTER TABLE external_sources MODIFY purpose ENUM('patients','employees','providers') NOT NULL;
|
||||
|
||||
-- Origen del empleado: nómina (se sincroniza) o proveedor de SAGA cargado a mano (médicos que facturan;
|
||||
-- la sincronización de nómina no los inactiva). Correo: los proveedores lo tienen, la nómina no.
|
||||
ALTER TABLE employees ADD COLUMN IF NOT EXISTS origin ENUM('payroll','provider') NOT NULL DEFAULT 'payroll' AFTER is_active;
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "project-linkedcorp-backend",
|
||||
"version": "4.12.0",
|
||||
"version": "4.13.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "project-linkedcorp-backend",
|
||||
"version": "4.12.0",
|
||||
"version": "4.13.0",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "project-linkedcorp-backend",
|
||||
"version": "4.12.0",
|
||||
"version": "4.13.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/server.js",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user