paperclip-plugin-autopilot-ops 0.1.5 → 0.1.6

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.6";
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; `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: {
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.6";
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; `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: {
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.6";
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; `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: {
@@ -40409,9 +40409,255 @@ var tareaOperativa = {
40409
40409
  { via: "cancelar", de: ["abierta", "en_curso", "pendiente_aprobacion"], a: "cancelada", actor: ["humano"], descripcion: "Cancelaci\xF3n manual." }
40410
40410
  ]
40411
40411
  };
40412
+ var TOPE_DIARIO_RECUPERO = 20;
40413
+ var TITULOS = /* @__PURE__ */ new Set(["dr", "dra", "doctor", "doctora", "lic", "licenciado", "licenciada", "prof"]);
40414
+ function normalizarNombre(v) {
40415
+ const s = texto(v);
40416
+ if (!s) return null;
40417
+ const palabras = s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").split(" ").filter((p) => p && !TITULOS.has(p));
40418
+ return palabras.length ? palabras.sort().join(" ") : null;
40419
+ }
40420
+ function horaHM(v) {
40421
+ const s = texto(v);
40422
+ const m = s ? /^(\d{1,2}):(\d{2})(?::\d{2})?$/.exec(s) : null;
40423
+ if (!m) return null;
40424
+ const h = Number(m[1]);
40425
+ const min = Number(m[2]);
40426
+ return h < 24 && min < 60 ? `${String(h).padStart(2, "0")}:${m[2]}` : null;
40427
+ }
40428
+ function refExterna(v) {
40429
+ if (typeof v === "number" && Number.isFinite(v)) return String(v);
40430
+ return texto(v);
40431
+ }
40432
+ function fechaAISO(v) {
40433
+ const s = texto(v);
40434
+ if (!s) return null;
40435
+ if (fechaISO(s.slice(0, 10))) return s.slice(0, 10);
40436
+ const m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})/.exec(s);
40437
+ if (!m) return null;
40438
+ const iso3 = `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`;
40439
+ return fechaISO(iso3) ? iso3 : null;
40440
+ }
40441
+ function leerCola(v) {
40442
+ if (!Array.isArray(v) || v.length === 0) return null;
40443
+ const out = [];
40444
+ for (const e of v) {
40445
+ if (!e || typeof e !== "object") return null;
40446
+ const r = e;
40447
+ const id = refExterna(r.solicitud_id);
40448
+ if (!id) return null;
40449
+ out.push({ solicitud_id: id, prioridad: texto(r.prioridad), alta: fechaAISO(r.alta) });
40450
+ }
40451
+ return out;
40452
+ }
40453
+ function ordenarCola(cola) {
40454
+ const pronta = (e) => normalizarNombre(e.prioridad)?.includes("pronta") ? 0 : 1;
40455
+ 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));
40456
+ }
40457
+ function posicionEnCola(datos, enCurso = []) {
40458
+ const cola = leerCola(datos.cola);
40459
+ const elegida = refExterna(datos.solicitud_id);
40460
+ const fuera = new Set(enCurso);
40461
+ const vigente = cola?.filter((e) => e.solicitud_id === elegida || !fuera.has(e.solicitud_id)) ?? null;
40462
+ if (!vigente || !elegida) return { posicion: null, total: vigente?.length ?? 0 };
40463
+ const i = ordenarCola(vigente).findIndex((e) => e.solicitud_id === elegida);
40464
+ return { posicion: i === -1 ? null : i + 1, total: vigente.length };
40465
+ }
40466
+ function fueraDePreferencia(preferencia, hora) {
40467
+ const p = normalizarNombre(preferencia);
40468
+ if (!p || !hora) return false;
40469
+ if (p.includes("manana")) return hora >= "13:00";
40470
+ if (p.includes("tarde")) return hora < "13:00";
40471
+ return false;
40472
+ }
40473
+ function validarLugar(caso, datos, contexto) {
40474
+ const l = { ...caso.datos, ...datos };
40475
+ const completo = fechaISO(l.fecha) && horaHM(l.hora) !== null && texto(l.profesional) !== null && texto(l.especialidad) !== null;
40476
+ const out = [completo ? ok("lugar_completo") : falla("lugar_completo", "el turno liberado necesita fecha (AAAA-MM-DD), hora (HH:MM), profesional y especialidad")];
40477
+ if (fechaISO(l.fecha) && contexto.hoy) {
40478
+ const fecha = String(l.fecha);
40479
+ const hora = horaHM(l.hora);
40480
+ const futuro = fecha > contexto.hoy || fecha === contexto.hoy && (!contexto.horaActual || hora !== null && hora > contexto.horaActual);
40481
+ out.push(futuro ? ok("lugar_futuro") : falla("lugar_futuro", `el turno del ${fecha}${hora ? ` a las ${hora}` : ""} ya pas\xF3: no se puede recuperar`));
40482
+ }
40483
+ return out;
40484
+ }
40485
+ var recuperoTurno = {
40486
+ clave: "recupero_turno",
40487
+ nombre: "Recupero de turnos liberados",
40488
+ puesto: "Pacientes y agenda",
40489
+ 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
+ inicial: "detectado",
40491
+ estados: [
40492
+ "detectado",
40493
+ "pendiente_aprobacion",
40494
+ "aprobado",
40495
+ "ofrecido",
40496
+ "aceptado",
40497
+ "asignado",
40498
+ "verificado",
40499
+ "con_diferencias",
40500
+ "sin_candidato",
40501
+ "rechazado",
40502
+ "cancelado"
40503
+ ],
40504
+ finalesOk: ["verificado"],
40505
+ finalesNoOk: ["sin_candidato", "rechazado", "cancelado"],
40506
+ minutosAbsorbidos: 15,
40507
+ transiciones: [
40508
+ {
40509
+ via: "proponer",
40510
+ de: ["detectado", "con_diferencias"],
40511
+ a: "pendiente_aprobacion",
40512
+ actor: ["agente", "humano"],
40513
+ nivelRequerido: "propone",
40514
+ 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.",
40515
+ validar: (caso, datos, contexto) => {
40516
+ const l = { ...caso.datos, ...datos };
40517
+ const out = validarLugar(caso, datos, contexto);
40518
+ 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"));
40519
+ const estado = normalizarNombre(datos.solicitud_estado);
40520
+ out.push(estado === "pendiente" ? ok("solicitud_pendiente") : falla("solicitud_pendiente", `la solicitud tiene que estar pendiente (est\xE1: ${texto(datos.solicitud_estado) ?? "sin informar"})`));
40521
+ const esp = normalizarNombre(l.especialidad);
40522
+ const espSolicitud = normalizarNombre(datos.solicitud_especialidad);
40523
+ 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
+ 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);
40526
+ 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
+ else if (posicion === null) out.push(falla("orden_de_la_cola", "la solicitud elegida no est\xE1 en la cola informada"));
40528
+ 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`));
40529
+ 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`));
40530
+ out.push(texto(datos.fuente) ? ok("fuente_presente") : falla("fuente_presente", "fuente (pantalla donde se vio el turno libre y la solicitud) es obligatoria"));
40531
+ return out;
40532
+ },
40533
+ autonomia: {
40534
+ aSinExcepcion: "aprobado",
40535
+ excepciones: (caso, datos, contexto) => {
40536
+ const l = { ...caso.datos, ...datos };
40537
+ const { posicion, total } = posicionEnCola(datos, contexto.solicitudesEnCurso);
40538
+ const pedido = normalizarNombre(datos.solicitud_profesional);
40539
+ const profesional = normalizarNombre(l.profesional);
40540
+ const paraHoy = Boolean(contexto.hoy && l.fecha === contexto.hoy);
40541
+ const asignados = contexto.asignacionesHoy ?? 0;
40542
+ const fuera = fueraDePreferencia(datos.solicitud_preferencia_horaria, horaHM(l.hora));
40543
+ return [
40544
+ 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"),
40545
+ 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"),
40546
+ 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"),
40547
+ excepcion("turno_para_hoy", paraHoy, paraHoy ? "el turno es para hoy" : "no es para hoy"),
40548
+ excepcion("sobreturno", l.sobreturno === true, l.sobreturno === true ? "es un sobreturno" : "turno de la grilla"),
40549
+ excepcion("reintento_tras_diferencias", caso.estado === "con_diferencias", caso.estado === "con_diferencias" ? "la asignaci\xF3n anterior no coincidi\xF3 al comprobar" : "primer intento"),
40550
+ excepcion("tope_diario", asignados >= TOPE_DIARIO_RECUPERO, `${asignados} turnos asignados hoy (tope ${TOPE_DIARIO_RECUPERO})`)
40551
+ ];
40552
+ }
40553
+ }
40554
+ },
40555
+ {
40556
+ via: "aprobar",
40557
+ de: ["pendiente_aprobacion"],
40558
+ a: "aprobado",
40559
+ actor: ["humano"],
40560
+ descripcion: "Aprobaci\xF3n del responsable del hospital."
40561
+ },
40562
+ {
40563
+ via: "rechazar",
40564
+ de: ["pendiente_aprobacion"],
40565
+ a: "rechazado",
40566
+ actor: ["humano"],
40567
+ descripcion: "Rechazo con motivo.",
40568
+ validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "el rechazo requiere motivo")]
40569
+ },
40570
+ {
40571
+ // Paso opcional: donde el hospital quiere que el paciente acepte antes de asignarle el turno.
40572
+ via: "ofrecer",
40573
+ de: ["aprobado"],
40574
+ a: "ofrecido",
40575
+ actor: ["agente", "humano"],
40576
+ 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)")]
40579
+ },
40580
+ {
40581
+ via: "aceptar",
40582
+ de: ["ofrecido"],
40583
+ a: "aceptado",
40584
+ actor: ["agente", "humano"],
40585
+ nivelRequerido: "observa",
40586
+ descripcion: "El paciente acept\xF3 el turno ofrecido.",
40587
+ validar: (_caso, datos) => [texto(datos.respuesta) ? ok("respuesta_registrada") : falla("respuesta_registrada", "aceptar requiere la respuesta del paciente")]
40588
+ },
40589
+ {
40590
+ via: "no_acepta",
40591
+ de: ["ofrecido"],
40592
+ a: "detectado",
40593
+ actor: ["agente", "humano"],
40594
+ nivelRequerido: "observa",
40595
+ descripcion: "El paciente no acept\xF3 o no respondi\xF3: el turno vuelve a buscar candidato.",
40596
+ 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
+ },
40598
+ {
40599
+ via: "asignar",
40600
+ de: ["aprobado", "aceptado"],
40601
+ a: "asignado",
40602
+ actor: ["agente", "humano"],
40603
+ nivelRequerido: "ejecuta",
40604
+ cargaManual: true,
40605
+ descripcion: "Dar el turno a la solicitud aprobada en el sistema de gesti\xF3n y registrar la acci\xF3n.",
40606
+ 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")
40609
+ ]
40610
+ },
40611
+ {
40612
+ via: "verificar",
40613
+ de: ["asignado"],
40614
+ a: (caso, datos) => {
40615
+ const l = caso.datos;
40616
+ 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));
40617
+ return coincide ? "verificado" : "con_diferencias";
40618
+ },
40619
+ actor: ["agente", "humano", "sistema"],
40620
+ nivelRequerido: "observa",
40621
+ 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.",
40622
+ validar: (_caso, datos) => [
40623
+ 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")
40624
+ ]
40625
+ },
40626
+ {
40627
+ via: "corregir",
40628
+ de: ["con_diferencias"],
40629
+ a: "asignado",
40630
+ actor: ["humano"],
40631
+ cargaManual: true,
40632
+ descripcion: "Corregir a mano el turno en el sistema de gesti\xF3n (lo aprobado no cambia) y volver a comprobar.",
40633
+ validar: (_caso, datos) => [
40634
+ texto(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "corregir requiere accion_id de la escritura registrada con la correcci\xF3n")
40635
+ ]
40636
+ },
40637
+ {
40638
+ via: "sin_candidato",
40639
+ de: ["detectado"],
40640
+ a: "sin_candidato",
40641
+ actor: ["agente", "humano"],
40642
+ nivelRequerido: "observa",
40643
+ descripcion: "No hay solicitudes pendientes que puedan tomar el turno liberado: queda libre.",
40644
+ 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)")]
40645
+ },
40646
+ {
40647
+ via: "descartar",
40648
+ de: ["detectado", "pendiente_aprobacion", "aprobado", "ofrecido", "aceptado"],
40649
+ a: "cancelado",
40650
+ actor: ["agente", "humano"],
40651
+ nivelRequerido: "observa",
40652
+ 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.",
40653
+ validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "descartar requiere motivo")]
40654
+ }
40655
+ ]
40656
+ };
40412
40657
  var WORKFLOWS = {
40413
40658
  [actualizacionArancel.clave]: actualizacionArancel,
40414
- [tareaOperativa.clave]: tareaOperativa
40659
+ [tareaOperativa.clave]: tareaOperativa,
40660
+ [recuperoTurno.clave]: recuperoTurno
40415
40661
  };
40416
40662
  function getWorkflow(clave) {
40417
40663
  return WORKFLOWS[clave] ?? null;
@@ -40428,6 +40674,7 @@ function modoHabilita(modo, nivelRequerido) {
40428
40674
  function evaluarTransicion(input2) {
40429
40675
  const { caso, via, actor } = input2;
40430
40676
  const datos = input2.datos ?? {};
40677
+ const contexto = input2.contexto ?? {};
40431
40678
  const wf = getWorkflow(caso.workflow);
40432
40679
  if (!wf) {
40433
40680
  return { ok: false, codigo: "workflow_desconocido", motivo: `workflow desconocido: ${caso.workflow}`, validaciones: [] };
@@ -40447,7 +40694,7 @@ function evaluarTransicion(input2) {
40447
40694
  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
40695
  return { ok: false, codigo: "politica", motivo, validaciones: [] };
40449
40696
  }
40450
- const validaciones = def2.validar ? def2.validar(caso, datos) : [];
40697
+ const validaciones = def2.validar ? def2.validar(caso, datos, contexto) : [];
40451
40698
  const fallidas = validaciones.filter((v) => !v.ok);
40452
40699
  if (fallidas.length > 0) {
40453
40700
  return { ok: false, codigo: "validacion", motivo: fallidas.map((f) => `${f.regla}: ${f.detalle ?? "fall\xF3"}`).join("; "), validaciones };
@@ -40455,7 +40702,7 @@ function evaluarTransicion(input2) {
40455
40702
  let a = typeof def2.a === "function" ? def2.a(caso, datos) : def2.a;
40456
40703
  let autoaprobado = false;
40457
40704
  if (actor === "agente" && input2.modo === "autonomo" && def2.autonomia) {
40458
- const excepciones = def2.autonomia.excepciones(caso, datos);
40705
+ const excepciones = def2.autonomia.excepciones(caso, datos, contexto);
40459
40706
  validaciones.push(...excepciones);
40460
40707
  if (excepciones.every((e) => e.ok)) {
40461
40708
  a = def2.autonomia.aSinExcepcion;
@@ -40748,6 +40995,32 @@ async function abrirCaso(ctx, input2) {
40748
40995
  }
40749
40996
  };
40750
40997
  }
40998
+ var ZONA_HOSPITAL = "America/Argentina/Buenos_Aires";
40999
+ function ahoraEnHospital(ahora = /* @__PURE__ */ new Date()) {
41000
+ 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);
41001
+ const p = (tipo) => partes.find((x) => x.type === tipo)?.value ?? "00";
41002
+ const hoy = `${p("year")}-${p("month")}-${p("day")}`;
41003
+ return { hoy, hora: `${p("hour")}:${p("minute")}`, inicioDelDia: /* @__PURE__ */ new Date(`${hoy}T03:00:00Z`) };
41004
+ }
41005
+ async function contextoDe(ctx, companyId, caso, via, datos) {
41006
+ const { hoy, hora, inicioDelDia } = ahoraEnHospital();
41007
+ const contexto = { hoy, horaActual: hora };
41008
+ if (caso.workflow !== "recupero_turno" || via !== "proponer") return contexto;
41009
+ const otros = await ctx.db.query(
41010
+ `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
+ [companyId, "recupero_turno", caso.id, "sin_candidato", "rechazado", "cancelado"]
41012
+ );
41013
+ contexto.solicitudesEnCurso = [...new Set(otros.map((r) => String(r.solicitud_id)))];
41014
+ const solicitud = refExterna(datos.solicitud_id);
41015
+ const mismo = solicitud ? otros.find((r) => String(r.solicitud_id) === solicitud) : void 0;
41016
+ contexto.solicitudEnOtroCaso = mismo ? String(mismo.id) : null;
41017
+ const asignados = await ctx.db.query(
41018
+ `SELECT count(*)::int AS cantidad FROM ${t(ctx, "transicion")} WHERE company_id = $1 AND via = $2 AND created_at >= $3`,
41019
+ [companyId, "asignar", inicioDelDia.toISOString()]
41020
+ );
41021
+ contexto.asignacionesHoy = Number(asignados[0]?.cantidad ?? 0);
41022
+ return contexto;
41023
+ }
40751
41024
  async function transicionar(ctx, input2) {
40752
41025
  const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
40753
41026
  if (!caso) throw new OpsError("caso_inexistente", `no existe el caso ${input2.casoId} en esta instituci\xF3n`);
@@ -40762,7 +41035,8 @@ async function transicionar(ctx, input2) {
40762
41035
  const datos = { ...input2.datos ?? {} };
40763
41036
  if (input2.motivo && !datos.motivo) datos.motivo = input2.motivo;
40764
41037
  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 });
