paperclip-plugin-autopilot-ops 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/manifest.js +12 -4
- package/dist/ui/index.js +12 -4
- package/dist/worker.js +409 -23
- package/migrations/003_opciones_politica.sql +6 -0
- package/package.json +1 -1
package/dist/manifest.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/manifest.ts
|
|
2
2
|
var PLUGIN_ID = "alephoo.autopilot-ops";
|
|
3
|
-
var PLUGIN_VERSION = "0.1.
|
|
3
|
+
var PLUGIN_VERSION = "0.1.7";
|
|
4
4
|
var DB_NAMESPACE_SLUG = "autopilot_ops";
|
|
5
5
|
var PAGE_ROUTE = "autopilot";
|
|
6
6
|
var TOOL_NAMES = {
|
|
@@ -34,7 +34,7 @@ var manifest = {
|
|
|
34
34
|
apiVersion: 1,
|
|
35
35
|
version: PLUGIN_VERSION,
|
|
36
36
|
displayName: "Autopilot Care",
|
|
37
|
-
description: "Operaci\xF3n aut\xF3noma gobernada para instituciones de salud: workflows determin\xEDsticos por puesto, validaciones, registro de negocio con evidencia, pol\xEDticas (observa / propone / ejecuta / aut\xF3nomo / corte), carga manual del equipo del hospital con comprobaci\xF3n del agente, tableros de gesti\xF3n publicados por agentes y panel del director.",
|
|
37
|
+
description: "Operaci\xF3n aut\xF3noma gobernada para instituciones de salud: workflows determin\xEDsticos por puesto, validaciones, registro de negocio con evidencia, pol\xEDticas (observa / propone / ejecuta / aut\xF3nomo / corte), carga manual del equipo del hospital con comprobaci\xF3n del agente, recupero de turnos liberados desde la lista de espera, tableros de gesti\xF3n publicados por agentes y panel del director.",
|
|
38
38
|
author: "Alephoo",
|
|
39
39
|
categories: ["automation", "ui"],
|
|
40
40
|
capabilities: [
|
|
@@ -84,7 +84,7 @@ var manifest = {
|
|
|
84
84
|
{
|
|
85
85
|
name: TOOL_NAMES.abrirCaso,
|
|
86
86
|
displayName: "Abrir caso",
|
|
87
|
-
description:
|
|
87
|
+
description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `actualizacion_arancel`, `tarea_operativa`, `recupero_turno` (datos: fecha AAAA-MM-DD, hora HH:MM, profesional, especialidad del turno libre; referencia "AAAA-MM-DD HH:MM \xB7 Profesional"). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.',
|
|
88
88
|
parametersSchema: {
|
|
89
89
|
type: "object",
|
|
90
90
|
properties: {
|
|
@@ -101,7 +101,7 @@ var manifest = {
|
|
|
101
101
|
{
|
|
102
102
|
name: TOOL_NAMES.transicionar,
|
|
103
103
|
displayName: "Transicionar caso",
|
|
104
|
-
description: "Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona del hospital carga a mano lo aprobado, el caso queda en `aplicado`: el agente lo comprueba releyendo el sistema y llamando `verificar` con `datos.valor_leido`.",
|
|
104
|
+
description: "Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona del hospital carga a mano lo aprobado, el caso queda en `aplicado`: el agente lo comprueba releyendo el sistema y llamando `verificar` con `datos.valor_leido`. En `recupero_turno`: `proponer` con solicitud_id, solicitud_estado, solicitud_especialidad, cola (la lista de espera pendiente de esa especialidad tal como se vio: [{solicitud_id, prioridad, alta}]; el plugin la ordena y exige justificaci\xF3n para no elegir la primera) y fuente; si la instituci\xF3n pide consulta previa (ops_leer_caso lo dice), `ofrecer` con canal y esperar `aceptar`/`no_acepta`; **antes de escribir en el sistema, `autorizar`** (sin esa autorizaci\xF3n no se escribe); despu\xE9s de escribir, `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo.",
|
|
105
105
|
parametersSchema: {
|
|
106
106
|
type: "object",
|
|
107
107
|
properties: {
|
|
@@ -221,6 +221,14 @@ var manifest = {
|
|
|
221
221
|
capability: "api.routes.register",
|
|
222
222
|
companyResolution: { from: "body", key: "companyId" }
|
|
223
223
|
},
|
|
224
|
+
{
|
|
225
|
+
routeKey: "respuesta-paciente",
|
|
226
|
+
method: "POST",
|
|
227
|
+
path: "/casos/:casoId/respuesta",
|
|
228
|
+
auth: "board",
|
|
229
|
+
capability: "api.routes.register",
|
|
230
|
+
companyResolution: { from: "body", key: "companyId" }
|
|
231
|
+
},
|
|
224
232
|
{
|
|
225
233
|
routeKey: "version",
|
|
226
234
|
method: "GET",
|
package/dist/ui/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
|
|
9
9
|
// src/manifest.ts
|
|
10
10
|
var PLUGIN_ID = "alephoo.autopilot-ops";
|
|
11
|
-
var PLUGIN_VERSION = "0.1.
|
|
11
|
+
var PLUGIN_VERSION = "0.1.7";
|
|
12
12
|
var DB_NAMESPACE_SLUG = "autopilot_ops";
|
|
13
13
|
var PAGE_ROUTE = "autopilot";
|
|
14
14
|
var TOOL_NAMES = {
|
|
@@ -42,7 +42,7 @@ var manifest = {
|
|
|
42
42
|
apiVersion: 1,
|
|
43
43
|
version: PLUGIN_VERSION,
|
|
44
44
|
displayName: "Autopilot Care",
|
|
45
|
-
description: "Operaci\xF3n aut\xF3noma gobernada para instituciones de salud: workflows determin\xEDsticos por puesto, validaciones, registro de negocio con evidencia, pol\xEDticas (observa / propone / ejecuta / aut\xF3nomo / corte), carga manual del equipo del hospital con comprobaci\xF3n del agente, tableros de gesti\xF3n publicados por agentes y panel del director.",
|
|
45
|
+
description: "Operaci\xF3n aut\xF3noma gobernada para instituciones de salud: workflows determin\xEDsticos por puesto, validaciones, registro de negocio con evidencia, pol\xEDticas (observa / propone / ejecuta / aut\xF3nomo / corte), carga manual del equipo del hospital con comprobaci\xF3n del agente, recupero de turnos liberados desde la lista de espera, tableros de gesti\xF3n publicados por agentes y panel del director.",
|
|
46
46
|
author: "Alephoo",
|
|
47
47
|
categories: ["automation", "ui"],
|
|
48
48
|
capabilities: [
|
|
@@ -92,7 +92,7 @@ var manifest = {
|
|
|
92
92
|
{
|
|
93
93
|
name: TOOL_NAMES.abrirCaso,
|
|
94
94
|
displayName: "Abrir caso",
|
|
95
|
-
description:
|
|
95
|
+
description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `actualizacion_arancel`, `tarea_operativa`, `recupero_turno` (datos: fecha AAAA-MM-DD, hora HH:MM, profesional, especialidad del turno libre; referencia "AAAA-MM-DD HH:MM \xB7 Profesional"). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.',
|
|
96
96
|
parametersSchema: {
|
|
97
97
|
type: "object",
|
|
98
98
|
properties: {
|
|
@@ -109,7 +109,7 @@ var manifest = {
|
|
|
109
109
|
{
|
|
110
110
|
name: TOOL_NAMES.transicionar,
|
|
111
111
|
displayName: "Transicionar caso",
|
|
112
|
-
description: "Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona del hospital carga a mano lo aprobado, el caso queda en `aplicado`: el agente lo comprueba releyendo el sistema y llamando `verificar` con `datos.valor_leido`.",
|
|
112
|
+
description: "Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona del hospital carga a mano lo aprobado, el caso queda en `aplicado`: el agente lo comprueba releyendo el sistema y llamando `verificar` con `datos.valor_leido`. En `recupero_turno`: `proponer` con solicitud_id, solicitud_estado, solicitud_especialidad, cola (la lista de espera pendiente de esa especialidad tal como se vio: [{solicitud_id, prioridad, alta}]; el plugin la ordena y exige justificaci\xF3n para no elegir la primera) y fuente; si la instituci\xF3n pide consulta previa (ops_leer_caso lo dice), `ofrecer` con canal y esperar `aceptar`/`no_acepta`; **antes de escribir en el sistema, `autorizar`** (sin esa autorizaci\xF3n no se escribe); despu\xE9s de escribir, `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo.",
|
|
113
113
|
parametersSchema: {
|
|
114
114
|
type: "object",
|
|
115
115
|
properties: {
|
|
@@ -229,6 +229,14 @@ var manifest = {
|
|
|
229
229
|
capability: "api.routes.register",
|
|
230
230
|
companyResolution: { from: "body", key: "companyId" }
|
|
231
231
|
},
|
|
232
|
+
{
|
|
233
|
+
routeKey: "respuesta-paciente",
|
|
234
|
+
method: "POST",
|
|
235
|
+
path: "/casos/:casoId/respuesta",
|
|
236
|
+
auth: "board",
|
|
237
|
+
capability: "api.routes.register",
|
|
238
|
+
companyResolution: { from: "body", key: "companyId" }
|
|
239
|
+
},
|
|
232
240
|
{
|
|
233
241
|
routeKey: "version",
|
|
234
242
|
method: "GET",
|
package/dist/worker.js
CHANGED
|
@@ -39932,7 +39932,7 @@ function startWorkerRpcHost(options) {
|
|
|
39932
39932
|
|
|
39933
39933
|
// src/manifest.ts
|
|
39934
39934
|
var PLUGIN_ID = "alephoo.autopilot-ops";
|
|
39935
|
-
var PLUGIN_VERSION = "0.1.
|
|
39935
|
+
var PLUGIN_VERSION = "0.1.7";
|
|
39936
39936
|
var DB_NAMESPACE_SLUG = "autopilot_ops";
|
|
39937
39937
|
var PAGE_ROUTE = "autopilot";
|
|
39938
39938
|
var TOOL_NAMES = {
|
|
@@ -39966,7 +39966,7 @@ var manifest = {
|
|
|
39966
39966
|
apiVersion: 1,
|
|
39967
39967
|
version: PLUGIN_VERSION,
|
|
39968
39968
|
displayName: "Autopilot Care",
|
|
39969
|
-
description: "Operaci\xF3n aut\xF3noma gobernada para instituciones de salud: workflows determin\xEDsticos por puesto, validaciones, registro de negocio con evidencia, pol\xEDticas (observa / propone / ejecuta / aut\xF3nomo / corte), carga manual del equipo del hospital con comprobaci\xF3n del agente, tableros de gesti\xF3n publicados por agentes y panel del director.",
|
|
39969
|
+
description: "Operaci\xF3n aut\xF3noma gobernada para instituciones de salud: workflows determin\xEDsticos por puesto, validaciones, registro de negocio con evidencia, pol\xEDticas (observa / propone / ejecuta / aut\xF3nomo / corte), carga manual del equipo del hospital con comprobaci\xF3n del agente, recupero de turnos liberados desde la lista de espera, tableros de gesti\xF3n publicados por agentes y panel del director.",
|
|
39970
39970
|
author: "Alephoo",
|
|
39971
39971
|
categories: ["automation", "ui"],
|
|
39972
39972
|
capabilities: [
|
|
@@ -40016,7 +40016,7 @@ var manifest = {
|
|
|
40016
40016
|
{
|
|
40017
40017
|
name: TOOL_NAMES.abrirCaso,
|
|
40018
40018
|
displayName: "Abrir caso",
|
|
40019
|
-
description:
|
|
40019
|
+
description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `actualizacion_arancel`, `tarea_operativa`, `recupero_turno` (datos: fecha AAAA-MM-DD, hora HH:MM, profesional, especialidad del turno libre; referencia "AAAA-MM-DD HH:MM \xB7 Profesional"). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.',
|
|
40020
40020
|
parametersSchema: {
|
|
40021
40021
|
type: "object",
|
|
40022
40022
|
properties: {
|
|
@@ -40033,7 +40033,7 @@ var manifest = {
|
|
|
40033
40033
|
{
|
|
40034
40034
|
name: TOOL_NAMES.transicionar,
|
|
40035
40035
|
displayName: "Transicionar caso",
|
|
40036
|
-
description: "Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona del hospital carga a mano lo aprobado, el caso queda en `aplicado`: el agente lo comprueba releyendo el sistema y llamando `verificar` con `datos.valor_leido`.",
|
|
40036
|
+
description: "Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona del hospital carga a mano lo aprobado, el caso queda en `aplicado`: el agente lo comprueba releyendo el sistema y llamando `verificar` con `datos.valor_leido`. En `recupero_turno`: `proponer` con solicitud_id, solicitud_estado, solicitud_especialidad, cola (la lista de espera pendiente de esa especialidad tal como se vio: [{solicitud_id, prioridad, alta}]; el plugin la ordena y exige justificaci\xF3n para no elegir la primera) y fuente; si la instituci\xF3n pide consulta previa (ops_leer_caso lo dice), `ofrecer` con canal y esperar `aceptar`/`no_acepta`; **antes de escribir en el sistema, `autorizar`** (sin esa autorizaci\xF3n no se escribe); despu\xE9s de escribir, `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo.",
|
|
40037
40037
|
parametersSchema: {
|
|
40038
40038
|
type: "object",
|
|
40039
40039
|
properties: {
|
|
@@ -40153,6 +40153,14 @@ var manifest = {
|
|
|
40153
40153
|
capability: "api.routes.register",
|
|
40154
40154
|
companyResolution: { from: "body", key: "companyId" }
|
|
40155
40155
|
},
|
|
40156
|
+
{
|
|
40157
|
+
routeKey: "respuesta-paciente",
|
|
40158
|
+
method: "POST",
|
|
40159
|
+
path: "/casos/:casoId/respuesta",
|
|
40160
|
+
auth: "board",
|
|
40161
|
+
capability: "api.routes.register",
|
|
40162
|
+
companyResolution: { from: "body", key: "companyId" }
|
|
40163
|
+
},
|
|
40156
40164
|
{
|
|
40157
40165
|
routeKey: "version",
|
|
40158
40166
|
method: "GET",
|
|
@@ -40409,9 +40417,300 @@ var tareaOperativa = {
|
|
|
40409
40417
|
{ via: "cancelar", de: ["abierta", "en_curso", "pendiente_aprobacion"], a: "cancelada", actor: ["humano"], descripcion: "Cancelaci\xF3n manual." }
|
|
40410
40418
|
]
|
|
40411
40419
|
};
|
|
40420
|
+
var TOPE_DIARIO_RECUPERO = 20;
|
|
40421
|
+
var TITULOS = /* @__PURE__ */ new Set(["dr", "dra", "doctor", "doctora", "lic", "licenciado", "licenciada", "prof"]);
|
|
40422
|
+
function normalizarNombre(v) {
|
|
40423
|
+
const s = texto(v);
|
|
40424
|
+
if (!s) return null;
|
|
40425
|
+
const palabras = s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").split(" ").filter((p) => p && !TITULOS.has(p));
|
|
40426
|
+
return palabras.length ? palabras.sort().join(" ") : null;
|
|
40427
|
+
}
|
|
40428
|
+
function horaHM(v) {
|
|
40429
|
+
const s = texto(v);
|
|
40430
|
+
const m = s ? /^(\d{1,2}):(\d{2})(?::\d{2})?$/.exec(s) : null;
|
|
40431
|
+
if (!m) return null;
|
|
40432
|
+
const h = Number(m[1]);
|
|
40433
|
+
const min = Number(m[2]);
|
|
40434
|
+
return h < 24 && min < 60 ? `${String(h).padStart(2, "0")}:${m[2]}` : null;
|
|
40435
|
+
}
|
|
40436
|
+
function refExterna(v) {
|
|
40437
|
+
if (typeof v === "number" && Number.isFinite(v)) return String(v);
|
|
40438
|
+
return texto(v);
|
|
40439
|
+
}
|
|
40440
|
+
function fechaAISO(v) {
|
|
40441
|
+
const s = texto(v);
|
|
40442
|
+
if (!s) return null;
|
|
40443
|
+
if (fechaISO(s.slice(0, 10))) return s.slice(0, 10);
|
|
40444
|
+
const m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})/.exec(s);
|
|
40445
|
+
if (!m) return null;
|
|
40446
|
+
const iso3 = `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`;
|
|
40447
|
+
return fechaISO(iso3) ? iso3 : null;
|
|
40448
|
+
}
|
|
40449
|
+
function leerCola(v) {
|
|
40450
|
+
if (!Array.isArray(v) || v.length === 0) return null;
|
|
40451
|
+
const out = [];
|
|
40452
|
+
for (const e of v) {
|
|
40453
|
+
if (!e || typeof e !== "object") return null;
|
|
40454
|
+
const r = e;
|
|
40455
|
+
const id = refExterna(r.solicitud_id);
|
|
40456
|
+
if (!id) return null;
|
|
40457
|
+
out.push({ solicitud_id: id, prioridad: texto(r.prioridad), alta: fechaAISO(r.alta) });
|
|
40458
|
+
}
|
|
40459
|
+
return out;
|
|
40460
|
+
}
|
|
40461
|
+
function ordenarCola(cola) {
|
|
40462
|
+
const pronta = (e) => normalizarNombre(e.prioridad)?.includes("pronta") ? 0 : 1;
|
|
40463
|
+
return [...cola].sort((a, b) => pronta(a) - pronta(b) || (a.alta ?? "9999-12-31").localeCompare(b.alta ?? "9999-12-31") || (Number(a.solicitud_id) || 0) - (Number(b.solicitud_id) || 0) || a.solicitud_id.localeCompare(b.solicitud_id));
|
|
40464
|
+
}
|
|
40465
|
+
function posicionEnCola(datos, enCurso = []) {
|
|
40466
|
+
const cola = leerCola(datos.cola);
|
|
40467
|
+
const elegida = refExterna(datos.solicitud_id);
|
|
40468
|
+
const fuera = new Set(enCurso);
|
|
40469
|
+
const vigente = cola?.filter((e) => e.solicitud_id === elegida || !fuera.has(e.solicitud_id)) ?? null;
|
|
40470
|
+
if (!vigente || !elegida) return { posicion: null, total: vigente?.length ?? 0 };
|
|
40471
|
+
const i = ordenarCola(vigente).findIndex((e) => e.solicitud_id === elegida);
|
|
40472
|
+
return { posicion: i === -1 ? null : i + 1, total: vigente.length };
|
|
40473
|
+
}
|
|
40474
|
+
function fueraDePreferencia(preferencia, hora) {
|
|
40475
|
+
const p = normalizarNombre(preferencia);
|
|
40476
|
+
if (!p || !hora) return false;
|
|
40477
|
+
if (p.includes("manana")) return hora >= "13:00";
|
|
40478
|
+
if (p.includes("tarde")) return hora < "13:00";
|
|
40479
|
+
return false;
|
|
40480
|
+
}
|
|
40481
|
+
var MARGEN_MINUTOS_ASIGNAR = 60;
|
|
40482
|
+
function minutosHastaLaCita(l, contexto) {
|
|
40483
|
+
const hora = horaHM(l.hora);
|
|
40484
|
+
if (!fechaISO(l.fecha) || !hora || !contexto.hoy || !contexto.horaActual) return null;
|
|
40485
|
+
const cita = Date.parse(`${String(l.fecha)}T${hora}:00Z`);
|
|
40486
|
+
const ahora = Date.parse(`${contexto.hoy}T${contexto.horaActual}:00Z`);
|
|
40487
|
+
return Number.isNaN(cita) || Number.isNaN(ahora) ? null : Math.round((cita - ahora) / 6e4);
|
|
40488
|
+
}
|
|
40489
|
+
function validarMargen(caso, contexto) {
|
|
40490
|
+
const minutos = minutosHastaLaCita(caso.datos, contexto);
|
|
40491
|
+
if (minutos === null) return ok("margen_para_avisar", "sin fecha y hora para medir");
|
|
40492
|
+
return minutos >= MARGEN_MINUTOS_ASIGNAR ? ok("margen_para_avisar", `faltan ${minutos} min`) : falla("margen_para_avisar", minutos <= 0 ? "la cita ya pas\xF3" : `la cita es en ${minutos} min: hacen falta al menos ${MARGEN_MINUTOS_ASIGNAR} para que el paciente se entere`);
|
|
40493
|
+
}
|
|
40494
|
+
function validarPacienteAcepto(caso, contexto) {
|
|
40495
|
+
if (!contexto.ofrecerPrimero) return ok("paciente_acepto", "la instituci\xF3n no pide consulta previa");
|
|
40496
|
+
return caso.estado === "aceptado" ? ok("paciente_acepto", "el paciente acept\xF3") : falla("paciente_acepto", "en esta instituci\xF3n el paciente tiene que aceptar la cita antes de asignarla: primero ofrecer y esperar que acepte");
|
|
40497
|
+
}
|
|
40498
|
+
function declinadas(caso) {
|
|
40499
|
+
return Array.isArray(caso.datos.declinaron) ? caso.datos.declinaron.map((v) => refExterna(v)).filter((v) => v !== null) : [];
|
|
40500
|
+
}
|
|
40501
|
+
function validarLugar(caso, datos, contexto) {
|
|
40502
|
+
const l = { ...caso.datos, ...datos };
|
|
40503
|
+
const completo = fechaISO(l.fecha) && horaHM(l.hora) !== null && texto(l.profesional) !== null && texto(l.especialidad) !== null;
|
|
40504
|
+
const out = [completo ? ok("lugar_completo") : falla("lugar_completo", "el turno liberado necesita fecha (AAAA-MM-DD), hora (HH:MM), profesional y especialidad")];
|
|
40505
|
+
if (fechaISO(l.fecha) && contexto.hoy) {
|
|
40506
|
+
const fecha = String(l.fecha);
|
|
40507
|
+
const hora = horaHM(l.hora);
|
|
40508
|
+
const futuro = fecha > contexto.hoy || fecha === contexto.hoy && (!contexto.horaActual || hora !== null && hora > contexto.horaActual);
|
|
40509
|
+
out.push(futuro ? ok("lugar_futuro") : falla("lugar_futuro", `el turno del ${fecha}${hora ? ` a las ${hora}` : ""} ya pas\xF3: no se puede recuperar`));
|
|
40510
|
+
}
|
|
40511
|
+
return out;
|
|
40512
|
+
}
|
|
40513
|
+
var recuperoTurno = {
|
|
40514
|
+
clave: "recupero_turno",
|
|
40515
|
+
nombre: "Recupero de citas",
|
|
40516
|
+
puesto: "Pacientes y agenda",
|
|
40517
|
+
descripcion: "Detecta un turno que se liber\xF3 en la agenda, elige de la lista de espera (solicitudes de turno) a quien corresponde por especialidad, prioridad y antig\xFCedad, le asigna el turno en el sistema de gesti\xF3n y comprueba reley\xE9ndolo que qued\xF3 asignado.",
|
|
40518
|
+
inicial: "detectado",
|
|
40519
|
+
estados: [
|
|
40520
|
+
"detectado",
|
|
40521
|
+
"pendiente_aprobacion",
|
|
40522
|
+
"aprobado",
|
|
40523
|
+
"ofrecido",
|
|
40524
|
+
"aceptado",
|
|
40525
|
+
"asignando",
|
|
40526
|
+
"asignado",
|
|
40527
|
+
"verificado",
|
|
40528
|
+
"con_diferencias",
|
|
40529
|
+
"sin_candidato",
|
|
40530
|
+
"rechazado",
|
|
40531
|
+
"cancelado"
|
|
40532
|
+
],
|
|
40533
|
+
finalesOk: ["verificado"],
|
|
40534
|
+
finalesNoOk: ["sin_candidato", "rechazado", "cancelado"],
|
|
40535
|
+
minutosAbsorbidos: 15,
|
|
40536
|
+
transiciones: [
|
|
40537
|
+
{
|
|
40538
|
+
via: "proponer",
|
|
40539
|
+
de: ["detectado", "con_diferencias"],
|
|
40540
|
+
a: "pendiente_aprobacion",
|
|
40541
|
+
actor: ["agente", "humano"],
|
|
40542
|
+
nivelRequerido: "propone",
|
|
40543
|
+
descripcion: "Proponer a qui\xE9n darle el turno liberado: una solicitud pendiente de la misma especialidad, informando la cola completa que se vio; el plugin la ordena (pronta respuesta primero, despu\xE9s antig\xFCedad) y elegir otra que no sea la primera exige justificaci\xF3n.",
|
|
40544
|
+
validar: (caso, datos, contexto) => {
|
|
40545
|
+
const l = { ...caso.datos, ...datos };
|
|
40546
|
+
const out = validarLugar(caso, datos, contexto);
|
|
40547
|
+
out.push(refExterna(datos.solicitud_id) ? ok("solicitud_presente") : falla("solicitud_presente", "proponer requiere solicitud_id: la solicitud de turno de la lista de espera"));
|
|
40548
|
+
const estado = normalizarNombre(datos.solicitud_estado);
|
|
40549
|
+
out.push(estado === "pendiente" ? ok("solicitud_pendiente") : falla("solicitud_pendiente", `la solicitud tiene que estar pendiente (est\xE1: ${texto(datos.solicitud_estado) ?? "sin informar"})`));
|
|
40550
|
+
const esp = normalizarNombre(l.especialidad);
|
|
40551
|
+
const espSolicitud = normalizarNombre(datos.solicitud_especialidad);
|
|
40552
|
+
out.push(esp && espSolicitud && esp === espSolicitud ? ok("especialidad_coincide") : falla("especialidad_coincide", `la solicitud es de ${texto(datos.solicitud_especialidad) ?? "(sin especialidad)"} y el turno es de ${texto(l.especialidad) ?? "(sin especialidad)"}`));
|
|
40553
|
+
out.push(contexto.solicitudEnOtroCaso ? falla("solicitud_libre", `esa solicitud ya tiene otro recupero en curso o asignado (${contexto.solicitudEnOtroCaso})`) : ok("solicitud_libre"));
|
|
40554
|
+
const yaNo = declinadas(caso);
|
|
40555
|
+
const elegida = refExterna(datos.solicitud_id);
|
|
40556
|
+
out.push(elegida && yaNo.includes(elegida) ? falla("no_la_rechazo", "el paciente de esa solicitud ya no acept\xF3 esta cita: proponer a la siguiente") : ok("no_la_rechazo"));
|
|
40557
|
+
const { posicion, total } = posicionEnCola(datos, [...contexto.solicitudesEnCurso ?? [], ...yaNo]);
|
|
40558
|
+
if (!leerCola(datos.cola)) out.push(falla("orden_de_la_cola", "cola es obligatoria: la lista de espera pendiente de esa especialidad tal como se vio, [{solicitud_id, prioridad, alta}]"));
|
|
40559
|
+
else if (posicion === null) out.push(falla("orden_de_la_cola", "la solicitud elegida no est\xE1 en la cola informada"));
|
|
40560
|
+
else if (posicion > 1 && !texto(datos.justificacion)) out.push(falla("orden_de_la_cola", `es la ${posicion}.\xAA de ${total} en la cola: pasarla adelante de las anteriores requiere justificacion`));
|
|
40561
|
+
else out.push(ok("orden_de_la_cola", posicion === 1 ? `primera de ${total} en la cola` : `${posicion}.\xAA de ${total} en la cola, con justificaci\xF3n`));
|
|
40562
|
+
out.push(texto(datos.fuente) ? ok("fuente_presente") : falla("fuente_presente", "fuente (pantalla donde se vio el turno libre y la solicitud) es obligatoria"));
|
|
40563
|
+
return out;
|
|
40564
|
+
},
|
|
40565
|
+
autonomia: {
|
|
40566
|
+
aSinExcepcion: "aprobado",
|
|
40567
|
+
excepciones: (caso, datos, contexto) => {
|
|
40568
|
+
const l = { ...caso.datos, ...datos };
|
|
40569
|
+
const { posicion, total } = posicionEnCola(datos, [...contexto.solicitudesEnCurso ?? [], ...declinadas(caso)]);
|
|
40570
|
+
const pedido = normalizarNombre(datos.solicitud_profesional);
|
|
40571
|
+
const profesional = normalizarNombre(l.profesional);
|
|
40572
|
+
const paraHoy = Boolean(contexto.hoy && l.fecha === contexto.hoy);
|
|
40573
|
+
const asignados = contexto.asignacionesHoy ?? 0;
|
|
40574
|
+
const fuera = fueraDePreferencia(datos.solicitud_preferencia_horaria, horaHM(l.hora));
|
|
40575
|
+
return [
|
|
40576
|
+
excepcion("saltea_la_cola", posicion !== null && posicion > 1, posicion !== null && posicion > 1 ? `es la ${posicion}.\xAA de ${total} en la cola` : "primera de la cola"),
|
|
40577
|
+
excepcion("profesional_distinto", Boolean(pedido && profesional && pedido !== profesional), pedido ? pedido === profesional ? "es el profesional que pidi\xF3" : `pidi\xF3 a ${texto(datos.solicitud_profesional)}` : "no pidi\xF3 un profesional en particular"),
|
|
40578
|
+
excepcion("fuera_de_preferencia_horaria", fuera, fuera ? `prefiere ${texto(datos.solicitud_preferencia_horaria)} y el turno es a las ${horaHM(l.hora)}` : "dentro de su preferencia o sin preferencia"),
|
|
40579
|
+
excepcion("turno_para_hoy", paraHoy, paraHoy ? "el turno es para hoy" : "no es para hoy"),
|
|
40580
|
+
excepcion("sobreturno", l.sobreturno === true, l.sobreturno === true ? "es un sobreturno" : "turno de la grilla"),
|
|
40581
|
+
excepcion("reintento_tras_diferencias", caso.estado === "con_diferencias", caso.estado === "con_diferencias" ? "la asignaci\xF3n anterior no coincidi\xF3 al comprobar" : "primer intento"),
|
|
40582
|
+
excepcion("tope_diario", asignados >= TOPE_DIARIO_RECUPERO, `${asignados} turnos asignados hoy (tope ${TOPE_DIARIO_RECUPERO})`)
|
|
40583
|
+
];
|
|
40584
|
+
}
|
|
40585
|
+
}
|
|
40586
|
+
},
|
|
40587
|
+
{
|
|
40588
|
+
via: "aprobar",
|
|
40589
|
+
de: ["pendiente_aprobacion"],
|
|
40590
|
+
a: "aprobado",
|
|
40591
|
+
actor: ["humano"],
|
|
40592
|
+
descripcion: "Aprobaci\xF3n del responsable del hospital."
|
|
40593
|
+
},
|
|
40594
|
+
{
|
|
40595
|
+
via: "rechazar",
|
|
40596
|
+
de: ["pendiente_aprobacion"],
|
|
40597
|
+
a: "rechazado",
|
|
40598
|
+
actor: ["humano"],
|
|
40599
|
+
descripcion: "Rechazo con motivo.",
|
|
40600
|
+
validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "el rechazo requiere motivo")]
|
|
40601
|
+
},
|
|
40602
|
+
{
|
|
40603
|
+
// Paso opcional: donde el hospital quiere que el paciente acepte antes de asignarle el turno.
|
|
40604
|
+
via: "ofrecer",
|
|
40605
|
+
de: ["aprobado"],
|
|
40606
|
+
a: "ofrecido",
|
|
40607
|
+
actor: ["agente", "humano"],
|
|
40608
|
+
nivelRequerido: "ejecuta",
|
|
40609
|
+
descripcion: "Ofrecerle la cita al paciente antes de asignarla (obligatorio donde la instituci\xF3n pide consulta previa).",
|
|
40610
|
+
validar: (caso, datos, contexto) => [
|
|
40611
|
+
texto(datos.canal) ? ok("canal_informado") : falla("canal_informado", "ofrecer requiere canal (tel\xE9fono, WhatsApp, correo)"),
|
|
40612
|
+
validarMargen(caso, contexto)
|
|
40613
|
+
]
|
|
40614
|
+
},
|
|
40615
|
+
{
|
|
40616
|
+
via: "aceptar",
|
|
40617
|
+
de: ["ofrecido"],
|
|
40618
|
+
a: "aceptado",
|
|
40619
|
+
actor: ["agente", "humano"],
|
|
40620
|
+
nivelRequerido: "observa",
|
|
40621
|
+
descripcion: "El paciente acept\xF3 el turno ofrecido.",
|
|
40622
|
+
validar: (_caso, datos) => [texto(datos.respuesta) ? ok("respuesta_registrada") : falla("respuesta_registrada", "aceptar requiere la respuesta del paciente")]
|
|
40623
|
+
},
|
|
40624
|
+
{
|
|
40625
|
+
via: "no_acepta",
|
|
40626
|
+
de: ["ofrecido"],
|
|
40627
|
+
a: "detectado",
|
|
40628
|
+
actor: ["agente", "humano"],
|
|
40629
|
+
nivelRequerido: "observa",
|
|
40630
|
+
descripcion: "El paciente no acept\xF3 o no respondi\xF3: el turno vuelve a buscar candidato.",
|
|
40631
|
+
validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "no_acepta requiere motivo (no contest\xF3, no puede ese d\xEDa\u2026)")]
|
|
40632
|
+
},
|
|
40633
|
+
{
|
|
40634
|
+
// El plugin autoriza ANTES de que se escriba en el sistema del hospital: la cita está a tiempo
|
|
40635
|
+
// y, donde la institución lo pide, el paciente aceptó. Sin esta autorización el agente no escribe.
|
|
40636
|
+
via: "autorizar",
|
|
40637
|
+
de: ["aprobado", "aceptado"],
|
|
40638
|
+
a: "asignando",
|
|
40639
|
+
actor: ["agente", "humano"],
|
|
40640
|
+
nivelRequerido: "ejecuta",
|
|
40641
|
+
descripcion: "Pedir autorizaci\xF3n para asignar la cita en el sistema de gesti\xF3n. Va siempre antes de escribir.",
|
|
40642
|
+
validar: (caso, _datos, contexto) => [
|
|
40643
|
+
...validarLugar(caso, {}, contexto),
|
|
40644
|
+
validarMargen(caso, contexto),
|
|
40645
|
+
validarPacienteAcepto(caso, contexto)
|
|
40646
|
+
]
|
|
40647
|
+
},
|
|
40648
|
+
{
|
|
40649
|
+
// Registra lo que ya se escribió: no se rechaza por la hora (la escritura ocurrió). Un agente solo
|
|
40650
|
+
// llega acá con la autorización; una persona que lo hizo a mano, desde aprobado o aceptado.
|
|
40651
|
+
via: "asignar",
|
|
40652
|
+
de: ["asignando", "aprobado", "aceptado"],
|
|
40653
|
+
a: "asignado",
|
|
40654
|
+
actor: ["agente", "humano"],
|
|
40655
|
+
nivelRequerido: "ejecuta",
|
|
40656
|
+
cargaManual: true,
|
|
40657
|
+
descripcion: "Registrar la cita asignada en el sistema de gesti\xF3n, con la acci\xF3n de escritura.",
|
|
40658
|
+
validar: (caso, datos, contexto) => [
|
|
40659
|
+
texto(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "asignar requiere accion_id de la escritura registrada con ops_registrar_accion"),
|
|
40660
|
+
contexto.actor === "agente" && caso.estado !== "asignando" ? falla("escritura_autorizada", "un agente tiene que pedir autorizar antes de escribir en el sistema") : ok("escritura_autorizada"),
|
|
40661
|
+
...caso.estado === "asignando" ? [] : [validarPacienteAcepto(caso, contexto)]
|
|
40662
|
+
]
|
|
40663
|
+
},
|
|
40664
|
+
{
|
|
40665
|
+
via: "verificar",
|
|
40666
|
+
de: ["asignado"],
|
|
40667
|
+
a: (caso, datos) => {
|
|
40668
|
+
const l = caso.datos;
|
|
40669
|
+
const coincide = texto(datos.leido_fecha) === texto(l.fecha) && horaHM(datos.leido_hora) === horaHM(l.hora) && normalizarNombre(datos.leido_profesional) === normalizarNombre(l.profesional) && (refExterna(datos.leido_solicitud_id) === null || refExterna(datos.leido_solicitud_id) === refExterna(l.solicitud_id));
|
|
40670
|
+
return coincide ? "verificado" : "con_diferencias";
|
|
40671
|
+
},
|
|
40672
|
+
actor: ["agente", "humano", "sistema"],
|
|
40673
|
+
nivelRequerido: "observa",
|
|
40674
|
+
descripcion: "Releer en el sistema el turno de la solicitud y compararlo con el aprobado (fecha, hora y profesional). Si no coincide, el caso queda con diferencias.",
|
|
40675
|
+
validar: (_caso, datos) => [
|
|
40676
|
+
fechaISO(datos.leido_fecha) && horaHM(datos.leido_hora) !== null && texto(datos.leido_profesional) ? ok("turno_leido_presente") : falla("turno_leido_presente", "verificar requiere leido_fecha (AAAA-MM-DD), leido_hora (HH:MM) y leido_profesional, rele\xEDdos en el sistema")
|
|
40677
|
+
]
|
|
40678
|
+
},
|
|
40679
|
+
{
|
|
40680
|
+
via: "corregir",
|
|
40681
|
+
de: ["con_diferencias"],
|
|
40682
|
+
a: "asignado",
|
|
40683
|
+
actor: ["humano"],
|
|
40684
|
+
cargaManual: true,
|
|
40685
|
+
descripcion: "Corregir a mano el turno en el sistema de gesti\xF3n (lo aprobado no cambia) y volver a comprobar.",
|
|
40686
|
+
validar: (_caso, datos) => [
|
|
40687
|
+
texto(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "corregir requiere accion_id de la escritura registrada con la correcci\xF3n")
|
|
40688
|
+
]
|
|
40689
|
+
},
|
|
40690
|
+
{
|
|
40691
|
+
via: "sin_candidato",
|
|
40692
|
+
de: ["detectado"],
|
|
40693
|
+
a: "sin_candidato",
|
|
40694
|
+
actor: ["agente", "humano"],
|
|
40695
|
+
nivelRequerido: "observa",
|
|
40696
|
+
descripcion: "No hay solicitudes pendientes que puedan tomar el turno liberado: queda libre.",
|
|
40697
|
+
validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "sin_candidato requiere motivo (qu\xE9 se busc\xF3 y por qu\xE9 no hay a qui\xE9n darlo)")]
|
|
40698
|
+
},
|
|
40699
|
+
{
|
|
40700
|
+
via: "descartar",
|
|
40701
|
+
de: ["detectado", "pendiente_aprobacion", "aprobado", "ofrecido", "aceptado", "asignando"],
|
|
40702
|
+
a: "cancelado",
|
|
40703
|
+
actor: ["agente", "humano"],
|
|
40704
|
+
nivelRequerido: "observa",
|
|
40705
|
+
descripcion: "El turno ya no est\xE1 libre (lo ocup\xF3 otra persona, se anul\xF3 la agenda o pas\xF3 la hora): se cierra sin asignar.",
|
|
40706
|
+
validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "descartar requiere motivo")]
|
|
40707
|
+
}
|
|
40708
|
+
]
|
|
40709
|
+
};
|
|
40412
40710
|
var WORKFLOWS = {
|
|
40413
40711
|
[actualizacionArancel.clave]: actualizacionArancel,
|
|
40414
|
-
[tareaOperativa.clave]: tareaOperativa
|
|
40712
|
+
[tareaOperativa.clave]: tareaOperativa,
|
|
40713
|
+
[recuperoTurno.clave]: recuperoTurno
|
|
40415
40714
|
};
|
|
40416
40715
|
function getWorkflow(clave) {
|
|
40417
40716
|
return WORKFLOWS[clave] ?? null;
|
|
@@ -40428,6 +40727,7 @@ function modoHabilita(modo, nivelRequerido) {
|
|
|
40428
40727
|
function evaluarTransicion(input2) {
|
|
40429
40728
|
const { caso, via, actor } = input2;
|
|
40430
40729
|
const datos = input2.datos ?? {};
|
|
40730
|
+
const contexto = { ...input2.contexto ?? {}, actor };
|
|
40431
40731
|
const wf = getWorkflow(caso.workflow);
|
|
40432
40732
|
if (!wf) {
|
|
40433
40733
|
return { ok: false, codigo: "workflow_desconocido", motivo: `workflow desconocido: ${caso.workflow}`, validaciones: [] };
|
|
@@ -40447,7 +40747,7 @@ function evaluarTransicion(input2) {
|
|
|
40447
40747
|
const motivo = input2.modo === "corte" ? `el workflow ${wf.clave} est\xE1 cortado para agentes en esta instituci\xF3n` : `la pol\xEDtica vigente es "${input2.modo}" y "${via}" requiere "${def2.nivelRequerido ?? "observa"}"`;
|
|
40448
40748
|
return { ok: false, codigo: "politica", motivo, validaciones: [] };
|
|
40449
40749
|
}
|
|
40450
|
-
const validaciones = def2.validar ? def2.validar(caso, datos) : [];
|
|
40750
|
+
const validaciones = def2.validar ? def2.validar(caso, datos, contexto) : [];
|
|
40451
40751
|
const fallidas = validaciones.filter((v) => !v.ok);
|
|
40452
40752
|
if (fallidas.length > 0) {
|
|
40453
40753
|
return { ok: false, codigo: "validacion", motivo: fallidas.map((f) => `${f.regla}: ${f.detalle ?? "fall\xF3"}`).join("; "), validaciones };
|
|
@@ -40455,7 +40755,7 @@ function evaluarTransicion(input2) {
|
|
|
40455
40755
|
let a = typeof def2.a === "function" ? def2.a(caso, datos) : def2.a;
|
|
40456
40756
|
let autoaprobado = false;
|
|
40457
40757
|
if (actor === "agente" && input2.modo === "autonomo" && def2.autonomia) {
|
|
40458
|
-
const excepciones = def2.autonomia.excepciones(caso, datos);
|
|
40758
|
+
const excepciones = def2.autonomia.excepciones(caso, datos, contexto);
|
|
40459
40759
|
validaciones.push(...excepciones);
|
|
40460
40760
|
if (excepciones.every((e) => e.ok)) {
|
|
40461
40761
|
a = def2.autonomia.aSinExcepcion;
|
|
@@ -40593,9 +40893,16 @@ async function obtenerModo(ctx, companyId, workflow) {
|
|
|
40593
40893
|
const modo = rows[0]?.modo;
|
|
40594
40894
|
return typeof modo === "string" ? modo : MODO_POR_DEFECTO;
|
|
40595
40895
|
}
|
|
40896
|
+
async function obtenerOpciones(ctx, companyId, workflow) {
|
|
40897
|
+
const rows = await ctx.db.query(
|
|
40898
|
+
`SELECT opciones FROM ${t(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`,
|
|
40899
|
+
[companyId, workflow]
|
|
40900
|
+
);
|
|
40901
|
+
return jsonb(rows[0]?.opciones);
|
|
40902
|
+
}
|
|
40596
40903
|
async function listarPoliticas(ctx, companyId) {
|
|
40597
40904
|
const rows = await ctx.db.query(
|
|
40598
|
-
`SELECT workflow, modo, motivo, updated_at FROM ${t(ctx, "politica")} WHERE company_id = $1`,
|
|
40905
|
+
`SELECT workflow, modo, motivo, opciones, updated_at FROM ${t(ctx, "politica")} WHERE company_id = $1`,
|
|
40599
40906
|
[companyId]
|
|
40600
40907
|
);
|
|
40601
40908
|
const porWorkflow = new Map(rows.map((r) => [String(r.workflow), r]));
|
|
@@ -40608,40 +40915,50 @@ async function listarPoliticas(ctx, companyId) {
|
|
|
40608
40915
|
modo: r ? String(r.modo) : MODO_POR_DEFECTO,
|
|
40609
40916
|
motivo: r ? r.motivo ?? null : null,
|
|
40610
40917
|
actualizadaEn: r ? iso(r.updated_at) : null,
|
|
40611
|
-
porDefecto: !r
|
|
40918
|
+
porDefecto: !r,
|
|
40919
|
+
opciones: r ? jsonb(r.opciones) : {}
|
|
40612
40920
|
};
|
|
40613
40921
|
});
|
|
40614
40922
|
}
|
|
40923
|
+
function describirOpciones(antes, despues) {
|
|
40924
|
+
const partes = [];
|
|
40925
|
+
if (Boolean(antes.ofrecer_primero) !== Boolean(despues.ofrecer_primero)) partes.push(`consultar al paciente antes de asignar: ${despues.ofrecer_primero ? "s\xED" : "no"}`);
|
|
40926
|
+
return partes.length ? partes.join(" \xB7 ") : null;
|
|
40927
|
+
}
|
|
40615
40928
|
async function cambiarPolitica(ctx, input2) {
|
|
40616
40929
|
if (!getWorkflow(input2.workflow)) throw new OpsError("workflow_desconocido", `workflow desconocido: ${input2.workflow}`);
|
|
40617
40930
|
if (input2.actor.tipo !== "humano") throw new OpsError("actor_no_permitido", "solo un humano puede cambiar la pol\xEDtica de un workflow");
|
|
40618
40931
|
const anterior = await obtenerModo(ctx, input2.companyId, input2.workflow);
|
|
40932
|
+
const opcionesAntes = await obtenerOpciones(ctx, input2.companyId, input2.workflow);
|
|
40933
|
+
const opciones = { ...opcionesAntes, ...input2.opciones ?? {} };
|
|
40934
|
+
const cambioOpciones = describirOpciones(opcionesAntes, opciones);
|
|
40935
|
+
const motivo = [input2.motivo ?? null, cambioOpciones].filter(Boolean).join(" \xB7 ") || null;
|
|
40619
40936
|
const existe = await ctx.db.query(
|
|
40620
40937
|
`SELECT id FROM ${t(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`,
|
|
40621
40938
|
[input2.companyId, input2.workflow]
|
|
40622
40939
|
);
|
|
40623
40940
|
if (existe.length > 0) {
|
|
40624
40941
|
await ctx.db.execute(
|
|
40625
|
-
`UPDATE ${t(ctx, "politica")} SET modo = $3, motivo = $4, actor_tipo = $5, actor_id = $6, updated_at = now() WHERE company_id = $1 AND workflow = $2`,
|
|
40626
|
-
[input2.companyId, input2.workflow, input2.modo,
|
|
40942
|
+
`UPDATE ${t(ctx, "politica")} SET modo = $3, motivo = $4, actor_tipo = $5, actor_id = $6, opciones = $7::jsonb, updated_at = now() WHERE company_id = $1 AND workflow = $2`,
|
|
40943
|
+
[input2.companyId, input2.workflow, input2.modo, motivo, input2.actor.tipo, input2.actor.id, JSON.stringify(opciones)]
|
|
40627
40944
|
);
|
|
40628
40945
|
} else {
|
|
40629
40946
|
await ctx.db.execute(
|
|
40630
|
-
`INSERT INTO ${t(ctx, "politica")} (id, company_id, workflow, modo, motivo, actor_tipo, actor_id) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
40631
|
-
[randomUUID(), input2.companyId, input2.workflow, input2.modo,
|
|
40947
|
+
`INSERT INTO ${t(ctx, "politica")} (id, company_id, workflow, modo, motivo, actor_tipo, actor_id, opciones) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)`,
|
|
40948
|
+
[randomUUID(), input2.companyId, input2.workflow, input2.modo, motivo, input2.actor.tipo, input2.actor.id, JSON.stringify(opciones)]
|
|
40632
40949
|
);
|
|
40633
40950
|
}
|
|
40634
40951
|
await ctx.db.execute(
|
|
40635
40952
|
`INSERT INTO ${t(ctx, "politica_historial")} (id, company_id, workflow, modo_anterior, modo_nuevo, motivo, actor_tipo, actor_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
40636
|
-
[randomUUID(), input2.companyId, input2.workflow, anterior, input2.modo,
|
|
40953
|
+
[randomUUID(), input2.companyId, input2.workflow, anterior, input2.modo, motivo, input2.actor.tipo, input2.actor.id]
|
|
40637
40954
|
);
|
|
40638
40955
|
await ctx.activity.log({
|
|
40639
40956
|
companyId: input2.companyId,
|
|
40640
|
-
message: `Autopilot Care: pol\xEDtica de ${input2.workflow} cambiada de ${anterior} a ${input2.modo}${
|
|
40957
|
+
message: `Autopilot Care: pol\xEDtica de ${input2.workflow} cambiada de ${anterior} a ${input2.modo}${motivo ? ` (${motivo})` : ""}`,
|
|
40641
40958
|
metadata: { workflow: input2.workflow, modoAnterior: anterior, modoNuevo: input2.modo, actorId: input2.actor.id }
|
|
40642
40959
|
});
|
|
40643
40960
|
const wf = getWorkflow(input2.workflow);
|
|
40644
|
-
return { workflow: wf.clave, nombre: wf.nombre, puesto: wf.puesto, modo: input2.modo, motivo
|
|
40961
|
+
return { workflow: wf.clave, nombre: wf.nombre, puesto: wf.puesto, modo: input2.modo, motivo, actualizadaEn: (/* @__PURE__ */ new Date()).toISOString(), porDefecto: false, opciones };
|
|
40645
40962
|
}
|
|
40646
40963
|
async function leerCaso(ctx, companyId, casoId) {
|
|
40647
40964
|
const rows = await ctx.db.query(
|
|
@@ -40748,6 +41065,34 @@ async function abrirCaso(ctx, input2) {
|
|
|
40748
41065
|
}
|
|
40749
41066
|
};
|
|
40750
41067
|
}
|
|
41068
|
+
var ZONA_HOSPITAL = "America/Argentina/Buenos_Aires";
|
|
41069
|
+
function ahoraEnHospital(ahora = /* @__PURE__ */ new Date()) {
|
|
41070
|
+
const partes = new Intl.DateTimeFormat("en-CA", { timeZone: ZONA_HOSPITAL, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(ahora);
|
|
41071
|
+
const p = (tipo) => partes.find((x) => x.type === tipo)?.value ?? "00";
|
|
41072
|
+
const hoy = `${p("year")}-${p("month")}-${p("day")}`;
|
|
41073
|
+
return { hoy, hora: `${p("hour")}:${p("minute")}`, inicioDelDia: /* @__PURE__ */ new Date(`${hoy}T03:00:00Z`) };
|
|
41074
|
+
}
|
|
41075
|
+
async function contextoDe(ctx, companyId, caso, via, datos) {
|
|
41076
|
+
const { hoy, hora, inicioDelDia } = ahoraEnHospital();
|
|
41077
|
+
const contexto = { hoy, horaActual: hora };
|
|
41078
|
+
if (caso.workflow !== "recupero_turno") return contexto;
|
|
41079
|
+
contexto.ofrecerPrimero = (await obtenerOpciones(ctx, companyId, caso.workflow)).ofrecer_primero === true;
|
|
41080
|
+
if (via !== "proponer") return contexto;
|
|
41081
|
+
const otros = await ctx.db.query(
|
|
41082
|
+
`SELECT id, datos->>'solicitud_id' AS solicitud_id FROM ${t(ctx, "caso")} WHERE company_id = $1 AND workflow = $2 AND id <> $3 AND datos->>'solicitud_id' IS NOT NULL AND estado NOT IN ($4, $5, $6) LIMIT 1000`,
|
|
41083
|
+
[companyId, "recupero_turno", caso.id, "sin_candidato", "rechazado", "cancelado"]
|
|
41084
|
+
);
|
|
41085
|
+
contexto.solicitudesEnCurso = [...new Set(otros.map((r) => String(r.solicitud_id)))];
|
|
41086
|
+
const solicitud = refExterna(datos.solicitud_id);
|
|
41087
|
+
const mismo = solicitud ? otros.find((r) => String(r.solicitud_id) === solicitud) : void 0;
|
|
41088
|
+
contexto.solicitudEnOtroCaso = mismo ? String(mismo.id) : null;
|
|
41089
|
+
const asignados = await ctx.db.query(
|
|
41090
|
+
`SELECT count(*)::int AS cantidad FROM ${t(ctx, "transicion")} WHERE company_id = $1 AND via = $2 AND created_at >= $3`,
|
|
41091
|
+
[companyId, "asignar", inicioDelDia.toISOString()]
|
|
41092
|
+
);
|
|
41093
|
+
contexto.asignacionesHoy = Number(asignados[0]?.cantidad ?? 0);
|
|
41094
|
+
return contexto;
|
|
41095
|
+
}
|
|
40751
41096
|
async function transicionar(ctx, input2) {
|
|
40752
41097
|
const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
|
|
40753
41098
|
if (!caso) throw new OpsError("caso_inexistente", `no existe el caso ${input2.casoId} en esta instituci\xF3n`);
|
|
@@ -40762,7 +41107,8 @@ async function transicionar(ctx, input2) {
|
|
|
40762
41107
|
const datos = { ...input2.datos ?? {} };
|
|
40763
41108
|
if (input2.motivo && !datos.motivo) datos.motivo = input2.motivo;
|
|
40764
41109
|
const modo = input2.actor.tipo === "agente" ? await obtenerModo(ctx, input2.companyId, caso.workflow) : "ejecuta";
|
|
40765
|
-
const
|
|
41110
|
+
const contexto = await contextoDe(ctx, input2.companyId, caso, input2.via, datos);
|
|
41111
|
+
const resultado = evaluarTransicion({ caso, via: input2.via, actor: input2.actor.tipo, datos, modo, contexto });
|
|
40766
41112
|
if (!resultado.ok) {
|
|
40767
41113
|
throw new OpsError(resultado.codigo, resultado.motivo, { validaciones: resultado.validaciones, estado: caso.estado });
|
|
40768
41114
|
}
|
|
@@ -40778,6 +41124,25 @@ async function transicionar(ctx, input2) {
|
|
|
40778
41124
|
throw new OpsError("validacion", `accion_registrada: ${problema}`, { validaciones: [{ regla: "accion_registrada", ok: false, detalle: problema }], estado: caso.estado });
|
|
40779
41125
|
}
|
|
40780
41126
|
}
|
|
41127
|
+
if (caso.workflow === "recupero_turno" && resultado.via === "no_acepta") {
|
|
41128
|
+
const previas = Array.isArray(caso.datos.declinaron) ? caso.datos.declinaron.map((v) => refExterna(v)).filter((v) => v !== null) : [];
|
|
41129
|
+
const esta = refExterna(caso.datos.solicitud_id);
|
|
41130
|
+
datos.declinaron = esta && !previas.includes(esta) ? [...previas, esta] : previas;
|
|
41131
|
+
}
|
|
41132
|
+
const excepcionesEvaluadas = resultado.validaciones.filter((v) => v.excepcion);
|
|
41133
|
+
if (excepcionesEvaluadas.length > 0) datos.excepciones = excepcionesEvaluadas.filter((v) => !v.ok).map((v) => v.regla);
|
|
41134
|
+
else if (resultado.via === "proponer") datos.excepciones = [];
|
|
41135
|
+
else delete datos.excepciones;
|
|
41136
|
+
if (caso.workflow === "recupero_turno" && resultado.via === "proponer") {
|
|
41137
|
+
const enCurso = new Set(contexto.solicitudesEnCurso ?? []);
|
|
41138
|
+
const { posicion, total } = posicionEnCola(datos, enCurso);
|
|
41139
|
+
datos.posicion_en_cola = posicion;
|
|
41140
|
+
datos.largo_cola = total;
|
|
41141
|
+
const cola = leerCola(datos.cola);
|
|
41142
|
+
const elegida = cola?.find((e) => e.solicitud_id === refExterna(datos.solicitud_id));
|
|
41143
|
+
if (elegida?.prioridad && !str(datos.solicitud_prioridad)) datos.solicitud_prioridad = elegida.prioridad;
|
|
41144
|
+
if (cola) datos.cola = ordenarCola(cola).map((e) => enCurso.has(e.solicitud_id) && e.solicitud_id !== elegida?.solicitud_id ? { ...e, en_otro_recupero: true } : e);
|
|
41145
|
+
}
|
|
40781
41146
|
const transicionId = randomUUID();
|
|
40782
41147
|
await ctx.db.execute(
|
|
40783
41148
|
`INSERT INTO ${t(ctx, "transicion")} (id, company_id, caso_id, de_estado, a_estado, via, actor_tipo, actor_id, run_id, motivo, datos, validaciones, idempotency_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12::jsonb, $13)`,
|
|
@@ -40829,7 +41194,11 @@ async function registrarAccion(ctx, input2) {
|
|
|
40829
41194
|
if (!modoHabilita(modo, "ejecuta")) throw new OpsError("politica", `la pol\xEDtica vigente de ${caso.workflow} es "${modo}": un agente no puede registrar escrituras sobre el sistema del hospital`);
|
|
40830
41195
|
}
|
|
40831
41196
|
const id = randomUUID();
|
|
40832
|
-
const evidencia = input2.evidencia ?? {};
|
|
41197
|
+
const evidencia = { ...input2.evidencia ?? {} };
|
|
41198
|
+
delete evidencia.fuera_de_flujo;
|
|
41199
|
+
if (caso.workflow === "recupero_turno" && input2.tipo === "escritura" && input2.actor.tipo === "agente" && caso.estado !== "asignando") {
|
|
41200
|
+
evidencia.fuera_de_flujo = true;
|
|
41201
|
+
}
|
|
40833
41202
|
await ctx.db.execute(
|
|
40834
41203
|
`INSERT INTO ${t(ctx, "accion")} (id, company_id, caso_id, tipo, conector, objetivo, resultado, evidencia, detalle, actor_tipo, actor_id, run_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11, $12)`,
|
|
40835
41204
|
[id, input2.companyId, caso.id, input2.tipo, input2.conector, objetivo, input2.resultado, JSON.stringify(evidencia), input2.detalle ?? null, input2.actor.tipo, input2.actor.id, input2.actor.runId ?? null]
|
|
@@ -40889,7 +41258,8 @@ async function registrarCargaManual(ctx, input2) {
|
|
|
40889
41258
|
if (!sinValor && valorCargado === null) {
|
|
40890
41259
|
throw new OpsError("validacion", "valor_cargado: debe ser un n\xFAmero", { validaciones: [{ regla: "valor_cargado_numerico", ok: false, detalle: "debe ser un n\xFAmero" }], estado: caso.estado });
|
|
40891
41260
|
}
|
|
40892
|
-
const
|
|
41261
|
+
const contextoPrevio = await contextoDe(ctx, input2.companyId, caso, def2.via, {});
|
|
41262
|
+
const previa = evaluarTransicion({ caso, via: def2.via, actor: "humano", datos: { accion_id: "(por registrar)" }, modo: "ejecuta", contexto: contextoPrevio });
|
|
40893
41263
|
if (!previa.ok) throw new OpsError(previa.codigo, previa.motivo, { validaciones: previa.validaciones, estado: caso.estado });
|
|
40894
41264
|
const persona = str(input2.persona);
|
|
40895
41265
|
const comentario = str(input2.comentario);
|
|
@@ -40934,6 +41304,16 @@ async function registrarCargaManual(ctx, input2) {
|
|
|
40934
41304
|
throw err;
|
|
40935
41305
|
}
|
|
40936
41306
|
}
|
|
41307
|
+
async function registrarRespuestaPaciente(ctx, input2) {
|
|
41308
|
+
if (input2.actor.tipo !== "humano") throw new OpsError("actor_no_permitido", "la respuesta del paciente la registra una persona del hospital; un agente usa ops_transicionar");
|
|
41309
|
+
const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
|
|
41310
|
+
if (!caso) throw new OpsError("caso_inexistente", `no existe el caso ${input2.casoId} en esta instituci\xF3n`);
|
|
41311
|
+
if (caso.workflow !== "recupero_turno") throw new OpsError("estado_invalido", "solo las citas ofrecidas al paciente llevan respuesta");
|
|
41312
|
+
const persona = str(input2.persona);
|
|
41313
|
+
const comentario = str(input2.comentario);
|
|
41314
|
+
const datos = input2.acepto ? { respuesta: comentario ?? "Acept\xF3 la cita", ...persona ? { registrado_por: persona } : {} } : { motivo: comentario ?? "No acept\xF3 la cita", ...persona ? { registrado_por: persona } : {} };
|
|
41315
|
+
return transicionar(ctx, { companyId: input2.companyId, casoId: caso.id, via: input2.acepto ? "aceptar" : "no_acepta", datos, motivo: comentario, idempotencyKey: input2.idempotencyKey, actor: input2.actor });
|
|
41316
|
+
}
|
|
40937
41317
|
function finalesOk() {
|
|
40938
41318
|
return [...new Set(listarWorkflows().flatMap((w) => w.finalesOk))];
|
|
40939
41319
|
}
|
|
@@ -41358,11 +41738,14 @@ function registrarHerramientas(ctx) {
|
|
|
41358
41738
|
const detalle = await leerCasoCompleto(ctx, runCtx.companyId, casoId);
|
|
41359
41739
|
if (!detalle) throw new OpsError("caso_inexistente", `no existe el caso ${casoId} en esta instituci\xF3n`);
|
|
41360
41740
|
const disponibles = detalle.disponibles.filter((d) => d.actor.includes("agente")).map((d) => d.via);
|
|
41741
|
+
const opciones = await obtenerOpciones(ctx, runCtx.companyId, detalle.caso.workflow);
|
|
41742
|
+
const consulta = detalle.caso.workflow === "recupero_turno" ? `
|
|
41743
|
+
Consulta previa al paciente: ${opciones.ofrecer_primero === true ? "S\xCD (ofrecer y esperar que acepte antes de autorizar)" : "no"}` : "";
|
|
41361
41744
|
return {
|
|
41362
41745
|
content: `${resumenCaso(detalle.caso)}
|
|
41363
41746
|
Transiciones disponibles para agente: ${disponibles.join(", ") || "ninguna"}
|
|
41364
|
-
Transiciones registradas: ${detalle.transiciones.length}; acciones registradas: ${detalle.acciones.length}`,
|
|
41365
|
-
data: detalle
|
|
41747
|
+
Transiciones registradas: ${detalle.transiciones.length}; acciones registradas: ${detalle.acciones.length}${consulta}`,
|
|
41748
|
+
data: { ...detalle, opciones }
|
|
41366
41749
|
};
|
|
41367
41750
|
}));
|
|
41368
41751
|
ctx.tools.register(TOOL_NAMES.abrirCaso, toolSchema(TOOL_NAMES.abrirCaso), async (params, runCtx) => conErrores(async () => {
|
|
@@ -41547,8 +41930,10 @@ async function manejarApi(ctx, input2) {
|
|
|
41547
41930
|
return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "rechazar", motivo: str2(body.motivo), datos: conPersona(obj(body.datos)), actor }));
|
|
41548
41931
|
case "marcar-cargado":
|
|
41549
41932
|
return respuesta(200, await registrarCargaManual(ctx, { companyId, casoId: input2.params.casoId ?? "", valorCargado: body.valorCargado, comentario: str2(body.comentario), persona: decididoPor, idempotencyKey: str2(body.idempotencyKey), actor }));
|
|
41933
|
+
case "respuesta-paciente":
|
|
41934
|
+
return respuesta(200, await registrarRespuestaPaciente(ctx, { companyId, casoId: input2.params.casoId ?? "", acepto: body.acepto === true, comentario: str2(body.comentario), persona: decididoPor, idempotencyKey: str2(body.idempotencyKey), actor }));
|
|
41550
41935
|
case "version":
|
|
41551
|
-
return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual"], workflows: listarWorkflows().map((w) => w.clave) });
|
|
41936
|
+
return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos", "ofrecer-primero"], workflows: listarWorkflows().map((w) => w.clave) });
|
|
41552
41937
|
case "politicas":
|
|
41553
41938
|
return respuesta(200, { politicas: await listarPoliticas(ctx, companyId) });
|
|
41554
41939
|
case "serie":
|
|
@@ -41565,7 +41950,8 @@ async function manejarApi(ctx, input2) {
|
|
|
41565
41950
|
const workflow = str2(body.workflow);
|
|
41566
41951
|
if (!workflow) return respuesta(400, { error: "workflow_requerido" });
|
|
41567
41952
|
if (!esModo(body.modo)) return respuesta(400, { error: "modo_invalido", detalle: "modo debe ser corte, observa, propone, ejecuta o autonomo" });
|
|
41568
|
-
|
|
41953
|
+
const opciones = body.opciones && typeof body.opciones === "object" && !Array.isArray(body.opciones) ? body.opciones : null;
|
|
41954
|
+
return respuesta(200, await cambiarPolitica(ctx, { companyId, workflow, modo: body.modo, motivo: motivoConPersona(str2(body.motivo)), opciones, actor }));
|
|
41569
41955
|
}
|
|
41570
41956
|
default:
|
|
41571
41957
|
return respuesta(404, { error: "ruta_desconocida", routeKey: input2.routeKey });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- Opciones de cada función por institución, además del nivel de autonomía.
|
|
2
|
+
-- recupero_turno: {"ofrecer_primero": true} → el paciente tiene que aceptar la cita
|
|
3
|
+
-- antes de que se asigne (el plugin no autoriza la escritura hasta que acepta).
|
|
4
|
+
|
|
5
|
+
ALTER TABLE plugin_autopilot_ops_1a68482ede.politica
|
|
6
|
+
ADD COLUMN opciones jsonb NOT NULL DEFAULT '{}'::jsonb;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperclip-plugin-autopilot-ops",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Autopilot Care · operación autónoma gobernada para instituciones de salud: workflows determinísticos, validaciones, registro de negocio, políticas (observa/propone/ejecuta/corte) y tablero del director, como plugin de Paperclip.",
|
|
6
6
|
"files": [
|