paperclip-plugin-autopilot-ops 0.1.32 → 0.1.34

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.32";
3
+ var PLUGIN_VERSION = "0.1.34";
4
4
  var DB_NAMESPACE_SLUG = "autopilot_ops";
5
5
  var PAGE_ROUTE = "autopilot";
6
6
  var TOOL_NAMES = {
@@ -68,13 +68,18 @@ var manifest = {
68
68
  {
69
69
  name: TOOL_NAMES.listarCasos,
70
70
  displayName: "Listar casos",
71
- description: "Lista casos del registro de negocio de la instituci\xF3n. Filtros opcionales: workflow, estado, limit (m\xE1x. 100). Solo lectura.",
71
+ description: 'Lista casos del registro de negocio de la instituci\xF3n, abiertos y cerrados, del m\xE1s recientemente actualizado al m\xE1s viejo. Filtros opcionales: workflow (si el plugin no tiene esa funci\xF3n responde el error `workflow_desconocido`); estado (uno) o estados (varios, en un arreglo); referencia (todos los casos de ese objeto real, por ejemplo `rentabilidad:<prestacion_id>:<financiador_id>`, para saber si ya tiene caso sin recorrer el listado); limit (por defecto 50, m\xE1x. 500). Para recorrer todos sin perder ninguno aunque cambien mientras tanto: `orden: "id"` y, en cada p\xE1gina siguiente, `despues_de` con el id que indica la respuesta (\xABPuede haber m\xE1s casos\u2026\xBB); si vuelven menos casos que el limit, no hay m\xE1s. `offset` pagina el orden por actualizaci\xF3n (se corre si los casos cambian entre una p\xE1gina y otra). Solo lectura.',
72
72
  parametersSchema: {
73
73
  type: "object",
74
74
  properties: {
75
75
  workflow: { type: "string" },
76
76
  estado: { type: "string" },
77
- limit: { type: "number" }
77
+ estados: { type: "array", items: { type: "string" }, description: "Varios estados a la vez (se ignora si viene estado)." },
78
+ referencia: { type: "string", description: "Solo los casos con esta referencia exacta." },
79
+ limit: { type: "number" },
80
+ orden: { type: "string", enum: ["actualizacion", "id"], description: '"id": orden estable para recorrer todo con despues_de. Por defecto, por \xFAltima actualizaci\xF3n.' },
81
+ despues_de: { type: "string", description: 'Id del \xFAltimo caso de la p\xE1gina anterior (con orden "id"): trae los que siguen.' },
82
+ offset: { type: "number" }
78
83
  }
79
84
  }
80
85
  },
@@ -128,7 +133,7 @@ var manifest = {
128
133
  {
129
134
  name: TOOL_NAMES.abrirCaso,
130
135
  displayName: "Abrir caso",
131
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `internacion_al_dia` (una internaci\xF3n abierta; referencia obligatoria `internacion:<id>`; datos: internacion_id), `honorario_en_cero` (referencia `honorarios:<profesional_id>`; datos: profesional_id, profesional), `debito_profesional` (referencia `debito:<profesional_id>:<nota_credito_id>`; datos: profesional_id, profesional, nota_credito_id, obra_social), `liquidacion_profesional` (referencia `liquidacion:<profesional_id>:<AAAA-MM-DD>`; datos: profesional_id, profesional, liquidado_el, institucion, mail_his), `cobranza_financiador` (un caso abierto por financiador; referencia obligatoria `cobranza:<obra_social_id>`; datos: obra_social (nombre como en el HIS), obra_social_id, institucion (nombre de la instituci\xF3n para el reclamo)), `circular_aranceles` (una circular de aranceles entera; referencia obligatoria `circular:<obra social>:<hash corto del archivo>`; datos: obra_social, convenio (nombre en el HIS), archivo {nombre, hash, paginas}, vigencia_desde AAAA-MM-DD, porcentaje_declarado y filas_declaradas si la circular los dice), `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id (regla activa) y candidato tal cual lo devuelve buscar-demanda.mjs; el plugin verifica la regla con el candidato (si no la cumple, no abre) y fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`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"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos, internacion_id opcional; en faltantes, `responsable` {tipo profesional, id, nombre} opcional para que el plugin avise a quien resuelve), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal de la instituci\xF3n), matricula_nacional, matricula_provincial, codigo_provincia, especialidades). `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.',
136
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `internacion_al_dia` (una internaci\xF3n abierta; referencia obligatoria `internacion:<id>`; datos: internacion_id), `honorario_en_cero` (referencia `honorarios:<profesional_id>`; datos: profesional_id, profesional), `debito_profesional` (referencia `debito:<profesional_id>:<nota_credito_id>`; datos: profesional_id, profesional, nota_credito_id, obra_social), `liquidacion_profesional` (referencia `liquidacion:<profesional_id>:<AAAA-MM-DD>`; datos: profesional_id, profesional, liquidado_el, institucion, mail_his), `cobranza_financiador` (un caso abierto por financiador; referencia obligatoria `cobranza:<obra_social_id>`; datos: obra_social (nombre como en el HIS), obra_social_id, institucion (nombre de la instituci\xF3n para el reclamo)), `circular_aranceles` (una circular de aranceles entera; referencia obligatoria `circular:<obra social>:<hash corto del archivo>`; datos: obra_social, convenio (nombre en el HIS), archivo {nombre, hash, paginas}, vigencia_desde AAAA-MM-DD, porcentaje_declarado y filas_declaradas si la circular los dice), `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id (regla activa) y candidato tal cual lo devuelve buscar-demanda.mjs; el plugin verifica la regla con el candidato (si no la cumple, no abre) y fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`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"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos, internacion_id opcional; en faltantes, `responsable` {tipo profesional, id, nombre} opcional para que el plugin avise a quien resuelve), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal de la instituci\xF3n), matricula_nacional, matricula_provincial, codigo_provincia, especialidades), `rentabilidad_practica` (referencia obligatoria `rentabilidad:<prestacion_id>:<financiador_id>`; datos: prestacion_id, financiador_id, codigo, practica, financiador), `rentabilidad_internacion` (una internaci\xF3n egresada; referencia obligatoria `rentabilidad_internacion:<internacion_id>`; datos: internacion_id, financiador_id, financiador). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese (creado: false). En `rentabilidad_internacion` se devuelve el caso existente aunque est\xE9 cerrado: una internaci\xF3n se mide una sola vez. Pasar `idempotencyKey` para reintentos seguros.',
132
137
  parametersSchema: {
133
138
  type: "object",
134
139
  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.32";
11
+ var PLUGIN_VERSION = "0.1.34";
12
12
  var DB_NAMESPACE_SLUG = "autopilot_ops";
13
13
  var PAGE_ROUTE = "autopilot";
14
14
  var TOOL_NAMES = {
@@ -76,13 +76,18 @@ var manifest = {
76
76
  {
77
77
  name: TOOL_NAMES.listarCasos,
78
78
  displayName: "Listar casos",
79
- description: "Lista casos del registro de negocio de la instituci\xF3n. Filtros opcionales: workflow, estado, limit (m\xE1x. 100). Solo lectura.",
79
+ description: 'Lista casos del registro de negocio de la instituci\xF3n, abiertos y cerrados, del m\xE1s recientemente actualizado al m\xE1s viejo. Filtros opcionales: workflow (si el plugin no tiene esa funci\xF3n responde el error `workflow_desconocido`); estado (uno) o estados (varios, en un arreglo); referencia (todos los casos de ese objeto real, por ejemplo `rentabilidad:<prestacion_id>:<financiador_id>`, para saber si ya tiene caso sin recorrer el listado); limit (por defecto 50, m\xE1x. 500). Para recorrer todos sin perder ninguno aunque cambien mientras tanto: `orden: "id"` y, en cada p\xE1gina siguiente, `despues_de` con el id que indica la respuesta (\xABPuede haber m\xE1s casos\u2026\xBB); si vuelven menos casos que el limit, no hay m\xE1s. `offset` pagina el orden por actualizaci\xF3n (se corre si los casos cambian entre una p\xE1gina y otra). Solo lectura.',
80
80
  parametersSchema: {
81
81
  type: "object",
82
82
  properties: {
83
83
  workflow: { type: "string" },
84
84
  estado: { type: "string" },
85
- limit: { type: "number" }
85
+ estados: { type: "array", items: { type: "string" }, description: "Varios estados a la vez (se ignora si viene estado)." },
86
+ referencia: { type: "string", description: "Solo los casos con esta referencia exacta." },
87
+ limit: { type: "number" },
88
+ orden: { type: "string", enum: ["actualizacion", "id"], description: '"id": orden estable para recorrer todo con despues_de. Por defecto, por \xFAltima actualizaci\xF3n.' },
89
+ despues_de: { type: "string", description: 'Id del \xFAltimo caso de la p\xE1gina anterior (con orden "id"): trae los que siguen.' },
90
+ offset: { type: "number" }
86
91
  }
87
92
  }
88
93
  },
@@ -136,7 +141,7 @@ var manifest = {
136
141
  {
137
142
  name: TOOL_NAMES.abrirCaso,
138
143
  displayName: "Abrir caso",
139
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `internacion_al_dia` (una internaci\xF3n abierta; referencia obligatoria `internacion:<id>`; datos: internacion_id), `honorario_en_cero` (referencia `honorarios:<profesional_id>`; datos: profesional_id, profesional), `debito_profesional` (referencia `debito:<profesional_id>:<nota_credito_id>`; datos: profesional_id, profesional, nota_credito_id, obra_social), `liquidacion_profesional` (referencia `liquidacion:<profesional_id>:<AAAA-MM-DD>`; datos: profesional_id, profesional, liquidado_el, institucion, mail_his), `cobranza_financiador` (un caso abierto por financiador; referencia obligatoria `cobranza:<obra_social_id>`; datos: obra_social (nombre como en el HIS), obra_social_id, institucion (nombre de la instituci\xF3n para el reclamo)), `circular_aranceles` (una circular de aranceles entera; referencia obligatoria `circular:<obra social>:<hash corto del archivo>`; datos: obra_social, convenio (nombre en el HIS), archivo {nombre, hash, paginas}, vigencia_desde AAAA-MM-DD, porcentaje_declarado y filas_declaradas si la circular los dice), `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id (regla activa) y candidato tal cual lo devuelve buscar-demanda.mjs; el plugin verifica la regla con el candidato (si no la cumple, no abre) y fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`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"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos, internacion_id opcional; en faltantes, `responsable` {tipo profesional, id, nombre} opcional para que el plugin avise a quien resuelve), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal de la instituci\xF3n), matricula_nacional, matricula_provincial, codigo_provincia, especialidades). `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.',
144
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `internacion_al_dia` (una internaci\xF3n abierta; referencia obligatoria `internacion:<id>`; datos: internacion_id), `honorario_en_cero` (referencia `honorarios:<profesional_id>`; datos: profesional_id, profesional), `debito_profesional` (referencia `debito:<profesional_id>:<nota_credito_id>`; datos: profesional_id, profesional, nota_credito_id, obra_social), `liquidacion_profesional` (referencia `liquidacion:<profesional_id>:<AAAA-MM-DD>`; datos: profesional_id, profesional, liquidado_el, institucion, mail_his), `cobranza_financiador` (un caso abierto por financiador; referencia obligatoria `cobranza:<obra_social_id>`; datos: obra_social (nombre como en el HIS), obra_social_id, institucion (nombre de la instituci\xF3n para el reclamo)), `circular_aranceles` (una circular de aranceles entera; referencia obligatoria `circular:<obra social>:<hash corto del archivo>`; datos: obra_social, convenio (nombre en el HIS), archivo {nombre, hash, paginas}, vigencia_desde AAAA-MM-DD, porcentaje_declarado y filas_declaradas si la circular los dice), `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id (regla activa) y candidato tal cual lo devuelve buscar-demanda.mjs; el plugin verifica la regla con el candidato (si no la cumple, no abre) y fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`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"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos, internacion_id opcional; en faltantes, `responsable` {tipo profesional, id, nombre} opcional para que el plugin avise a quien resuelve), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal de la instituci\xF3n), matricula_nacional, matricula_provincial, codigo_provincia, especialidades), `rentabilidad_practica` (referencia obligatoria `rentabilidad:<prestacion_id>:<financiador_id>`; datos: prestacion_id, financiador_id, codigo, practica, financiador), `rentabilidad_internacion` (una internaci\xF3n egresada; referencia obligatoria `rentabilidad_internacion:<internacion_id>`; datos: internacion_id, financiador_id, financiador). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese (creado: false). En `rentabilidad_internacion` se devuelve el caso existente aunque est\xE9 cerrado: una internaci\xF3n se mide una sola vez. Pasar `idempotencyKey` para reintentos seguros.',
140
145
  parametersSchema: {
141
146
  type: "object",
142
147
  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.32";
39935
+ var PLUGIN_VERSION = "0.1.34";
39936
39936
  var DB_NAMESPACE_SLUG = "autopilot_ops";
39937
39937
  var PAGE_ROUTE = "autopilot";
39938
39938
  var TOOL_NAMES = {
@@ -40000,13 +40000,18 @@ var manifest = {
40000
40000
  {
40001
40001
  name: TOOL_NAMES.listarCasos,
40002
40002
  displayName: "Listar casos",
40003
- description: "Lista casos del registro de negocio de la instituci\xF3n. Filtros opcionales: workflow, estado, limit (m\xE1x. 100). Solo lectura.",
40003
+ description: 'Lista casos del registro de negocio de la instituci\xF3n, abiertos y cerrados, del m\xE1s recientemente actualizado al m\xE1s viejo. Filtros opcionales: workflow (si el plugin no tiene esa funci\xF3n responde el error `workflow_desconocido`); estado (uno) o estados (varios, en un arreglo); referencia (todos los casos de ese objeto real, por ejemplo `rentabilidad:<prestacion_id>:<financiador_id>`, para saber si ya tiene caso sin recorrer el listado); limit (por defecto 50, m\xE1x. 500). Para recorrer todos sin perder ninguno aunque cambien mientras tanto: `orden: "id"` y, en cada p\xE1gina siguiente, `despues_de` con el id que indica la respuesta (\xABPuede haber m\xE1s casos\u2026\xBB); si vuelven menos casos que el limit, no hay m\xE1s. `offset` pagina el orden por actualizaci\xF3n (se corre si los casos cambian entre una p\xE1gina y otra). Solo lectura.',
40004
40004
  parametersSchema: {
40005
40005
  type: "object",
40006
40006
  properties: {
40007
40007
  workflow: { type: "string" },
40008
40008
  estado: { type: "string" },
40009
- limit: { type: "number" }
40009
+ estados: { type: "array", items: { type: "string" }, description: "Varios estados a la vez (se ignora si viene estado)." },
40010
+ referencia: { type: "string", description: "Solo los casos con esta referencia exacta." },
40011
+ limit: { type: "number" },
40012
+ orden: { type: "string", enum: ["actualizacion", "id"], description: '"id": orden estable para recorrer todo con despues_de. Por defecto, por \xFAltima actualizaci\xF3n.' },
40013
+ despues_de: { type: "string", description: 'Id del \xFAltimo caso de la p\xE1gina anterior (con orden "id"): trae los que siguen.' },
40014
+ offset: { type: "number" }
40010
40015
  }
40011
40016
  }
40012
40017
  },
@@ -40060,7 +40065,7 @@ var manifest = {
40060
40065
  {
40061
40066
  name: TOOL_NAMES.abrirCaso,
40062
40067
  displayName: "Abrir caso",
40063
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `internacion_al_dia` (una internaci\xF3n abierta; referencia obligatoria `internacion:<id>`; datos: internacion_id), `honorario_en_cero` (referencia `honorarios:<profesional_id>`; datos: profesional_id, profesional), `debito_profesional` (referencia `debito:<profesional_id>:<nota_credito_id>`; datos: profesional_id, profesional, nota_credito_id, obra_social), `liquidacion_profesional` (referencia `liquidacion:<profesional_id>:<AAAA-MM-DD>`; datos: profesional_id, profesional, liquidado_el, institucion, mail_his), `cobranza_financiador` (un caso abierto por financiador; referencia obligatoria `cobranza:<obra_social_id>`; datos: obra_social (nombre como en el HIS), obra_social_id, institucion (nombre de la instituci\xF3n para el reclamo)), `circular_aranceles` (una circular de aranceles entera; referencia obligatoria `circular:<obra social>:<hash corto del archivo>`; datos: obra_social, convenio (nombre en el HIS), archivo {nombre, hash, paginas}, vigencia_desde AAAA-MM-DD, porcentaje_declarado y filas_declaradas si la circular los dice), `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id (regla activa) y candidato tal cual lo devuelve buscar-demanda.mjs; el plugin verifica la regla con el candidato (si no la cumple, no abre) y fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`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"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos, internacion_id opcional; en faltantes, `responsable` {tipo profesional, id, nombre} opcional para que el plugin avise a quien resuelve), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal de la instituci\xF3n), matricula_nacional, matricula_provincial, codigo_provincia, especialidades). `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.',
40068
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `internacion_al_dia` (una internaci\xF3n abierta; referencia obligatoria `internacion:<id>`; datos: internacion_id), `honorario_en_cero` (referencia `honorarios:<profesional_id>`; datos: profesional_id, profesional), `debito_profesional` (referencia `debito:<profesional_id>:<nota_credito_id>`; datos: profesional_id, profesional, nota_credito_id, obra_social), `liquidacion_profesional` (referencia `liquidacion:<profesional_id>:<AAAA-MM-DD>`; datos: profesional_id, profesional, liquidado_el, institucion, mail_his), `cobranza_financiador` (un caso abierto por financiador; referencia obligatoria `cobranza:<obra_social_id>`; datos: obra_social (nombre como en el HIS), obra_social_id, institucion (nombre de la instituci\xF3n para el reclamo)), `circular_aranceles` (una circular de aranceles entera; referencia obligatoria `circular:<obra social>:<hash corto del archivo>`; datos: obra_social, convenio (nombre en el HIS), archivo {nombre, hash, paginas}, vigencia_desde AAAA-MM-DD, porcentaje_declarado y filas_declaradas si la circular los dice), `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id (regla activa) y candidato tal cual lo devuelve buscar-demanda.mjs; el plugin verifica la regla con el candidato (si no la cumple, no abre) y fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`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"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos, internacion_id opcional; en faltantes, `responsable` {tipo profesional, id, nombre} opcional para que el plugin avise a quien resuelve), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal de la instituci\xF3n), matricula_nacional, matricula_provincial, codigo_provincia, especialidades), `rentabilidad_practica` (referencia obligatoria `rentabilidad:<prestacion_id>:<financiador_id>`; datos: prestacion_id, financiador_id, codigo, practica, financiador), `rentabilidad_internacion` (una internaci\xF3n egresada; referencia obligatoria `rentabilidad_internacion:<internacion_id>`; datos: internacion_id, financiador_id, financiador). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese (creado: false). En `rentabilidad_internacion` se devuelve el caso existente aunque est\xE9 cerrado: una internaci\xF3n se mide una sola vez. Pasar `idempotencyKey` para reintentos seguros.',
40064
40069
  parametersSchema: {
40065
40070
  type: "object",
40066
40071
  properties: {
@@ -43110,12 +43115,12 @@ var destinoRentabilidad = (caso, _datos, contexto) => {
43110
43115
  var rentabilidadPractica = {
43111
43116
  clave: "rentabilidad_practica",
43112
43117
  nombre: "Rentabilidad por pr\xE1ctica",
43113
- puesto: "Rentabilidad por pr\xE1ctica",
43118
+ puesto: "Costos y rentabilidad",
43114
43119
  descripcion: "Calcula, para cada pr\xE1ctica y cada financiador, lo que deja: lo que paga el convenio menos el honorario del profesional (con la regla del HIS) y los insumos. Marca las que dan p\xE9rdida o quedan debajo del umbral de la instituci\xF3n, y las que no se pueden calcular porque faltan datos en el HIS. Decide la direcci\xF3n: renegociar con el financiador o aceptarla. El puesto no escribe en el sistema.",
43115
43120
  inicial: "detectado",
43116
43121
  estados: ["detectado", "pendiente_aprobacion", "datos_incompletos", "completado", "en_renegociacion", "resuelta", "aceptada", "rentable", "descartado"],
43117
- finalesOk: ["resuelta"],
43118
- finalesNoOk: ["aceptada", "rentable", "descartado"],
43122
+ finalesOk: ["resuelta", "rentable", "aceptada"],
43123
+ finalesNoOk: ["descartado"],
43119
43124
  minutosAbsorbidos: 15,
43120
43125
  transiciones: [
43121
43126
  {
@@ -43165,7 +43170,123 @@ var rentabilidadPractica = {
43165
43170
  }
43166
43171
  ]
43167
43172
  };
43168
- var WORKFLOWS_SOLO_LECTURA = /* @__PURE__ */ new Set([cuentaCompleta.clave, facturacionPendiente.clave, credencialProfesional.clave, internacionAlDia.clave, honorarioEnCero.clave, rentabilidadPractica.clave]);
43173
+ function validarLecturaCostoInternacion(caso, datos, contexto) {
43174
+ const r = contexto.costoInternacion;
43175
+ const propia = texto4(caso.datos.internacion_id) ?? "";
43176
+ return [
43177
+ !r ? falla("lectura_valida", "falta lectura: la internaci\xF3n tal como la devuelve leer-internaciones.mjs") : "error" in r ? falla("lectura_valida", r.error) : ok("lectura_valida", `margen ${r.evaluacion.margen_pct ?? "sin calcular"} %`),
43178
+ r && !("error" in r) && r.internacionId !== propia ? falla("misma_internacion", `la lectura es de la internaci\xF3n ${r.internacionId} y el caso es de la ${propia}`) : ok("misma_internacion"),
43179
+ texto4(datos.lectura_id) ? ok("lectura_registrada") : falla("lectura_registrada", "hace falta lectura_id: la lectura de la base registrada con ops_registrar_accion")
43180
+ ];
43181
+ }
43182
+ var DIAS_REVISION_CUENTA = 14;
43183
+ var centavos = (v) => typeof v === "number" && Number.isFinite(v) ? Math.round(v * 100) : null;
43184
+ var fechaCortaISO = (v) => {
43185
+ const iso5 = fechaAISO(v);
43186
+ return iso5 ? `${iso5.slice(8, 10)}/${iso5.slice(5, 7)}/${iso5.slice(0, 4)}` : null;
43187
+ };
43188
+ function baseRevisionCuenta(d) {
43189
+ const e = d.evaluacion && typeof d.evaluacion === "object" ? d.evaluacion : {};
43190
+ return {
43191
+ facturado: typeof d.revision_facturado === "number" ? d.revision_facturado : typeof e.facturado === "number" ? e.facturado : null,
43192
+ costo: typeof d.revision_costo === "number" ? d.revision_costo : typeof e.costo === "number" ? e.costo : null
43193
+ };
43194
+ }
43195
+ function resultadoRevisionCuenta(datosCaso, e, hoy) {
43196
+ const base = baseRevisionCuenta(datosCaso);
43197
+ const cambio2 = (antes, ahora) => antes !== null && centavos(antes) !== centavos(ahora);
43198
+ if (cambio2(base.facturado, e.facturado) || cambio2(base.costo, e.costo)) return "con_cambios";
43199
+ const vence = fechaAISO(datosCaso.revision_vence_el);
43200
+ return !vence || !hoy || hoy >= vence ? "sin_cambios" : null;
43201
+ }
43202
+ function notaRevisionCuenta(d, estado, e, hoy) {
43203
+ const pedida = fechaCortaISO(d.revision_pedida_el);
43204
+ const desde = pedida ? `Se pidi\xF3 revisar la cuenta el ${pedida}` : "Se pidi\xF3 revisar la cuenta";
43205
+ if (estado === "en_revision") {
43206
+ const vence = fechaCortaISO(d.revision_vence_el);
43207
+ return `${desde}: si no cambia, vuelve a la direcci\xF3n${vence ? ` el ${vence}` : ""}.`;
43208
+ }
43209
+ if (estado !== "pendiente_aprobacion" || typeof d.revisada_sin_cambios !== "boolean") return "";
43210
+ if (d.revisada_sin_cambios) {
43211
+ const dias = hoy ? diasEntre3(d.revision_pedida_el, hoy) : null;
43212
+ return `${desde} y${dias !== null && dias > 0 ? ` en ${dias} ${dias === 1 ? "d\xEDa" : "d\xEDas"}` : ""} no cambi\xF3.`;
43213
+ }
43214
+ const base = baseRevisionCuenta(d);
43215
+ const cambios = [
43216
+ base.facturado !== null && centavos(base.facturado) !== centavos(e.facturado) ? `lo facturado pas\xF3 de ${pesos2(base.facturado)} a ${pesos2(e.facturado)}` : null,
43217
+ base.costo !== null && centavos(base.costo) !== centavos(e.costo) ? `el costo pas\xF3 de ${pesos2(base.costo)} a ${pesos2(e.costo)}` : null
43218
+ ].filter((x) => x !== null);
43219
+ const sigue = e.clase === "perdida" ? "sigue con p\xE9rdida" : "sigue debajo del margen m\xEDnimo";
43220
+ return `${desde}: ${cambios.length ? cambios.join(" y ") : "la cuenta cambi\xF3 desde entonces"}, y ${sigue}.`;
43221
+ }
43222
+ var destinoCostoInternacion = (caso, _datos, contexto) => {
43223
+ const r = contexto.costoInternacion;
43224
+ if (!r || "error" in r) return caso.estado;
43225
+ const clase2 = r.evaluacion.clase;
43226
+ if (clase2 === "datos_incompletos") return "datos_incompletos";
43227
+ if (clase2 === "rentable") return caso.estado === "detectado" ? "rentable" : "resuelta";
43228
+ if (caso.estado === "en_revision") return resultadoRevisionCuenta(caso.datos, r.evaluacion, contexto.hoy) ? "pendiente_aprobacion" : "en_revision";
43229
+ return "pendiente_aprobacion";
43230
+ };
43231
+ var rentabilidadInternacion = {
43232
+ clave: "rentabilidad_internacion",
43233
+ nombre: "Rentabilidad por internaci\xF3n",
43234
+ puesto: "Costos y rentabilidad",
43235
+ descripcion: "Calcula, para cada internaci\xF3n egresada, lo que dej\xF3: lo facturado contra lo que cost\xF3 (honorarios con la regla del HIS, insumos que farmacia le entreg\xF3 al paciente a costo de compra, d\xEDas cama y quir\xF3fano con los costos que fija la instituci\xF3n). Las que dan p\xE9rdida o quedan debajo del umbral van a la direcci\xF3n: revisar la cuenta (consumos sin facturar, renegociar el m\xF3dulo con el financiador) o aceptarla as\xED. El puesto no escribe en el sistema.",
43236
+ inicial: "detectado",
43237
+ estados: ["detectado", "pendiente_aprobacion", "datos_incompletos", "completado", "en_revision", "resuelta", "aceptada", "rentable", "descartado"],
43238
+ finalesOk: ["resuelta", "rentable", "aceptada"],
43239
+ finalesNoOk: ["descartado"],
43240
+ minutosAbsorbidos: 20,
43241
+ transiciones: [
43242
+ {
43243
+ via: "evaluar",
43244
+ de: ["detectado"],
43245
+ a: destinoCostoInternacion,
43246
+ actor: ["agente", "humano", "sistema"],
43247
+ nivelRequerido: "observa",
43248
+ descripcion: "Primera lectura de la internaci\xF3n. El plugin calcula el resultado y decide: pendiente de aprobaci\xF3n (p\xE9rdida o margen bajo), datos incompletos o rentable.",
43249
+ validar: validarLecturaCostoInternacion
43250
+ },
43251
+ { via: "aprobar", de: ["pendiente_aprobacion"], a: "en_revision", actor: ["humano"], descripcion: `La direcci\xF3n pide revisar la cuenta: consumos que no se facturaron o el m\xF3dulo con el financiador. El puesto vuelve a medir: si la cuenta cambia y sigue con p\xE9rdida o margen bajo, o si pasan ${DIAS_REVISION_CUENTA} d\xEDas sin cambios, vuelve a la direcci\xF3n.` },
43252
+ {
43253
+ via: "rechazar",
43254
+ de: ["pendiente_aprobacion", "en_revision"],
43255
+ a: "aceptada",
43256
+ actor: ["humano"],
43257
+ descripcion: "La direcci\xF3n acepta el resultado de la internaci\xF3n as\xED. Queda con el motivo.",
43258
+ validar: (_caso, datos) => [texto4(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "rechazar requiere motivo: por qu\xE9 se acepta el resultado as\xED")]
43259
+ },
43260
+ {
43261
+ via: "completar",
43262
+ de: ["datos_incompletos"],
43263
+ a: "completado",
43264
+ actor: ["humano"],
43265
+ cargaManual: true,
43266
+ descripcion: "Una persona de la instituci\xF3n carg\xF3 lo que faltaba (el costo de un lote en el HIS, o el costo del d\xEDa cama o del quir\xF3fano en Opera). El puesto lo comprueba volviendo a leer.",
43267
+ validar: (_caso, datos) => [texto4(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "completar requiere accion_id de la carga registrada")]
43268
+ },
43269
+ {
43270
+ via: "comprobar",
43271
+ de: ["pendiente_aprobacion", "datos_incompletos", "completado", "en_revision"],
43272
+ a: destinoCostoInternacion,
43273
+ actor: ["agente", "humano", "sistema"],
43274
+ nivelRequerido: "observa",
43275
+ descripcion: `Volver a leer la internaci\xF3n. El plugin decide: resuelta (ya deja margen, por ejemplo porque se factur\xF3 lo que faltaba), pendiente de aprobaci\xF3n (sigue con p\xE9rdida o margen bajo; desde la revisi\xF3n, solo si la cuenta cambi\xF3 o pasaron ${DIAS_REVISION_CUENTA} d\xEDas sin cambios), sigue en revisi\xF3n o datos incompletos.`,
43276
+ validar: validarLecturaCostoInternacion
43277
+ },
43278
+ {
43279
+ via: "descartar",
43280
+ de: ["detectado", "pendiente_aprobacion", "datos_incompletos", "completado", "en_revision"],
43281
+ a: "descartado",
43282
+ actor: ["agente", "humano"],
43283
+ nivelRequerido: "observa",
43284
+ descripcion: "La internaci\xF3n se anul\xF3 o no corresponde medirla.",
43285
+ validar: (_caso, datos) => [texto4(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "descartar requiere motivo")]
43286
+ }
43287
+ ]
43288
+ };
43289
+ var WORKFLOWS_SOLO_LECTURA = /* @__PURE__ */ new Set([cuentaCompleta.clave, facturacionPendiente.clave, credencialProfesional.clave, internacionAlDia.clave, honorarioEnCero.clave, rentabilidadPractica.clave, rentabilidadInternacion.clave]);
43169
43290
  var WORKFLOWS = {
43170
43291
  [actualizacionArancel.clave]: actualizacionArancel,
43171
43292
  [tareaOperativa.clave]: tareaOperativa,
@@ -43180,7 +43301,8 @@ var WORKFLOWS = {
43180
43301
  [honorarioEnCero.clave]: honorarioEnCero,
43181
43302
  [debitoProfesional.clave]: debitoProfesional,
43182
43303
  [liquidacionProfesional.clave]: liquidacionProfesional,
43183
- [rentabilidadPractica.clave]: rentabilidadPractica
43304
+ [rentabilidadPractica.clave]: rentabilidadPractica,
43305
+ [rentabilidadInternacion.clave]: rentabilidadInternacion
43184
43306
  };
43185
43307
  function getWorkflow(clave) {
43186
43308
  return WORKFLOWS[clave] ?? null;
@@ -43577,8 +43699,8 @@ async function requerirRegla(ctx, companyId, id3) {
43577
43699
  if (!r) throw new OpsError("regla_inexistente", `no existe la regla ${id3} en esta instituci\xF3n`);
43578
43700
  return r;
43579
43701
  }
43580
- function cambio(version2, actor, texto9) {
43581
- return { version: version2, fecha: (/* @__PURE__ */ new Date()).toISOString(), actorTipo: actor.tipo, actorId: actor.id, cambio: texto9 };
43702
+ function cambio(version2, actor, texto10) {
43703
+ return { version: version2, fecha: (/* @__PURE__ */ new Date()).toISOString(), actorTipo: actor.tipo, actorId: actor.id, cambio: texto10 };
43582
43704
  }
43583
43705
  function soloPersonas(actor, que) {
43584
43706
  if (actor.tipo !== "humano") throw new OpsError("actor_no_permitido", `${que} lo hace una persona de la instituci\xF3n desde el portal; el agente solo lee las reglas e informa la prueba`);
@@ -43767,7 +43889,7 @@ function evaluarRentabilidad(input2) {
43767
43889
  if (l.insumos.length === 0) avisos.push("sin_insumos");
43768
43890
  const debajo = [];
43769
43891
  const honorarios = [];
43770
- if (l.honorarios.length === 0) faltas.push("sin_regla_honorario");
43892
+ if (l.honorarios.length === 0) faltas.push("sin_profesional");
43771
43893
  for (const h of l.honorarios) {
43772
43894
  const nombre = h.profesional ?? h.profesional_id;
43773
43895
  if (!h.regla) {
@@ -43817,6 +43939,7 @@ var NOMBRE_FALTA = {
43817
43939
  sin_valor_convenio: "el convenio no tiene valor para esta pr\xE1ctica",
43818
43940
  insumo_sin_precio: "hay insumos sin precio",
43819
43941
  sin_regla_honorario: "hay profesionales sin regla de honorarios",
43942
+ sin_profesional: "se factur\xF3 sin un profesional (si es un medicamento o un descartable, no corresponde medirla como pr\xE1ctica)",
43820
43943
  regla_sin_valor: "hay reglas de honorarios sin valor"
43821
43944
  };
43822
43945
  var porcentaje = (n) => `${n.toLocaleString("es-AR", { maximumFractionDigits: 1 })} %`;
@@ -43921,6 +44044,204 @@ async function insumosPorCodigo(ctx, companyId) {
43921
44044
  return out;
43922
44045
  }
43923
44046
 
44047
+ // src/costos_internacion.ts
44048
+ var texto7 = (v) => typeof v === "string" && v.trim() ? v.trim() : typeof v === "number" && Number.isFinite(v) ? String(v) : null;
44049
+ var num7 = (v) => {
44050
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() ? Number(v) : NaN;
44051
+ return Number.isFinite(n) ? n : null;
44052
+ };
44053
+ var redondear5 = (n) => Math.round(n * 100) / 100;
44054
+ var fechaHora2 = (v) => {
44055
+ const s = texto7(v);
44056
+ return s && /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?)?/.test(s) ? s.replace(" ", "T").slice(0, 19) : null;
44057
+ };
44058
+ function leerLecturaCostoInternacion(v) {
44059
+ if (!v || typeof v !== "object" || Array.isArray(v)) return { error: "lectura: tiene que ser un objeto" };
44060
+ const r = v;
44061
+ const internacion_id = texto7(r.internacion_id);
44062
+ const financiador_id = texto7(r.financiador_id);
44063
+ const ingreso = fechaHora2(r.ingreso);
44064
+ if (!internacion_id) return { error: "lectura: falta internacion_id" };
44065
+ if (!financiador_id) return { error: "lectura: falta financiador_id" };
44066
+ if (!ingreso) return { error: "lectura.ingreso: fecha y hora AAAA-MM-DD HH:MM" };
44067
+ for (const k of ["eventos", "facturado", "consumos", "cirugias"]) if (!Array.isArray(r[k])) return { error: `lectura.${k}: tiene que ser una lista` };
44068
+ const eventos = [];
44069
+ for (const [i, x] of r.eventos.entries()) {
44070
+ const e = x ?? {};
44071
+ const fecha5 = fechaHora2(e.fecha);
44072
+ const tipo = texto7(e.tipo);
44073
+ if (!fecha5 || !tipo) return { error: `lectura.eventos[${i}]: faltan fecha o tipo` };
44074
+ eventos.push({ fecha: fecha5, tipo, servicio: texto7(e.servicio) });
44075
+ }
44076
+ const facturado = [];
44077
+ for (const [i, x] of r.facturado.entries()) {
44078
+ const f = x ?? {};
44079
+ const cantidad2 = num7(f.cantidad);
44080
+ const monto = num7(f.monto);
44081
+ if (cantidad2 === null || cantidad2 < 0 || monto === null || monto < 0) return { error: `lectura.facturado[${i}]: cantidad y monto tienen que ser n\xFAmeros \u2265 0` };
44082
+ if (f.regla !== null && f.regla !== void 0 && !leerRegla(f.regla)) return { error: `lectura.facturado[${i}].regla: tiene que ser {id, prof, inst} o null` };
44083
+ const noLiquidable = f.liquidable === false || f.liquidable === 0 || f.liquidable === "0" || f.liquidable === "false";
44084
+ facturado.push({ prestacion_id: texto7(f.prestacion_id) ?? String(i), nombre: texto7(f.nombre) ?? "", cantidad: cantidad2, monto: redondear5(monto), profesional_id: texto7(f.profesional_id), regla: leerRegla(f.regla), ...noLiquidable ? { liquidable: false } : {} });
44085
+ }
44086
+ const consumos = [];
44087
+ for (const [i, x] of r.consumos.entries()) {
44088
+ const c = x ?? {};
44089
+ const dispensado = num7(c.dispensado) ?? 0;
44090
+ const registrado = num7(c.registrado) ?? 0;
44091
+ const costo = num7(c.costo_unitario);
44092
+ if (dispensado < 0 && registrado <= 0) continue;
44093
+ if (costo !== null && costo < 0) return { error: `lectura.consumos[${i}].costo_unitario: no puede ser negativo` };
44094
+ consumos.push({ articulo_id: texto7(c.articulo_id) ?? String(i), articulo: texto7(c.articulo) ?? "", dispensado: Math.max(0, dispensado), registrado: Math.max(0, registrado), costo_unitario: costo === null || costo === 0 ? null : costo });
44095
+ }
44096
+ const cirugias = r.cirugias.map((x) => {
44097
+ const m = num7((x ?? {}).minutos);
44098
+ return { minutos: m !== null && m > 0 && m <= 24 * 60 ? Math.round(m) : null };
44099
+ });
44100
+ return { lectura: { internacion_id, financiador_id, financiador: texto7(r.financiador) ?? financiador_id, ingreso, egreso: fechaHora2(r.egreso), eventos, facturado, consumos, cirugias } };
44101
+ }
44102
+ var DIA_MS = 864e5;
44103
+ var ABREN = /* @__PURE__ */ new Set(["ingreso", "pase_cama", "reingreso"]);
44104
+ var CIERRAN = /* @__PURE__ */ new Set(["egreso"]);
44105
+ var ORDEN_EVENTO = { egreso: 0, ingreso: 1, reingreso: 1, pase_cama: 2 };
44106
+ var claveServicio = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/\s+/g, " ").trim();
44107
+ function repartir2(pesos4, unidades) {
44108
+ const suma = pesos4.reduce((a, b) => a + b, 0);
44109
+ const exactas = pesos4.map((p) => suma > 0 ? p / suma * unidades : 0);
44110
+ const enteras = exactas.map((x) => Math.floor(x));
44111
+ let resto = unidades - enteras.reduce((a, b) => a + b, 0);
44112
+ const orden = pesos4.map((_, i) => i).sort((a, b) => exactas[b] - enteras[b] - (exactas[a] - enteras[a]) || pesos4[b] - pesos4[a] || a - b);
44113
+ for (const i of orden) {
44114
+ if (resto <= 0) break;
44115
+ enteras[i] += 1;
44116
+ resto -= 1;
44117
+ }
44118
+ return enteras;
44119
+ }
44120
+ function repartirDecimas(pesos4, total) {
44121
+ const decimas = Math.round(total * 10);
44122
+ if (pesos4.length > decimas) return repartir2(pesos4, decimas);
44123
+ const suma = pesos4.reduce((a, b) => a + b, 0);
44124
+ const excedentes = pesos4.map((p) => Math.max(0, p / suma * decimas - 1));
44125
+ return repartir2(excedentes, decimas - pesos4.length).map((d) => d + 1);
44126
+ }
44127
+ function diasPorServicio(l, hoy) {
44128
+ const inicio = Date.parse(l.ingreso);
44129
+ const fin = Math.max(inicio, Date.parse(l.egreso ?? `${hoy}T23:59:59`));
44130
+ const recortar = (t6) => Math.min(fin, Math.max(inicio, t6));
44131
+ const evs = l.eventos.filter((e) => ABREN.has(e.tipo) || CIERRAN.has(e.tipo)).map((e) => ({ ...e, t: recortar(Date.parse(e.fecha)) })).sort((a, b) => a.t - b.t || (ORDEN_EVENTO[a.tipo] ?? 1) - (ORDEN_EVENTO[b.tipo] ?? 1) || a.fecha.localeCompare(b.fecha));
44132
+ const tramos = /* @__PURE__ */ new Map();
44133
+ const sumar = (servicio, ms) => {
44134
+ const k = claveServicio(servicio);
44135
+ const t6 = tramos.get(k);
44136
+ if (t6) t6.ms += Math.max(0, ms);
44137
+ else tramos.set(k, { nombre: servicio, ms: Math.max(0, ms) });
44138
+ };
44139
+ let abierto = null;
44140
+ let ultimoServicio = "Sin servicio";
44141
+ for (const e of evs) {
44142
+ if (abierto) sumar(abierto.servicio, e.t - abierto.desde);
44143
+ abierto = null;
44144
+ if (ABREN.has(e.tipo)) {
44145
+ ultimoServicio = e.servicio ?? ultimoServicio;
44146
+ abierto = { desde: e.t, servicio: ultimoServicio };
44147
+ }
44148
+ }
44149
+ if (abierto) sumar(abierto.servicio, fin - abierto.desde);
44150
+ if (tramos.size === 0) sumar(ultimoServicio, fin - inicio);
44151
+ const totalMs = [...tramos.values()].reduce((a, t6) => a + t6.ms, 0);
44152
+ const total = Math.max(1, Math.ceil(totalMs / DIA_MS - 0.01));
44153
+ let activos = [...tramos.values()].filter((t6) => t6.ms > 0);
44154
+ if (activos.length === 0) activos = [{ ...tramos.get(claveServicio(ultimoServicio)) ?? { nombre: ultimoServicio }, ms: 1 }];
44155
+ const decimas = repartirDecimas(activos.map((t6) => t6.ms), total);
44156
+ const msActivos = activos.reduce((a, t6) => a + t6.ms, 0);
44157
+ const out = /* @__PURE__ */ new Map();
44158
+ const exactos = /* @__PURE__ */ new Map();
44159
+ activos.forEach((t6, i) => {
44160
+ out.set(t6.nombre, decimas[i] / 10);
44161
+ exactos.set(t6.nombre, t6.ms / msActivos * total);
44162
+ });
44163
+ return { tramos: out, total, exactos };
44164
+ }
44165
+ function evaluarCostoInternacion(input2) {
44166
+ const { lectura: l, hoy } = input2;
44167
+ const umbral = input2.umbral ?? UMBRAL_MARGEN_POR_DEFECTO;
44168
+ const faltas = [];
44169
+ const avisos = [];
44170
+ const facturado = redondear5(l.facturado.reduce((a, f) => a + f.monto * f.cantidad, 0));
44171
+ if (facturado <= 0) faltas.push("sin_facturar");
44172
+ if (!l.egreso) avisos.push("en_curso");
44173
+ let honorarios = 0;
44174
+ let sinRegla = 0;
44175
+ for (const f of l.facturado) {
44176
+ if (!f.profesional_id || f.liquidable === false) continue;
44177
+ if (!f.regla || f.regla.prof === null && f.regla.inst === null) sinRegla += 1;
44178
+ honorarios += honorarioDe({ regla: f.regla, monto: f.monto, cantidad: f.cantidad });
44179
+ }
44180
+ honorarios = redondear5(honorarios);
44181
+ if (sinRegla > 0) avisos.push("prestaciones_sin_regla");
44182
+ let insumos = 0;
44183
+ const sinCosto = [];
44184
+ for (const c of l.consumos) {
44185
+ const cantidad2 = Math.max(c.dispensado, c.registrado);
44186
+ if (cantidad2 <= 0) continue;
44187
+ if (c.costo_unitario === null) {
44188
+ sinCosto.push(c.articulo || c.articulo_id);
44189
+ continue;
44190
+ }
44191
+ insumos += cantidad2 * c.costo_unitario;
44192
+ }
44193
+ insumos = redondear5(insumos);
44194
+ if (sinCosto.length) faltas.push("insumo_sin_costo");
44195
+ const { tramos, total, exactos } = diasPorServicio(l, hoy);
44196
+ const porServicio = input2.costoDiaPorServicio ?? {};
44197
+ const porClave = /* @__PURE__ */ new Map();
44198
+ for (const [s, c] of Object.entries(porServicio)) if (!porClave.has(claveServicio(s))) porClave.set(claveServicio(s), c);
44199
+ const general = input2.costoDiaCama ?? null;
44200
+ let cama = 0;
44201
+ let faltaCama = false;
44202
+ const dias_por_servicio = [...tramos.entries()].map(([servicio, dias]) => {
44203
+ const costo_dia = porServicio[servicio] ?? porClave.get(claveServicio(servicio)) ?? general;
44204
+ if (costo_dia === null || costo_dia === void 0) faltaCama = true;
44205
+ else cama += (exactos.get(servicio) ?? dias) * costo_dia;
44206
+ return { servicio, dias, costo_dia: costo_dia ?? null };
44207
+ });
44208
+ cama = redondear5(cama);
44209
+ if (faltaCama) faltas.push("sin_costo_cama");
44210
+ const minutos = l.cirugias.reduce((a, c) => a + (c.minutos ?? 0), 0);
44211
+ let quirofano = 0;
44212
+ if (l.cirugias.length > 0) {
44213
+ if (input2.costoHoraQuirofano === null || input2.costoHoraQuirofano === void 0) faltas.push("sin_costo_quirofano");
44214
+ else quirofano = redondear5(minutos / 60 * input2.costoHoraQuirofano);
44215
+ }
44216
+ if (l.cirugias.some((c) => c.minutos === null)) faltas.push("cirugia_sin_duracion");
44217
+ const costo = redondear5(honorarios + insumos + cama + quirofano);
44218
+ const calculable = faltas.length === 0 && facturado > 0;
44219
+ const margen = calculable ? redondear5(facturado - costo) : null;
44220
+ const margen_pct = margen !== null ? Math.round(margen / facturado * 1e3) / 10 : null;
44221
+ let clase2;
44222
+ if (margen === null || margen_pct === null) clase2 = "datos_incompletos";
44223
+ else if (margen < 0) clase2 = "perdida";
44224
+ else if (margen_pct < umbral) clase2 = "margen_bajo";
44225
+ else clase2 = "rentable";
44226
+ return { clase: clase2, facturado, honorarios, insumos, cama, quirofano, costo, margen, margen_pct, umbral, dias: total, dias_por_servicio, minutos_quirofano: minutos, faltas, avisos, insumos_sin_costo: sinCosto, prestaciones_sin_regla: sinRegla };
44227
+ }
44228
+ var NOMBRE_FALTA2 = {
44229
+ sin_facturar: "no tiene nada facturado",
44230
+ insumo_sin_costo: "hay insumos sin costo de compra en el HIS",
44231
+ sin_costo_cama: "falta el costo del d\xEDa cama",
44232
+ sin_costo_quirofano: "falta el costo de la hora de quir\xF3fano",
44233
+ cirugia_sin_duracion: "hay cirug\xEDas sin la duraci\xF3n en el parte quir\xFArgico"
44234
+ };
44235
+ var porcentaje2 = (n) => `${n.toLocaleString("es-AR", { maximumFractionDigits: 1 })} %`;
44236
+ var enumerar = (xs) => xs.length > 1 ? `${xs.slice(0, -1).join(", ")} y ${xs[xs.length - 1]}` : xs[0] ?? "";
44237
+ function oracionCostoInternacion(l, e) {
44238
+ const quien = `La internaci\xF3n ${l.internacion_id} con ${l.financiador} (${e.dias} ${e.dias === 1 ? "d\xEDa" : "d\xEDas"})`;
44239
+ if (e.clase === "datos_incompletos") return `${quien}: no se puede calcular el resultado porque ${enumerar(e.faltas.map((f) => NOMBRE_FALTA2[f] ?? f.replace(/_/g, " ")))}.`;
44240
+ if (e.clase === "perdida") return `${quien} dio p\xE9rdida: se facturaron ${pesos2(e.facturado)} y cost\xF3 ${pesos2(e.costo)} (${pesos2(Math.abs(e.margen))} de p\xE9rdida).`;
44241
+ if (e.clase === "margen_bajo") return `${quien} dej\xF3 un margen de ${porcentaje2(e.margen_pct)}, debajo del ${porcentaje2(e.umbral)} de la instituci\xF3n: se facturaron ${pesos2(e.facturado)} y cost\xF3 ${pesos2(e.costo)}.`;
44242
+ return `${quien} dej\xF3 un margen de ${porcentaje2(e.margen_pct)}.`;
44243
+ }
44244
+
43924
44245
  // src/ops.ts
43925
44246
  var OpsError = class extends Error {
43926
44247
  constructor(codigo, motivo, detalle) {
@@ -44062,6 +44383,11 @@ function describirOpciones(antes, despues) {
44062
44383
  if (antes.hora_corte !== despues.hora_corte && despues.hora_corte !== void 0) partes.push(`hora de corte de la evoluci\xF3n: ${String(despues.hora_corte)}`);
44063
44384
  if (antes.dias_sin_actividad !== despues.dias_sin_actividad && despues.dias_sin_actividad !== void 0) partes.push(`d\xEDas sin actividad para sospechar un egreso sin registrar: ${String(despues.dias_sin_actividad)}`);
44064
44385
  if (antes.tope_variacion !== despues.tope_variacion && despues.tope_variacion !== void 0) partes.push(`tope de variaci\xF3n de las circulares: ${String(despues.tope_variacion)} %`);
44386
+ if (antes.umbral_margen !== despues.umbral_margen && despues.umbral_margen !== void 0) partes.push(`margen m\xEDnimo: ${String(despues.umbral_margen)} %`);
44387
+ if (antes.meses_precio !== despues.meses_precio && despues.meses_precio !== void 0) partes.push(`meses para avisar un precio de insumo viejo: ${String(despues.meses_precio)}`);
44388
+ if (antes.costo_dia_cama !== despues.costo_dia_cama && despues.costo_dia_cama !== void 0) partes.push(`costo del d\xEDa cama: ${despues.costo_dia_cama === null ? "sin fijar" : `$ ${String(despues.costo_dia_cama)}`}`);
44389
+ if (antes.costo_hora_quirofano !== despues.costo_hora_quirofano && despues.costo_hora_quirofano !== void 0) partes.push(`costo de la hora de quir\xF3fano: ${despues.costo_hora_quirofano === null ? "sin fijar" : `$ ${String(despues.costo_hora_quirofano)}`}`);
44390
+ if (JSON.stringify(antes.costo_dia_servicio ?? {}) !== JSON.stringify(despues.costo_dia_servicio ?? {}) && despues.costo_dia_servicio !== void 0) partes.push(`costo del d\xEDa cama por servicio: ${Object.entries(despues.costo_dia_servicio).map(([s, m]) => `${s} $ ${m}`).join(", ") || "ninguno"}`);
44065
44391
  if (Boolean(antes.ofrecer_primero) !== Boolean(despues.ofrecer_primero)) partes.push(`consultar al paciente antes de asignar: ${despues.ofrecer_primero ? "s\xED" : "no"}`);
44066
44392
  const plazosAntes = JSON.stringify(leerPlazos(antes.plazos));
44067
44393
  const plazosDespues = leerPlazos(despues.plazos);
@@ -44074,6 +44400,22 @@ function describirOpciones(antes, despues) {
44074
44400
  function conInsumosDeOpera(l, insumos) {
44075
44401
  l.insumos = insumos.get(normalizarCodigo(l.codigo)) ?? [];
44076
44402
  }
44403
+ function montoNoNegativo(v) {
44404
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() ? Number(v) : NaN;
44405
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 100) / 100 : null;
44406
+ }
44407
+ function leerCostosPorServicio(v) {
44408
+ if (!v || typeof v !== "object" || Array.isArray(v)) return {};
44409
+ const out = {};
44410
+ for (const [k, x] of Object.entries(v)) {
44411
+ const m = montoNoNegativo(x);
44412
+ if (k.trim() && k.length <= 80 && m !== null) out[k.trim()] = m;
44413
+ }
44414
+ return out;
44415
+ }
44416
+ function opcionesCostos(op) {
44417
+ return { umbral: enteroEntre(op.umbral_margen, 0, 90) ?? UMBRAL_MARGEN_POR_DEFECTO, costoDiaCama: montoNoNegativo(op.costo_dia_cama), costoDiaPorServicio: leerCostosPorServicio(op.costo_dia_servicio), costoHoraQuirofano: montoNoNegativo(op.costo_hora_quirofano) };
44418
+ }
44077
44419
  function opcionesRentabilidad(op) {
44078
44420
  return { umbral: enteroEntre(op.umbral_margen, 0, 90) ?? UMBRAL_MARGEN_POR_DEFECTO, mesesPrecio: enteroEntre(op.meses_precio, 1, 36) ?? MESES_PRECIO_POR_DEFECTO };
44079
44421
  }
@@ -44143,6 +44485,23 @@ async function cambiarPolitica(ctx, input2) {
44143
44485
  if (tope === null) throw new OpsError("validacion", `tope_variacion: tiene que ser un n\xFAmero entre ${LIMITES_TOPE[0]} y ${LIMITES_TOPE[1]}`);
44144
44486
  opciones.tope_variacion = tope;
44145
44487
  }
44488
+ for (const clave of ["umbral_margen", "meses_precio"]) {
44489
+ if (opciones[clave] === void 0) continue;
44490
+ const [min, max] = clave === "umbral_margen" ? [0, 90] : [1, 36];
44491
+ const v = enteroEntre(opciones[clave], min, max);
44492
+ if (v === null) throw new OpsError("validacion", `${clave}: tiene que ser un n\xFAmero entero entre ${min} y ${max}`);
44493
+ opciones[clave] = v;
44494
+ }
44495
+ for (const clave of ["costo_dia_cama", "costo_hora_quirofano"]) {
44496
+ if (opciones[clave] === void 0 || opciones[clave] === null || opciones[clave] === "") {
44497
+ if (opciones[clave] === "") opciones[clave] = null;
44498
+ continue;
44499
+ }
44500
+ const v = montoNoNegativo(opciones[clave]);
44501
+ if (v === null) throw new OpsError("validacion", `${clave}: tiene que ser un monto mayor o igual a 0`);
44502
+ opciones[clave] = v;
44503
+ }
44504
+ if (opciones.costo_dia_servicio !== void 0) opciones.costo_dia_servicio = leerCostosPorServicio(opciones.costo_dia_servicio);
44146
44505
  const cambioOpciones = describirOpciones(opcionesAntes, opciones);
44147
44506
  const motivo = [input2.motivo ?? null, cambioOpciones].filter(Boolean).join(" \xB7 ") || null;
44148
44507
  const existe = await ctx.db.query(
@@ -44202,10 +44561,22 @@ async function leerCasoCompleto(ctx, companyId, casoId) {
44202
44561
  async function listarCasos(ctx, input2) {
44203
44562
  const params = [input2.companyId];
44204
44563
  const where = ["company_id = $1"];
44564
+ const despuesDe = str2(input2.despuesDe);
44565
+ if (despuesDe && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(despuesDe)) throw new OpsError("validacion", `despues_de tiene que ser el id de un caso (el \xFAltimo de la p\xE1gina anterior); vino \xAB${despuesDe}\xBB`);
44566
+ const porId = input2.orden === "id" || despuesDe !== null;
44567
+ if (despuesDe) {
44568
+ params.push(despuesDe);
44569
+ where.push(`id > $${params.length}`);
44570
+ }
44205
44571
  if (input2.workflow) {
44206
44572
  params.push(input2.workflow);
44207
44573
  where.push(`workflow = $${params.length}`);
44208
44574
  }
44575
+ const referencia = str2(input2.referencia);
44576
+ if (referencia) {
44577
+ params.push(referencia);
44578
+ where.push(`referencia = $${params.length}`);
44579
+ }
44209
44580
  if (input2.estado) {
44210
44581
  params.push(input2.estado);
44211
44582
  where.push(`estado = $${params.length}`);
@@ -44216,14 +44587,18 @@ async function listarCasos(ctx, input2) {
44216
44587
  });
44217
44588
  where.push(`estado IN (${marks.join(", ")})`);
44218
44589
  }
44219
- const limit = Math.min(Math.max(Number(input2.limit ?? 50) || 50, 1), 100);
44590
+ const limit = Math.min(Math.max(Number(input2.limit ?? 50) || 50, 1), 500);
44591
+ const offset = Math.min(Math.max(Math.floor(Number(input2.offset ?? 0)) || 0, 0), 1e6);
44220
44592
  params.push(limit);
44593
+ const pLimit = params.length;
44594
+ params.push(offset);
44221
44595
  const rows = await ctx.db.query(
44222
- `SELECT ${CASO_COLS} FROM ${t3(ctx, "caso")} WHERE ${where.join(" AND ")} ORDER BY updated_at DESC LIMIT $${params.length}`,
44596
+ `SELECT ${CASO_COLS} FROM ${t3(ctx, "caso")} WHERE ${where.join(" AND ")} ORDER BY ${porId ? "id" : "updated_at DESC, id"} LIMIT $${pLimit} OFFSET $${params.length}`,
44223
44597
  params
44224
44598
  );
44225
44599
  return rows.map(mapCaso);
44226
44600
  }
44601
+ var CLAVE_UNA_POR_REFERENCIA = "una-por-referencia:";
44227
44602
  async function abrirCaso(ctx, input2) {
44228
44603
  const wf = getWorkflow(input2.workflow);
44229
44604
  if (!wf) throw new OpsError("workflow_desconocido", `workflow desconocido: ${input2.workflow}. Disponibles: ${listarWorkflows().map((w) => w.clave).join(", ")}`);
@@ -44280,6 +44655,11 @@ async function abrirCaso(ctx, input2) {
44280
44655
  if (!pid || !/^\d{1,12}$/.test(pid) || !fid || !/^\d{1,12}$/.test(fid)) throw new OpsError("validacion", "rentabilidad_practica necesita prestacion_id y financiador_id (los ids en el HIS)");
44281
44656
  if (referencia !== `rentabilidad:${pid}:${fid}`) throw new OpsError("referencia_requerida", `en rentabilidad_practica la referencia es obligatoria: rentabilidad:${pid}:${fid}`);
44282
44657
  }
44658
+ if (wf.clave === "rentabilidad_internacion") {
44659
+ const id4 = str2(input2.datos?.internacion_id);
44660
+ if (!id4 || !/^\d{1,12}$/.test(id4)) throw new OpsError("validacion", "rentabilidad_internacion necesita internacion_id (el id de la internaci\xF3n en el HIS)");
44661
+ if (referencia !== `rentabilidad_internacion:${id4}`) throw new OpsError("referencia_requerida", `en rentabilidad_internacion la referencia es obligatoria: rentabilidad_internacion:${id4}`);
44662
+ }
44283
44663
  if (wf.clave === "circular_aranceles") {
44284
44664
  const d = input2.datos ?? {};
44285
44665
  if (!referencia || !/^circular:\S+$/.test(referencia)) throw new OpsError("referencia_requerida", "en circular_aranceles la referencia es obligatoria: circular:<obra social>:<hash corto del archivo>");
@@ -44322,18 +44702,26 @@ async function abrirCaso(ctx, input2) {
44322
44702
  extra.detectado_el = hoy;
44323
44703
  }
44324
44704
  if (referencia) {
44325
- const abiertos = await ctx.db.query(
44326
- `SELECT ${CASO_COLS} FROM ${t3(ctx, "caso")} WHERE company_id = $1 AND workflow = $2 AND referencia = $3 AND cerrado_at IS NULL ORDER BY created_at DESC LIMIT 1`,
44327
- [input2.companyId, input2.workflow, referencia]
44705
+ const soloAbiertos = wf.clave !== "rentabilidad_internacion";
44706
+ const previos = await ctx.db.query(
44707
+ `SELECT ${CASO_COLS} FROM ${t3(ctx, "caso")} WHERE company_id = $1 AND workflow = $2 AND referencia = $3${soloAbiertos ? " AND cerrado_at IS NULL" : ""} ORDER BY created_at DESC LIMIT 1`,
44708
+ [input2.companyId, wf.clave, referencia]
44328
44709
  );
44329
- if (abiertos[0]) return { caso: mapCaso(abiertos[0]), creado: false };
44710
+ if (previos[0]) return { caso: mapCaso(previos[0]), creado: false };
44330
44711
  }
44331
44712
  const id3 = randomUUID3();
44332
44713
  const datos = { ...input2.datos ?? {}, ...extra };
44333
- await ctx.db.execute(
44334
- `INSERT INTO ${t3(ctx, "caso")} (id, company_id, workflow, estado, referencia, titulo, datos, abierto_por_tipo, abierto_por_id, agente_id, issue_id, idempotency_key) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, $10, $11, $12)`,
44335
- [id3, input2.companyId, wf.clave, wf.inicial, referencia, titulo, JSON.stringify(datos), input2.actor.tipo, input2.actor.id, input2.agenteId ?? null, input2.issueId ?? null, idem]
44336
- );
44714
+ const clave = wf.clave === "rentabilidad_internacion" && referencia ? `${CLAVE_UNA_POR_REFERENCIA}${referencia}` : idem;
44715
+ try {
44716
+ await ctx.db.execute(
44717
+ `INSERT INTO ${t3(ctx, "caso")} (id, company_id, workflow, estado, referencia, titulo, datos, abierto_por_tipo, abierto_por_id, agente_id, issue_id, idempotency_key) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, $10, $11, $12)`,
44718
+ [id3, input2.companyId, wf.clave, wf.inicial, referencia, titulo, JSON.stringify(datos), input2.actor.tipo, input2.actor.id, input2.agenteId ?? null, input2.issueId ?? null, clave]
44719
+ );
44720
+ } catch (err) {
44721
+ const ganador = clave ? await ctx.db.query(`SELECT ${CASO_COLS} FROM ${t3(ctx, "caso")} WHERE company_id = $1 AND idempotency_key = $2 LIMIT 1`, [input2.companyId, clave]) : [];
44722
+ if (ganador[0]) return { caso: mapCaso(ganador[0]), creado: false };
44723
+ throw err;
44724
+ }
44337
44725
  const now = (/* @__PURE__ */ new Date()).toISOString();
44338
44726
  return {
44339
44727
  creado: true,
@@ -44402,6 +44790,14 @@ async function contextoDe(ctx, companyId, caso, via, datos) {
44402
44790
  }
44403
44791
  return contexto;
44404
44792
  }
44793
+ if (caso.workflow === "rentabilidad_internacion") {
44794
+ if (via === "evaluar" || via === "comprobar") {
44795
+ const op = await obtenerOpciones(ctx, companyId, caso.workflow);
44796
+ const l = leerLecturaCostoInternacion(datos.lectura);
44797
+ contexto.costoInternacion = "error" in l ? { error: l.error } : { internacionId: l.lectura.internacion_id, evaluacion: evaluarCostoInternacion({ lectura: l.lectura, hoy, ...opcionesCostos(op) }) };
44798
+ }
44799
+ return contexto;
44800
+ }
44405
44801
  if (caso.workflow === "cobranza_financiador") {
44406
44802
  const op = await obtenerOpciones(ctx, companyId, caso.workflow);
44407
44803
  contexto.plazosPago = leerPlazos(op.plazos_pago);
@@ -44640,6 +45036,46 @@ async function transicionar(ctx, input2) {
44640
45036
  }
44641
45037
  delete datos.lectura;
44642
45038
  }
45039
+ if (caso.workflow === "rentabilidad_internacion") {
45040
+ for (const k of ["evaluacion", "oracion", "financiador", "ingreso", "egreso", "facturado", "consumos", "cirugias", "comprobado_el", "revisada_sin_cambios", "revision_pedida_el", "revision_vence_el", "revision_facturado", "revision_costo"]) delete datos[k];
45041
+ const hoy = contexto.hoy ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
45042
+ const r = contexto.costoInternacion;
45043
+ const l = leerLecturaCostoInternacion(datos.lectura);
45044
+ const nueva = r && !("error" in r) && !("error" in l) ? { lectura: l.lectura, evaluacion: r.evaluacion } : null;
45045
+ if (resultado.via === "aprobar") {
45046
+ const vista = caso.datos.evaluacion && typeof caso.datos.evaluacion === "object" ? caso.datos.evaluacion : {};
45047
+ Object.assign(datos, {
45048
+ revision_pedida_el: hoy,
45049
+ revision_vence_el: sumarDias4(hoy, DIAS_REVISION_CUENTA),
45050
+ revision_facturado: typeof vista.facturado === "number" ? vista.facturado : null,
45051
+ revision_costo: typeof vista.costo === "number" ? vista.costo : null,
45052
+ revisada_sin_cambios: null
45053
+ });
45054
+ } else if (nueva && resultado.a === "pendiente_aprobacion") {
45055
+ if (caso.estado === "en_revision") datos.revisada_sin_cambios = resultadoRevisionCuenta(caso.datos, nueva.evaluacion, hoy) === "sin_cambios";
45056
+ else if (caso.datos.revisada_sin_cambios === true && resultadoRevisionCuenta(caso.datos, nueva.evaluacion, hoy) === "con_cambios") datos.revisada_sin_cambios = false;
45057
+ }
45058
+ if (nueva) {
45059
+ Object.assign(datos, {
45060
+ financiador: nueva.lectura.financiador,
45061
+ ingreso: nueva.lectura.ingreso,
45062
+ egreso: nueva.lectura.egreso,
45063
+ facturado: nueva.lectura.facturado,
45064
+ consumos: nueva.lectura.consumos,
45065
+ cirugias: nueva.lectura.cirugias,
45066
+ evaluacion: nueva.evaluacion,
45067
+ comprobado_el: contexto.hoy ?? null
45068
+ });
45069
+ }
45070
+ const despues = { ...caso.datos, ...datos };
45071
+ const e = nueva?.evaluacion ?? (despues.evaluacion && typeof despues.evaluacion === "object" ? despues.evaluacion : null);
45072
+ if (e && typeof e.facturado === "number" && typeof e.costo === "number") {
45073
+ const quien = { internacion_id: str2(despues.internacion_id) ?? "", financiador: str2(despues.financiador) ?? "" };
45074
+ const nota = notaRevisionCuenta(despues, resultado.a, e, hoy);
45075
+ datos.oracion = `${oracionCostoInternacion(nueva?.lectura ?? quien, e)}${nota ? ` ${nota}` : ""}`;
45076
+ }
45077
+ delete datos.lectura;
45078
+ }
44643
45079
  if (caso.workflow === "cobranza_financiador") {
44644
45080
  for (const k of ["items", "resumen", "reclamo", "reclamos", "ultimo_reclamo", "proximo_reclamo", "nc_al_reclamar", "cobrado_el"]) delete datos[k];
44645
45081
  const hoy = contexto.hoy ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -45403,7 +45839,7 @@ import { randomUUID as randomUUID4 } from "node:crypto";
45403
45839
  function t4(ctx, table) {
45404
45840
  return `${ctx.db.namespace}.${table}`;
45405
45841
  }
45406
- function texto7(v) {
45842
+ function texto8(v) {
45407
45843
  return typeof v === "string" && v.trim().length > 0 ? v.trim() : typeof v === "number" ? String(v) : null;
45408
45844
  }
45409
45845
  function iso3(v) {
@@ -45441,7 +45877,8 @@ var ESPERAN_PERSONA = {
45441
45877
  credencial_profesional: ["observada"],
45442
45878
  internacion_al_dia: ["con_pendientes"],
45443
45879
  honorario_en_cero: ["para_cargar"],
45444
- debito_profesional: ["aprobado", "con_diferencias"]
45880
+ debito_profesional: ["aprobado", "con_diferencias"],
45881
+ rentabilidad_internacion: ["en_revision"]
45445
45882
  };
45446
45883
  var CANALES_AVISO = ["mail", "whatsapp"];
45447
45884
  var HORARIO_AVISOS = { desde: "08:00", hasta: "18:00" };
@@ -45462,7 +45899,7 @@ function pendientesDe(caso, hoy) {
45462
45899
  const estados = ESPERAN_PERSONA[caso.workflow];
45463
45900
  if (!estados || !estados.includes(caso.estado)) return [];
45464
45901
  const d = caso.datos;
45465
- const bono = `Bono ${texto7(d.bono_numero) ?? texto7(d.bono_id) ?? "sin n\xFAmero"}${texto7(d.obra_social) ? ` \xB7 ${texto7(d.obra_social)}` : ""}`;
45902
+ const bono = `Bono ${texto8(d.bono_numero) ?? texto8(d.bono_id) ?? "sin n\xFAmero"}${texto8(d.obra_social) ? ` \xB7 ${texto8(d.obra_social)}` : ""}`;
45466
45903
  const base = { casoId: caso.id, workflow: caso.workflow, titulo: caso.titulo };
45467
45904
  if (caso.workflow === "cuenta_completa") {
45468
45905
  const out = [];
@@ -45470,7 +45907,7 @@ function pendientesDe(caso, hoy) {
45470
45907
  const nombreRegla = REGLAS_CUENTA[f.regla]?.nombre ?? f.regla;
45471
45908
  if (f.regla === "epicrisis_faltante") {
45472
45909
  const p = f.responsable?.tipo === "profesional" && f.responsable.id ? f.responsable : null;
45473
- const internacion = texto7(d.internacion_id) ? `Internaci\xF3n ${texto7(d.internacion_id)}` : bono;
45910
+ const internacion = texto8(d.internacion_id) ? `Internaci\xF3n ${texto8(d.internacion_id)}` : bono;
45474
45911
  out.push({
45475
45912
  ...base,
45476
45913
  que: `${internacion}: falta la epicrisis`,
@@ -45484,7 +45921,7 @@ function pendientesDe(caso, hoy) {
45484
45921
  }
45485
45922
  return out;
45486
45923
  }
45487
- const profesionalH = texto7(d.profesional) ?? (d.profesional && typeof d.profesional === "object" ? texto7(d.profesional.nombre) : null);
45924
+ const profesionalH = texto8(d.profesional) ?? (d.profesional && typeof d.profesional === "object" ? texto8(d.profesional.nombre) : null);
45488
45925
  if (caso.workflow === "honorario_en_cero") {
45489
45926
  const r = d.resumen && typeof d.resumen === "object" ? d.resumen : {};
45490
45927
  const n = typeof r.prestaciones === "number" ? r.prestaciones : 0;
@@ -45493,22 +45930,22 @@ function pendientesDe(caso, hoy) {
45493
45930
  if (caso.workflow === "debito_profesional") {
45494
45931
  const r = d.resumen && typeof d.resumen === "object" ? d.resumen : {};
45495
45932
  const total = typeof r.total === "number" ? r.total.toLocaleString("es-AR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : "";
45496
- return [{ ...base, que: `${profesionalH ?? "Un profesional"}: cargar un d\xE9bito de $ ${total} aprobado por la direcci\xF3n (nota de cr\xE9dito de ${texto7(d.obra_social) ?? "la obra social"})`, responsable: rol("honorarios") }];
45933
+ return [{ ...base, que: `${profesionalH ?? "Un profesional"}: cargar un d\xE9bito de $ ${total} aprobado por la direcci\xF3n (nota de cr\xE9dito de ${texto8(d.obra_social) ?? "la obra social"})`, responsable: rol("honorarios") }];
45497
45934
  }
45498
45935
  if (caso.workflow === "internacion_al_dia") {
45499
- const internacion = `Internaci\xF3n ${texto7(d.internacion_id) ?? "sin n\xFAmero"}${texto7(d.servicio) ? ` (${texto7(d.servicio)})` : ""}`;
45936
+ const internacion = `Internaci\xF3n ${texto8(d.internacion_id) ?? "sin n\xFAmero"}${texto8(d.servicio) ? ` (${texto8(d.servicio)})` : ""}`;
45500
45937
  const resp = d.responsable && typeof d.responsable === "object" ? d.responsable : null;
45501
- const medicoId = resp ? texto7(resp.id) : null;
45938
+ const medicoId = resp ? texto8(resp.id) : null;
45502
45939
  const out = [];
45503
45940
  for (const p of Array.isArray(d.pendientes) ? d.pendientes : []) {
45504
- const regla = texto7(p.regla);
45941
+ const regla = texto8(p.regla);
45505
45942
  if (regla === "evolucion_de_hoy") {
45506
- const dia5 = texto7(p.dia);
45943
+ const dia5 = texto8(p.dia);
45507
45944
  if (!dia5 || hoy && dia5 !== hoy) continue;
45508
45945
  out.push({
45509
45946
  ...base,
45510
45947
  que: `${internacion}: todav\xEDa no tiene evoluci\xF3n hoy`,
45511
- responsable: medicoId ? { clave: `profesional:${medicoId}`, nombre: texto7(resp?.nombre) ?? `Profesional ${medicoId}`, reserva: "rol:internacion" } : rol("internacion"),
45948
+ responsable: medicoId ? { clave: `profesional:${medicoId}`, nombre: texto8(resp?.nombre) ?? `Profesional ${medicoId}`, reserva: "rol:internacion" } : rol("internacion"),
45512
45949
  marca: `evolucion:${dia5}`
45513
45950
  });
45514
45951
  } else if (regla === "alta_sin_registrar") {
@@ -45520,13 +45957,17 @@ function pendientesDe(caso, hoy) {
45520
45957
  return out;
45521
45958
  }
45522
45959
  if (caso.workflow === "facturacion_pendiente") {
45523
- const vence = texto7(d.vence_el);
45960
+ const vence = texto8(d.vence_el);
45524
45961
  return [{ ...base, que: `${bono}: sin presentar${vence ? `, vence el ${vence.slice(8, 10)}/${vence.slice(5, 7)}` : ""}`, responsable: rol("facturacion") }];
45525
45962
  }
45963
+ if (caso.workflow === "rentabilidad_internacion") {
45964
+ const vence = texto8(d.revision_vence_el);
45965
+ return [{ ...base, que: `${caso.titulo}: revisar la cuenta (consumos sin facturar o m\xF3dulo con el financiador)${vence ? `, antes del ${vence.slice(8, 10)}/${vence.slice(5, 7)}` : ""}`, responsable: rol("facturacion") }];
45966
+ }
45526
45967
  if (caso.workflow === "credencial_profesional") {
45527
- const id3 = texto7(d.profesional_id);
45528
- const nombre = texto7(d.profesional) ?? (id3 ? `Profesional ${id3}` : "Profesional");
45529
- const obs = texto7(d.observacion);
45968
+ const id3 = texto8(d.profesional_id);
45969
+ const nombre = texto8(d.profesional) ?? (id3 ? `Profesional ${id3}` : "Profesional");
45970
+ const obs = texto8(d.observacion);
45530
45971
  const que = `${nombre}: ${(obs && OBSERVACIONES_MATRICULA[obs]) ?? "la matr\xEDcula tiene una observaci\xF3n del registro nacional"}`;
45531
45972
  return [{ ...base, que, responsable: id3 ? { clave: `profesional:${id3}`, nombre, reserva: "rol:personal" } : rol("personal") }];
45532
45973
  }
@@ -45536,7 +45977,7 @@ var CLAVE_RE = /^(profesional:[A-Za-z0-9_-]{1,40}|financiador:\d{1,12}|rol:(admi
45536
45977
  var MAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
45537
45978
  var COLS_CONTACTO = "id, clave, nombre, mail, whatsapp, activo, updated_at";
45538
45979
  function mapContacto(r) {
45539
- return { id: String(r.id), clave: String(r.clave), nombre: String(r.nombre), mail: texto7(r.mail), whatsapp: texto7(r.whatsapp), activo: r.activo !== false, actualizadoEn: iso3(r.updated_at) };
45980
+ return { id: String(r.id), clave: String(r.clave), nombre: String(r.nombre), mail: texto8(r.mail), whatsapp: texto8(r.whatsapp), activo: r.activo !== false, actualizadoEn: iso3(r.updated_at) };
45540
45981
  }
45541
45982
  async function listarContactos(ctx, companyId) {
45542
45983
  const rows = await ctx.db.query(`SELECT ${COLS_CONTACTO} FROM ${t4(ctx, "contacto")} WHERE company_id = $1 ORDER BY nombre ASC`, [companyId]);
@@ -45550,15 +45991,15 @@ function leerWhatsapp(v) {
45550
45991
  }
45551
45992
  async function guardarContacto(ctx, input2) {
45552
45993
  if (input2.actor.tipo !== "humano") throw new OpsError("actor_no_permitido", "los contactos los carga una persona de la instituci\xF3n desde el portal");
45553
- const clave = texto7(input2.clave) ?? "";
45994
+ const clave = texto8(input2.clave) ?? "";
45554
45995
  if (!CLAVE_RE.test(clave)) throw new OpsError("validacion", "clave: tiene que ser profesional:<id>, financiador:<id> o rol:<admision|aranceles|facturacion|internacion|personal>");
45555
- const mail = texto7(input2.mail)?.toLowerCase() ?? null;
45996
+ const mail = texto8(input2.mail)?.toLowerCase() ?? null;
45556
45997
  if (mail && !MAIL_RE.test(mail)) throw new OpsError("validacion", "mail: no tiene formato de correo");
45557
45998
  const whatsapp = leerWhatsapp(input2.whatsapp);
45558
45999
  const activo = input2.activo !== false;
45559
46000
  if (activo && !mail && !whatsapp) throw new OpsError("validacion", "hace falta un mail o un WhatsApp para poder avisar");
45560
46001
  const existente = (await ctx.db.query(`SELECT ${COLS_CONTACTO} FROM ${t4(ctx, "contacto")} WHERE company_id = $1 AND clave = $2 LIMIT 1`, [input2.companyId, clave]))[0];
45561
- const nombre = texto7(input2.nombre)?.slice(0, 120) ?? (existente ? String(existente.nombre) : clave.startsWith("rol:") ? ROLES[clave.slice(4)] ?? clave : null);
46002
+ const nombre = texto8(input2.nombre)?.slice(0, 120) ?? (existente ? String(existente.nombre) : clave.startsWith("rol:") ? ROLES[clave.slice(4)] ?? clave : null);
45562
46003
  if (!nombre) throw new OpsError("validacion", "nombre: es obligatorio para un profesional nuevo");
45563
46004
  if (existente) {
45564
46005
  await ctx.db.execute(
@@ -45588,8 +46029,8 @@ function mapAviso(r) {
45588
46029
  texto: String(r.texto),
45589
46030
  estado: ["pendiente", "enviado", "fallido", "cancelado"].includes(String(r.estado)) ? String(r.estado) : "pendiente",
45590
46031
  intentosEnvio: Number(r.intentos_envio) || 0,
45591
- error: texto7(r.error),
45592
- proveedorId: texto7(r.proveedor_id),
46032
+ error: texto8(r.error),
46033
+ proveedorId: texto8(r.proveedor_id),
45593
46034
  enviadoEn: iso3(r.enviado_en),
45594
46035
  creadoEn: iso3(r.created_at)
45595
46036
  };
@@ -45633,7 +46074,7 @@ function redactarAviso(nombre, institucion, pendientes) {
45633
46074
  const repetidos = pendientes.filter((p) => p.intento > 1).length;
45634
46075
  const asunto = `${n === 1 ? "1 pendiente" : `${n} pendientes`} para resolver en el HIS \xB7 ${institucion}`;
45635
46076
  const lineas = pendientes.map((p) => `- ${p.que}${p.intento > 1 ? " (segundo aviso)" : ""}`);
45636
- const texto9 = [
46077
+ const texto10 = [
45637
46078
  `Hola ${nombre}:`,
45638
46079
  "",
45639
46080
  `Desde ${institucion}, Autopilot Opera te avisa que ${n === 1 ? "ten\xE9s 1 pendiente" : `ten\xE9s ${n} pendientes`} para resolver en el HIS:`,
@@ -45644,7 +46085,7 @@ function redactarAviso(nombre, institucion, pendientes) {
45644
46085
  "",
45645
46086
  "Autopilot Opera \xB7 avisos autom\xE1ticos de la administraci\xF3n"
45646
46087
  ].join("\n");
45647
- return { asunto, texto: texto9 };
46088
+ return { asunto, texto: texto10 };
45648
46089
  }
45649
46090
  async function generarAvisos(ctx, input2) {
45650
46091
  const { hoy, hora } = ahoraEnHospital(input2.ahora);
@@ -45721,7 +46162,7 @@ async function informarResultadoAviso(ctx, input2) {
45721
46162
  if (input2.resultado === "enviado") {
45722
46163
  await ctx.db.execute(
45723
46164
  `UPDATE ${t4(ctx, "aviso")} SET estado = $3, proveedor_id = $4, enviado_en = now(), intentos_envio = intentos_envio + 1, error = NULL, updated_at = now() WHERE company_id = $1 AND id = $2`,
45724
- [input2.companyId, aviso.id, "enviado", texto7(input2.proveedorId)]
46165
+ [input2.companyId, aviso.id, "enviado", texto8(input2.proveedorId)]
45725
46166
  );
45726
46167
  for (const c of aviso.casos) {
45727
46168
  await registrarAccion(ctx, {
@@ -45735,11 +46176,11 @@ async function informarResultadoAviso(ctx, input2) {
45735
46176
  actor
45736
46177
  });
45737
46178
  }
45738
- return { ...aviso, estado: "enviado", proveedorId: texto7(input2.proveedorId), enviadoEn: (/* @__PURE__ */ new Date()).toISOString(), intentosEnvio: aviso.intentosEnvio + 1, error: null };
46179
+ return { ...aviso, estado: "enviado", proveedorId: texto8(input2.proveedorId), enviadoEn: (/* @__PURE__ */ new Date()).toISOString(), intentosEnvio: aviso.intentosEnvio + 1, error: null };
45739
46180
  }
45740
46181
  const intentos = aviso.intentosEnvio + 1;
45741
46182
  const estado = intentos >= MAX_INTENTOS_ENVIO ? "fallido" : "pendiente";
45742
- const error62 = texto7(input2.error)?.slice(0, 300) ?? "no se pudo enviar";
46183
+ const error62 = texto8(input2.error)?.slice(0, 300) ?? "no se pudo enviar";
45743
46184
  await ctx.db.execute(
45744
46185
  `UPDATE ${t4(ctx, "aviso")} SET estado = $3, error = $4, intentos_envio = $5, updated_at = now() WHERE company_id = $1 AND id = $2`,
45745
46186
  [input2.companyId, aviso.id, estado, error62, intentos]
@@ -45816,7 +46257,7 @@ function arr(v) {
45816
46257
  }
45817
46258
  return [];
45818
46259
  }
45819
- function texto8(v, max, campo, problemas, obligatorio = false) {
46260
+ function texto9(v, max, campo, problemas, obligatorio = false) {
45820
46261
  if (v === void 0 || v === null || v === "") {
45821
46262
  if (obligatorio) problemas.push(`${campo} es obligatorio`);
45822
46263
  return void 0;
@@ -45927,24 +46368,24 @@ function validarWidgets(entrada) {
45927
46368
  const a = ancho(o.ancho);
45928
46369
  switch (tipo) {
45929
46370
  case "indicador": {
45930
- const etiqueta = texto8(o.etiqueta, 80, `${ref}.etiqueta`, problemas, true) ?? "";
45931
- const valor = typeof o.valor === "number" && Number.isFinite(o.valor) ? o.valor : texto8(o.valor, 40, `${ref}.valor`, problemas, true) ?? "";
46371
+ const etiqueta = texto9(o.etiqueta, 80, `${ref}.etiqueta`, problemas, true) ?? "";
46372
+ const valor = typeof o.valor === "number" && Number.isFinite(o.valor) ? o.valor : texto9(o.valor, 40, `${ref}.valor`, problemas, true) ?? "";
45932
46373
  const tono = typeof o.tono === "string" && TONOS.includes(o.tono) ? o.tono : void 0;
45933
46374
  if (o.tono !== void 0 && !tono) problemas.push(`${ref}.tono debe ser uno de ${TONOS.join(", ")}`);
45934
- const unidad = texto8(o.unidad, 16, `${ref}.unidad`, problemas);
46375
+ const unidad = texto9(o.unidad, 16, `${ref}.unidad`, problemas);
45935
46376
  if (unidad === "%" && !ES_VARIACION.test(etiqueta)) controlarPorcentaje(valor, `${ref}.valor`, problemas);
45936
46377
  const { calculo, origen } = leerCalculo(o.calculo, ref, problemas);
45937
- salida.push({ tipo, etiqueta, valor, unidad, contexto: texto8(o.contexto, 160, `${ref}.contexto`, problemas), tono, ancho: a, ...calculo ? { calculo } : {}, origen });
46378
+ salida.push({ tipo, etiqueta, valor, unidad, contexto: texto9(o.contexto, 160, `${ref}.contexto`, problemas), tono, ancho: a, ...calculo ? { calculo } : {}, origen });
45938
46379
  break;
45939
46380
  }
45940
46381
  case "barras": {
45941
- const titulo = texto8(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
46382
+ const titulo = texto9(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
45942
46383
  const series = arr(o.series);
45943
46384
  if (series.length === 0 || series.length > 40) problemas.push(`${ref}.series debe tener entre 1 y 40 barras`);
45944
- const unidad = texto8(o.unidad, 16, `${ref}.unidad`, problemas);
46385
+ const unidad = texto9(o.unidad, 16, `${ref}.unidad`, problemas);
45945
46386
  const barras = series.slice(0, 40).map((s, j) => {
45946
46387
  const so = s && typeof s === "object" ? s : {};
45947
- return { etiqueta: texto8(so.etiqueta, 60, `${ref}.series[${j}].etiqueta`, problemas, true) ?? "", valor: numero2(so.valor, `${ref}.series[${j}].valor`, problemas) };
46388
+ return { etiqueta: texto9(so.etiqueta, 60, `${ref}.series[${j}].etiqueta`, problemas, true) ?? "", valor: numero2(so.valor, `${ref}.series[${j}].valor`, problemas) };
45948
46389
  });
45949
46390
  if (unidad === "%" && !ES_VARIACION.test(titulo)) barras.forEach((b, j) => controlarPorcentaje(b.valor, `${ref}.series[${j}].valor`, problemas));
45950
46391
  const { calculo, origen } = leerCalculo(o.calculo, ref, problemas);
@@ -45952,13 +46393,13 @@ function validarWidgets(entrada) {
45952
46393
  break;
45953
46394
  }
45954
46395
  case "linea": {
45955
- const titulo = texto8(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
46396
+ const titulo = texto9(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
45956
46397
  const puntos = arr(o.puntos);
45957
46398
  if (puntos.length === 0 || puntos.length > 400) problemas.push(`${ref}.puntos debe tener entre 1 y 400 puntos`);
45958
- const unidad = texto8(o.unidad, 16, `${ref}.unidad`, problemas);
46399
+ const unidad = texto9(o.unidad, 16, `${ref}.unidad`, problemas);
45959
46400
  const serie2 = puntos.slice(0, 400).map((p, j) => {
45960
46401
  const po = p && typeof p === "object" ? p : {};
45961
- return { x: texto8(po.x, 40, `${ref}.puntos[${j}].x`, problemas, true) ?? "", y: numero2(po.y, `${ref}.puntos[${j}].y`, problemas) };
46402
+ return { x: texto9(po.x, 40, `${ref}.puntos[${j}].x`, problemas, true) ?? "", y: numero2(po.y, `${ref}.puntos[${j}].y`, problemas) };
45962
46403
  });
45963
46404
  if (unidad === "%" && !ES_VARIACION.test(titulo)) serie2.forEach((p, j) => controlarPorcentaje(p.y, `${ref}.puntos[${j}].y`, problemas));
45964
46405
  const { calculo, origen } = leerCalculo(o.calculo, ref, problemas);
@@ -45966,12 +46407,12 @@ function validarWidgets(entrada) {
45966
46407
  break;
45967
46408
  }
45968
46409
  case "tabla": {
45969
- const titulo = texto8(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
46410
+ const titulo = texto9(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
45970
46411
  const columnas = arr(o.columnas);
45971
46412
  if (columnas.length === 0 || columnas.length > 12) problemas.push(`${ref}.columnas debe tener entre 1 y 12 columnas`);
45972
46413
  const filas = arr(o.filas);
45973
46414
  if (filas.length > 200) problemas.push(`${ref}.filas admite hasta 200 filas`);
45974
- const cols = columnas.slice(0, 12).map((c, j) => texto8(c, 60, `${ref}.columnas[${j}]`, problemas, true) ?? "");
46415
+ const cols = columnas.slice(0, 12).map((c, j) => texto9(c, 60, `${ref}.columnas[${j}]`, problemas, true) ?? "");
45975
46416
  const celdasTabla = filas.slice(0, 200).map((f, j) => {
45976
46417
  const celdas = arr(f);
45977
46418
  if (celdas.length !== cols.length) problemas.push(`${ref}.filas[${j}] tiene ${celdas.length} celdas y la tabla ${cols.length} columnas`);
@@ -45987,7 +46428,7 @@ function validarWidgets(entrada) {
45987
46428
  break;
45988
46429
  }
45989
46430
  case "texto": {
45990
- salida.push({ tipo, titulo: texto8(o.titulo, 120, `${ref}.titulo`, problemas), texto: texto8(o.texto, 4e3, `${ref}.texto`, problemas, true) ?? "", ancho: a });
46431
+ salida.push({ tipo, titulo: texto9(o.titulo, 120, `${ref}.titulo`, problemas), texto: texto9(o.texto, 4e3, `${ref}.texto`, problemas, true) ?? "", ancho: a });
45991
46432
  break;
45992
46433
  }
45993
46434
  }
@@ -46073,6 +46514,13 @@ function numOrNull(v) {
46073
46514
  const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
46074
46515
  return Number.isFinite(n) ? n : null;
46075
46516
  }
46517
+ function listaTextos(v) {
46518
+ const crudos = (Array.isArray(v) ? v : [v]).flatMap((x) => typeof x === "string" ? x.split(",") : []);
46519
+ return [...new Set(crudos.map(str3).filter((s) => s !== null))].slice(0, 20);
46520
+ }
46521
+ function ordenCasos(orden, despuesDe) {
46522
+ return orden === "id" || despuesDe !== null ? "id" : null;
46523
+ }
46076
46524
  function companyFrom(params, fallback) {
46077
46525
  const companyId = str3(params.companyId) ?? str3(fallback);
46078
46526
  if (!companyId) throw new OpsError("company_requerida", "companyId es obligatorio");
@@ -46102,10 +46550,19 @@ async function conErrores(fn) {
46102
46550
  function registrarHerramientas(ctx) {
46103
46551
  ctx.tools.register(TOOL_NAMES.listarCasos, toolSchema(TOOL_NAMES.listarCasos), async (params, runCtx) => conErrores(async () => {
46104
46552
  const p = obj(params);
46105
- const casos = await listarCasos(ctx, { companyId: runCtx.companyId, workflow: str3(p.workflow), estado: str3(p.estado), limit: numOrNull(p.limit) });
46553
+ const workflow = str3(p.workflow);
46554
+ if (workflow && !getWorkflow(workflow)) throw new OpsError("workflow_desconocido", `workflow desconocido: ${workflow}. Disponibles: ${listarWorkflows().map((w) => w.clave).join(", ")}`);
46555
+ const despuesDe = str3(p.despues_de);
46556
+ const porId = ordenCasos(p.orden, despuesDe) === "id";
46557
+ const casos = await listarCasos(ctx, { companyId: runCtx.companyId, workflow, estado: str3(p.estado), estados: listaTextos(p.estados), referencia: str3(p.referencia), limit: numOrNull(p.limit), offset: numOrNull(p.offset), orden: ordenCasos(p.orden, despuesDe), despuesDe });
46558
+ const limite = Math.min(Math.max(numOrNull(p.limit) || 50, 1), 500);
46559
+ const desde = Math.max(Math.floor(numOrNull(p.offset) ?? 0), 0);
46560
+ const mas = casos.length < limite ? "" : porId ? `
46561
+ Puede haber m\xE1s casos: ped\xED la p\xE1gina siguiente con los mismos filtros, orden "id" y despues_de "${casos[casos.length - 1].id}" (limit ${limite}).` : `
46562
+ Puede haber m\xE1s casos: ped\xED la p\xE1gina siguiente con offset ${desde + casos.length} (limit ${limite}).`;
46106
46563
  return {
46107
- content: casos.length ? casos.map(resumenCaso).join("\n") : "No hay casos que coincidan.",
46108
- data: { casos }
46564
+ content: casos.length ? `${casos.map(resumenCaso).join("\n")}${mas}` : "No hay casos que coincidan.",
46565
+ data: { casos, ...mas && porId ? { despues_de: casos[casos.length - 1].id } : {} }
46109
46566
  };
46110
46567
  }));
46111
46568
  ctx.tools.register(TOOL_NAMES.leerCaso, toolSchema(TOOL_NAMES.leerCaso), async (params, runCtx) => conErrores(async () => {
@@ -46196,8 +46653,8 @@ Transiciones registradas: ${detalle.transiciones.length}; acciones registradas:
46196
46653
  const p = obj(params);
46197
46654
  const quedo = p.quedo === true ? true : p.quedo === false ? false : null;
46198
46655
  const r = await verificarEscritura(ctx, { companyId: runCtx.companyId, accionId: str3(p.accionId) ?? "", quedo, leido: obj(p.leido), detalle: str3(p.detalle), actor: actorAgente(runCtx) });
46199
- const texto9 = quedo === true ? "qued\xF3 escrita: se da por hecha" : quedo === false ? "no se escribi\xF3: se puede reintentar" : r.paraPersona ? "no se pudo saber: pasa a una persona" : "no se pudo saber: volv\xE9 a verificar m\xE1s tarde";
46200
- return { content: `Escritura ${r.accion.id}: ${texto9}`, data: r };
46656
+ const texto10 = quedo === true ? "qued\xF3 escrita: se da por hecha" : quedo === false ? "no se escribi\xF3: se puede reintentar" : r.paraPersona ? "no se pudo saber: pasa a una persona" : "no se pudo saber: volv\xE9 a verificar m\xE1s tarde";
46657
+ return { content: `Escritura ${r.accion.id}: ${texto10}`, data: r };
46201
46658
  }));
46202
46659
  ctx.tools.register(TOOL_NAMES.listarReglas, toolSchema(TOOL_NAMES.listarReglas), async (params, runCtx) => conErrores(async () => {
46203
46660
  const p = obj(params);
@@ -46275,7 +46732,7 @@ function registrarDatosYAcciones(ctx) {
46275
46732
  transiciones: w.transiciones.map((t6) => ({ via: t6.via, de: t6.de, a: typeof t6.a === "string" ? t6.a : "(seg\xFAn datos)", actor: t6.actor, nivelRequerido: t6.nivelRequerido ?? null, descripcion: t6.descripcion }))
46276
46733
  })));
46277
46734
  ctx.data.register(DATA_KEYS.tablero, async (params) => tablero(ctx, companyFrom(params), numOrNull(params.dias)));
46278
- ctx.data.register(DATA_KEYS.casos, async (params) => listarCasos(ctx, { companyId: companyFrom(params), workflow: str3(params.workflow), estado: str3(params.estado), limit: numOrNull(params.limit) }));
46735
+ ctx.data.register(DATA_KEYS.casos, async (params) => listarCasos(ctx, { companyId: companyFrom(params), workflow: str3(params.workflow), estado: str3(params.estado), estados: listaTextos(params.estados), referencia: str3(params.referencia), limit: numOrNull(params.limit), offset: numOrNull(params.offset), orden: ordenCasos(params.orden, str3(params.despues_de)), despuesDe: str3(params.despues_de) }));
46279
46736
  ctx.data.register(DATA_KEYS.caso, async (params) => {
46280
46737
  const casoId = str3(params.casoId);
46281
46738
  if (!casoId) throw new OpsError("caso_requerido", "casoId es obligatorio");
@@ -46341,7 +46798,7 @@ async function manejarApi(ctx, input2) {
46341
46798
  case "tablero":
46342
46799
  return respuesta(200, await tablero(ctx, companyId, numOrNull(qs("dias"))));
46343
46800
  case "casos":
46344
- return respuesta(200, { casos: await listarCasos(ctx, { companyId, workflow: qs("workflow"), estado: qs("estado"), limit: numOrNull(qs("limit")) }) });
46801
+ return respuesta(200, { casos: await listarCasos(ctx, { companyId, workflow: qs("workflow"), estado: qs("estado"), estados: listaTextos(q.estados), referencia: qs("referencia"), limit: numOrNull(qs("limit")), offset: numOrNull(qs("offset")), orden: ordenCasos(qs("orden"), qs("despues_de")), despuesDe: qs("despues_de") }) });
46345
46802
  case "caso": {
46346
46803
  const detalle = await leerCasoCompleto(ctx, companyId, input2.params.casoId ?? "");
46347
46804
  return detalle ? respuesta(200, detalle) : respuesta(404, { error: "caso_inexistente" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-plugin-autopilot-ops",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
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": [