paperclip-plugin-autopilot-ops 0.1.6 → 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 +10 -2
- package/dist/ui/index.js +10 -2
- package/dist/worker.js +127 -30
- 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 = {
|
|
@@ -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`. 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; `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo.",
|
|
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 = {
|
|
@@ -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`. 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; `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo.",
|
|
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 = {
|
|
@@ -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`. 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; `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo.",
|
|
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",
|
|
@@ -40470,6 +40478,26 @@ function fueraDePreferencia(preferencia, hora) {
|
|
|
40470
40478
|
if (p.includes("tarde")) return hora < "13:00";
|
|
40471
40479
|
return false;
|
|
40472
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
|
+
}
|
|
40473
40501
|
function validarLugar(caso, datos, contexto) {
|
|
40474
40502
|
const l = { ...caso.datos, ...datos };
|
|
40475
40503
|
const completo = fechaISO(l.fecha) && horaHM(l.hora) !== null && texto(l.profesional) !== null && texto(l.especialidad) !== null;
|
|
@@ -40484,7 +40512,7 @@ function validarLugar(caso, datos, contexto) {
|
|
|
40484
40512
|
}
|
|
40485
40513
|
var recuperoTurno = {
|
|
40486
40514
|
clave: "recupero_turno",
|
|
40487
|
-
nombre: "Recupero de
|
|
40515
|
+
nombre: "Recupero de citas",
|
|
40488
40516
|
puesto: "Pacientes y agenda",
|
|
40489
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.",
|
|
40490
40518
|
inicial: "detectado",
|
|
@@ -40494,6 +40522,7 @@ var recuperoTurno = {
|
|
|
40494
40522
|
"aprobado",
|
|
40495
40523
|
"ofrecido",
|
|
40496
40524
|
"aceptado",
|
|
40525
|
+
"asignando",
|
|
40497
40526
|
"asignado",
|
|
40498
40527
|
"verificado",
|
|
40499
40528
|
"con_diferencias",
|
|
@@ -40522,7 +40551,10 @@ var recuperoTurno = {
|
|
|
40522
40551
|
const espSolicitud = normalizarNombre(datos.solicitud_especialidad);
|
|
40523
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)"}`));
|
|
40524
40553
|
out.push(contexto.solicitudEnOtroCaso ? falla("solicitud_libre", `esa solicitud ya tiene otro recupero en curso o asignado (${contexto.solicitudEnOtroCaso})`) : ok("solicitud_libre"));
|
|
40525
|
-
const
|
|
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]);
|
|
40526
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}]"));
|
|
40527
40559
|
else if (posicion === null) out.push(falla("orden_de_la_cola", "la solicitud elegida no est\xE1 en la cola informada"));
|
|
40528
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`));
|
|
@@ -40534,7 +40566,7 @@ var recuperoTurno = {
|
|
|
40534
40566
|
aSinExcepcion: "aprobado",
|
|
40535
40567
|
excepciones: (caso, datos, contexto) => {
|
|
40536
40568
|
const l = { ...caso.datos, ...datos };
|
|
40537
|
-
const { posicion, total } = posicionEnCola(datos, contexto.solicitudesEnCurso);
|
|
40569
|
+
const { posicion, total } = posicionEnCola(datos, [...contexto.solicitudesEnCurso ?? [], ...declinadas(caso)]);
|
|
40538
40570
|
const pedido = normalizarNombre(datos.solicitud_profesional);
|
|
40539
40571
|
const profesional = normalizarNombre(l.profesional);
|
|
40540
40572
|
const paraHoy = Boolean(contexto.hoy && l.fecha === contexto.hoy);
|
|
@@ -40574,8 +40606,11 @@ var recuperoTurno = {
|
|
|
40574
40606
|
a: "ofrecido",
|
|
40575
40607
|
actor: ["agente", "humano"],
|
|
40576
40608
|
nivelRequerido: "ejecuta",
|
|
40577
|
-
descripcion: "Ofrecerle
|
|
40578
|
-
validar: (
|
|
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
|
+
]
|
|
40579
40614
|
},
|
|
40580
40615
|
{
|
|
40581
40616
|
via: "aceptar",
|
|
@@ -40596,16 +40631,34 @@ var recuperoTurno = {
|
|
|
40596
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)")]
|
|
40597
40632
|
},
|
|
40598
40633
|
{
|
|
40599
|
-
|
|
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",
|
|
40600
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"],
|
|
40601
40653
|
a: "asignado",
|
|
40602
40654
|
actor: ["agente", "humano"],
|
|
40603
40655
|
nivelRequerido: "ejecuta",
|
|
40604
40656
|
cargaManual: true,
|
|
40605
|
-
descripcion: "
|
|
40657
|
+
descripcion: "Registrar la cita asignada en el sistema de gesti\xF3n, con la acci\xF3n de escritura.",
|
|
40606
40658
|
validar: (caso, datos, contexto) => [
|
|
40607
|
-
|
|
40608
|
-
|
|
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)]
|
|
40609
40662
|
]
|
|
40610
40663
|
},
|
|
40611
40664
|
{
|
|
@@ -40645,7 +40698,7 @@ var recuperoTurno = {
|
|
|
40645
40698
|
},
|
|
40646
40699
|
{
|
|
40647
40700
|
via: "descartar",
|
|
40648
|
-
de: ["detectado", "pendiente_aprobacion", "aprobado", "ofrecido", "aceptado"],
|
|
40701
|
+
de: ["detectado", "pendiente_aprobacion", "aprobado", "ofrecido", "aceptado", "asignando"],
|
|
40649
40702
|
a: "cancelado",
|
|
40650
40703
|
actor: ["agente", "humano"],
|
|
40651
40704
|
nivelRequerido: "observa",
|
|
@@ -40674,7 +40727,7 @@ function modoHabilita(modo, nivelRequerido) {
|
|
|
40674
40727
|
function evaluarTransicion(input2) {
|
|
40675
40728
|
const { caso, via, actor } = input2;
|
|
40676
40729
|
const datos = input2.datos ?? {};
|
|
40677
|
-
const contexto = input2.contexto ?? {};
|
|
40730
|
+
const contexto = { ...input2.contexto ?? {}, actor };
|
|
40678
40731
|
const wf = getWorkflow(caso.workflow);
|
|
40679
40732
|
if (!wf) {
|
|
40680
40733
|
return { ok: false, codigo: "workflow_desconocido", motivo: `workflow desconocido: ${caso.workflow}`, validaciones: [] };
|
|
@@ -40840,9 +40893,16 @@ async function obtenerModo(ctx, companyId, workflow) {
|
|
|
40840
40893
|
const modo = rows[0]?.modo;
|
|
40841
40894
|
return typeof modo === "string" ? modo : MODO_POR_DEFECTO;
|
|
40842
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
|
+
}
|
|
40843
40903
|
async function listarPoliticas(ctx, companyId) {
|
|
40844
40904
|
const rows = await ctx.db.query(
|
|
40845
|
-
`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`,
|
|
40846
40906
|
[companyId]
|
|
40847
40907
|
);
|
|
40848
40908
|
const porWorkflow = new Map(rows.map((r) => [String(r.workflow), r]));
|
|
@@ -40855,40 +40915,50 @@ async function listarPoliticas(ctx, companyId) {
|
|
|
40855
40915
|
modo: r ? String(r.modo) : MODO_POR_DEFECTO,
|
|
40856
40916
|
motivo: r ? r.motivo ?? null : null,
|
|
40857
40917
|
actualizadaEn: r ? iso(r.updated_at) : null,
|
|
40858
|
-
porDefecto: !r
|
|
40918
|
+
porDefecto: !r,
|
|
40919
|
+
opciones: r ? jsonb(r.opciones) : {}
|
|
40859
40920
|
};
|
|
40860
40921
|
});
|
|
40861
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
|
+
}
|
|
40862
40928
|
async function cambiarPolitica(ctx, input2) {
|
|
40863
40929
|
if (!getWorkflow(input2.workflow)) throw new OpsError("workflow_desconocido", `workflow desconocido: ${input2.workflow}`);
|
|
40864
40930
|
if (input2.actor.tipo !== "humano") throw new OpsError("actor_no_permitido", "solo un humano puede cambiar la pol\xEDtica de un workflow");
|
|
40865
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;
|
|
40866
40936
|
const existe = await ctx.db.query(
|
|
40867
40937
|
`SELECT id FROM ${t(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`,
|
|
40868
40938
|
[input2.companyId, input2.workflow]
|
|
40869
40939
|
);
|
|
40870
40940
|
if (existe.length > 0) {
|
|
40871
40941
|
await ctx.db.execute(
|
|
40872
|
-
`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`,
|
|
40873
|
-
[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)]
|
|
40874
40944
|
);
|
|
40875
40945
|
} else {
|
|
40876
40946
|
await ctx.db.execute(
|
|
40877
|
-
`INSERT INTO ${t(ctx, "politica")} (id, company_id, workflow, modo, motivo, actor_tipo, actor_id) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
40878
|
-
[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)]
|
|
40879
40949
|
);
|
|
40880
40950
|
}
|
|
40881
40951
|
await ctx.db.execute(
|
|
40882
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)`,
|
|
40883
|
-
[randomUUID(), input2.companyId, input2.workflow, anterior, input2.modo,
|
|
40953
|
+
[randomUUID(), input2.companyId, input2.workflow, anterior, input2.modo, motivo, input2.actor.tipo, input2.actor.id]
|
|
40884
40954
|
);
|
|
40885
40955
|
await ctx.activity.log({
|
|
40886
40956
|
companyId: input2.companyId,
|
|
40887
|
-
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})` : ""}`,
|
|
40888
40958
|
metadata: { workflow: input2.workflow, modoAnterior: anterior, modoNuevo: input2.modo, actorId: input2.actor.id }
|
|
40889
40959
|
});
|
|
40890
40960
|
const wf = getWorkflow(input2.workflow);
|
|
40891
|
-
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 };
|
|
40892
40962
|
}
|
|
40893
40963
|
async function leerCaso(ctx, companyId, casoId) {
|
|
40894
40964
|
const rows = await ctx.db.query(
|
|
@@ -41005,7 +41075,9 @@ function ahoraEnHospital(ahora = /* @__PURE__ */ new Date()) {
|
|
|
41005
41075
|
async function contextoDe(ctx, companyId, caso, via, datos) {
|
|
41006
41076
|
const { hoy, hora, inicioDelDia } = ahoraEnHospital();
|
|
41007
41077
|
const contexto = { hoy, horaActual: hora };
|
|
41008
|
-
if (caso.workflow !== "recupero_turno"
|
|
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;
|
|
41009
41081
|
const otros = await ctx.db.query(
|
|
41010
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`,
|
|
41011
41083
|
[companyId, "recupero_turno", caso.id, "sin_candidato", "rechazado", "cancelado"]
|
|
@@ -41052,6 +41124,11 @@ async function transicionar(ctx, input2) {
|
|
|
41052
41124
|
throw new OpsError("validacion", `accion_registrada: ${problema}`, { validaciones: [{ regla: "accion_registrada", ok: false, detalle: problema }], estado: caso.estado });
|
|
41053
41125
|
}
|
|
41054
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
|
+
}
|
|
41055
41132
|
const excepcionesEvaluadas = resultado.validaciones.filter((v) => v.excepcion);
|
|
41056
41133
|
if (excepcionesEvaluadas.length > 0) datos.excepciones = excepcionesEvaluadas.filter((v) => !v.ok).map((v) => v.regla);
|
|
41057
41134
|
else if (resultado.via === "proponer") datos.excepciones = [];
|
|
@@ -41117,7 +41194,11 @@ async function registrarAccion(ctx, input2) {
|
|
|
41117
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`);
|
|
41118
41195
|
}
|
|
41119
41196
|
const id = randomUUID();
|
|
41120
|
-
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
|
+
}
|
|
41121
41202
|
await ctx.db.execute(
|
|
41122
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)`,
|
|
41123
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]
|
|
@@ -41177,8 +41258,8 @@ async function registrarCargaManual(ctx, input2) {
|
|
|
41177
41258
|
if (!sinValor && valorCargado === null) {
|
|
41178
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 });
|
|
41179
41260
|
}
|
|
41180
|
-
const
|
|
41181
|
-
const previa = evaluarTransicion({ caso, via: def2.via, actor: "humano", datos: { accion_id: "(por registrar)" }, modo: "ejecuta", contexto:
|
|
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 });
|
|
41182
41263
|
if (!previa.ok) throw new OpsError(previa.codigo, previa.motivo, { validaciones: previa.validaciones, estado: caso.estado });
|
|
41183
41264
|
const persona = str(input2.persona);
|
|
41184
41265
|
const comentario = str(input2.comentario);
|
|
@@ -41223,6 +41304,16 @@ async function registrarCargaManual(ctx, input2) {
|
|
|
41223
41304
|
throw err;
|
|
41224
41305
|
}
|
|
41225
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
|
+
}
|
|
41226
41317
|
function finalesOk() {
|
|
41227
41318
|
return [...new Set(listarWorkflows().flatMap((w) => w.finalesOk))];
|
|
41228
41319
|
}
|
|
@@ -41647,11 +41738,14 @@ function registrarHerramientas(ctx) {
|
|
|
41647
41738
|
const detalle = await leerCasoCompleto(ctx, runCtx.companyId, casoId);
|
|
41648
41739
|
if (!detalle) throw new OpsError("caso_inexistente", `no existe el caso ${casoId} en esta instituci\xF3n`);
|
|
41649
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"}` : "";
|
|
41650
41744
|
return {
|
|
41651
41745
|
content: `${resumenCaso(detalle.caso)}
|
|
41652
41746
|
Transiciones disponibles para agente: ${disponibles.join(", ") || "ninguna"}
|
|
41653
|
-
Transiciones registradas: ${detalle.transiciones.length}; acciones registradas: ${detalle.acciones.length}`,
|
|
41654
|
-
data: detalle
|
|
41747
|
+
Transiciones registradas: ${detalle.transiciones.length}; acciones registradas: ${detalle.acciones.length}${consulta}`,
|
|
41748
|
+
data: { ...detalle, opciones }
|
|
41655
41749
|
};
|
|
41656
41750
|
}));
|
|
41657
41751
|
ctx.tools.register(TOOL_NAMES.abrirCaso, toolSchema(TOOL_NAMES.abrirCaso), async (params, runCtx) => conErrores(async () => {
|
|
@@ -41836,8 +41930,10 @@ async function manejarApi(ctx, input2) {
|
|
|
41836
41930
|
return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "rechazar", motivo: str2(body.motivo), datos: conPersona(obj(body.datos)), actor }));
|
|
41837
41931
|
case "marcar-cargado":
|
|
41838
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 }));
|
|
41839
41935
|
case "version":
|
|
41840
|
-
return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos"], 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) });
|
|
41841
41937
|
case "politicas":
|
|
41842
41938
|
return respuesta(200, { politicas: await listarPoliticas(ctx, companyId) });
|
|
41843
41939
|
case "serie":
|
|
@@ -41854,7 +41950,8 @@ async function manejarApi(ctx, input2) {
|
|
|
41854
41950
|
const workflow = str2(body.workflow);
|
|
41855
41951
|
if (!workflow) return respuesta(400, { error: "workflow_requerido" });
|
|
41856
41952
|
if (!esModo(body.modo)) return respuesta(400, { error: "modo_invalido", detalle: "modo debe ser corte, observa, propone, ejecuta o autonomo" });
|
|
41857
|
-
|
|
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 }));
|
|
41858
41955
|
}
|
|
41859
41956
|
default:
|
|
41860
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": [
|