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 CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/manifest.ts
2
2
  var PLUGIN_ID = "alephoo.autopilot-ops";
3
- var PLUGIN_VERSION = "0.1.5";
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: "Abre un caso nuevo en un workflow, en su estado inicial. `referencia` identifica el objeto real (convenio, cuenta, profesional) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.",
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.5";
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: "Abre un caso nuevo en un workflow, en su estado inicial. `referencia` identifica el objeto real (convenio, cuenta, profesional) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.",
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.5";
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: "Abre un caso nuevo en un workflow, en su estado inicial. `referencia` identifica el objeto real (convenio, cuenta, profesional) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.",
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, 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)]
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, 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)]
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, 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]
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}${input2.motivo ? ` (${input2.motivo})` : ""}`,
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: 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 };
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 resultado = evaluarTransicion({ caso, via: input2.via, actor: input2.actor.tipo, datos, modo });
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 previa = evaluarTransicion({ caso, via: def2.via, actor: "humano", datos: { accion_id: "(por registrar)" }, modo: "ejecuta" });
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
- 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 }));
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.5",
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": [