41038
+ const contexto = await contextoDe(ctx, input2.companyId, caso, input2.via, datos);
41039
+ const resultado = evaluarTransicion({ caso, via: input2.via, actor: input2.actor.tipo, datos, modo, contexto });
40766
41040
  if (!resultado.ok) {
40767
41041
  throw new OpsError(resultado.codigo, resultado.motivo, { validaciones: resultado.validaciones, estado: caso.estado });
40768
41042
  }
@@ -40778,6 +41052,20 @@ async function transicionar(ctx, input2) {
40778
41052
  throw new OpsError("validacion", `accion_registrada: ${problema}`, { validaciones: [{ regla: "accion_registrada", ok: false, detalle: problema }], estado: caso.estado });
40779
41053
  }
40780
41054
  }
41055
+ const excepcionesEvaluadas = resultado.validaciones.filter((v) => v.excepcion);
41056
+ if (excepcionesEvaluadas.length > 0) datos.excepciones = excepcionesEvaluadas.filter((v) => !v.ok).map((v) => v.regla);
41057
+ else if (resultado.via === "proponer") datos.excepciones = [];
41058
+ else delete datos.excepciones;
41059
+ if (caso.workflow === "recupero_turno" && resultado.via === "proponer") {
41060
+ const enCurso = new Set(contexto.solicitudesEnCurso ?? []);
41061
+ const { posicion, total } = posicionEnCola(datos, enCurso);
41062
+ datos.posicion_en_cola = posicion;
41063
+ datos.largo_cola = total;
41064
+ const cola = leerCola(datos.cola);
41065
+ const elegida = cola?.find((e) => e.solicitud_id === refExterna(datos.solicitud_id));
41066
+ if (elegida?.prioridad && !str(datos.solicitud_prioridad)) datos.solicitud_prioridad = elegida.prioridad;
41067
+ 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);
41068
+ }
40781
41069
  const transicionId = randomUUID();
40782
41070
  await ctx.db.execute(
40783
41071
  `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)`,
@@ -40889,7 +41177,8 @@ async function registrarCargaManual(ctx, input2) {
40889
41177
  if (!sinValor && valorCargado === null) {
40890
41178
  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
41179
  }
40892
- const previa = evaluarTransicion({ caso, via: def2.via, actor: "humano", datos: { accion_id: "(por registrar)" }, modo: "ejecuta" });
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 } });
40893
41182
  if (!previa.ok) throw new OpsError(previa.codigo, previa.motivo, { validaciones: previa.validaciones, estado: caso.estado });
40894
41183
  const persona = str(input2.persona);
40895
41184
  const comentario = str(input2.comentario);
@@ -41548,7 +41837,7 @@ async function manejarApi(ctx, input2) {
41548
41837
  case "marcar-cargado":
41549
41838
  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 }));
41550
41839
  case "version":
41551
- return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual"], workflows: listarWorkflows().map((w) => w.clave) });
41840
+ return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos"], workflows: listarWorkflows().map((w) => w.clave) });
41552
41841
  case "politicas":
41553
41842
  return respuesta(200, { politicas: await listarPoliticas(ctx, companyId) });
41554
41843
  case "serie":
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.6",
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": [