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 CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/manifest.ts
2
2
  var PLUGIN_ID = "alephoo.autopilot-ops";
3
- var PLUGIN_VERSION = "0.1.6";
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.6";
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.6";
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 turnos liberados",
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 { posicion, total } = posicionEnCola(datos, contexto.solicitudesEnCurso);
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 el turno al paciente antes de asignarlo (paso opcional seg\xFAn el hospital).",
40578
- validar: (_caso, datos) => [texto(datos.canal) ? ok("canal_informado") : falla("canal_informado", "ofrecer requiere canal (tel\xE9fono, WhatsApp, correo)")]
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
- via: "asignar",
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: "Dar el turno a la solicitud aprobada en el sistema de gesti\xF3n y registrar la acci\xF3n.",
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
- ...validarLugar(caso, {}, contexto),
40608
- texto(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "asignar requiere accion_id de la escritura registrada con ops_registrar_accion")
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, input2.motivo ?? null, input2.actor.tipo, input2.actor.id]
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, input2.motivo ?? null, input2.actor.tipo, input2.actor.id]
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, input2.motivo ?? null, input2.actor.tipo, input2.actor.id]
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}${input2.motivo ? ` (${input2.motivo})` : ""}`,
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: input2.motivo ?? null, actualizadaEn: (/* @__PURE__ */ new Date()).toISOString(), porDefecto: false };
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" || via !== "proponer") return contexto;
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 { hoy, hora } = ahoraEnHospital();
41181
- const previa = evaluarTransicion({ caso, via: def2.via, actor: "humano", datos: { accion_id: "(por registrar)" }, modo: "ejecuta", contexto: { hoy, horaActual: hora } });
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
- return respuesta(200, await cambiarPolitica(ctx, { companyId, workflow, modo: body.modo, motivo: motivoConPersona(str2(body.motivo)), actor }));
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.6",
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": [