paperclip-plugin-autopilot-ops 0.1.44 → 0.1.46

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.44";
3
+ var PLUGIN_VERSION = "0.1.46";
4
4
  var DB_NAMESPACE_SLUG = "autopilot_ops";
5
5
  var PAGE_ROUTE = "autopilot";
6
6
  var TOOL_NAMES = {
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.44";
11
+ var PLUGIN_VERSION = "0.1.46";
12
12
  var DB_NAMESPACE_SLUG = "autopilot_ops";
13
13
  var PAGE_ROUTE = "autopilot";
14
14
  var TOOL_NAMES = {
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.44";
39935
+ var PLUGIN_VERSION = "0.1.46";
39936
39936
  var DB_NAMESPACE_SLUG = "autopilot_ops";
39937
39937
  var PAGE_ROUTE = "autopilot";
39938
39938
  var TOOL_NAMES = {
@@ -41432,7 +41432,8 @@ function leerRegla(v) {
41432
41432
  if (!rid) return null;
41433
41433
  const prof = num3(r.prof);
41434
41434
  const inst = num3(r.inst);
41435
- return { id: rid, prof: prof === null ? null : redondear3(prof), inst: inst === null ? null : redondear3(inst) };
41435
+ const importe = num3(r.importe);
41436
+ return { id: rid, prof: prof === null ? null : redondear3(prof), inst: inst === null ? null : redondear3(inst), ...importe !== null ? { importe: redondear3(importe) } : {} };
41436
41437
  }
41437
41438
  function honorarioDe(input2) {
41438
41439
  const { regla, monto, cantidad: cantidad2 } = input2;
@@ -43592,6 +43593,8 @@ var rentabilidadPractica = {
43592
43593
  inicial: "detectado",
43593
43594
  estados: ["detectado", "pendiente_aprobacion", "datos_incompletos", "completado", "en_renegociacion", "resuelta", "aceptada", "rentable", "descartado"],
43594
43595
  finalesOk: ["resuelta", "rentable", "aceptada"],
43596
+ // «Rentable» es el resultado de una lectura: no es un trabajo terminado ni ahorra tiempo al equipo.
43597
+ finalesSinTrabajo: ["rentable"],
43595
43598
  finalesNoOk: ["descartado"],
43596
43599
  minutosAbsorbidos: 15,
43597
43600
  transiciones: [
@@ -43708,6 +43711,8 @@ var rentabilidadInternacion = {
43708
43711
  inicial: "detectado",
43709
43712
  estados: ["detectado", "pendiente_aprobacion", "datos_incompletos", "completado", "en_revision", "resuelta", "aceptada", "rentable", "descartado"],
43710
43713
  finalesOk: ["resuelta", "rentable", "aceptada"],
43714
+ // «Rentable» es el resultado de una lectura: no es un trabajo terminado ni ahorra tiempo al equipo.
43715
+ finalesSinTrabajo: ["rentable"],
43711
43716
  finalesNoOk: ["descartado"],
43712
43717
  minutosAbsorbidos: 20,
43713
43718
  transiciones: [
@@ -43834,7 +43839,7 @@ function evaluarTransicion(input2) {
43834
43839
  a,
43835
43840
  via,
43836
43841
  validaciones,
43837
- minutosAbsorbidos: esFinalOk ? wf.minutosSegun ? wf.minutosSegun(caso) : wf.minutosAbsorbidos : 0,
43842
+ minutosAbsorbidos: esFinalOk && !wf.finalesSinTrabajo?.includes(a) ? wf.minutosSegun ? wf.minutosSegun(caso) : wf.minutosAbsorbidos : 0,
43838
43843
  esFinal: esFinalOk || wf.finalesNoOk.includes(a),
43839
43844
  autoaprobado
43840
43845
  };
@@ -44281,6 +44286,19 @@ function dia3(v) {
44281
44286
  const s = texto5(v);
44282
44287
  return s && /^\d{4}-\d{2}-\d{2}/.test(s) ? s.slice(0, 10) : null;
44283
44288
  }
44289
+ function honorarioDeRegla(regla, valor) {
44290
+ const pct = (n) => `${n.toLocaleString("es-AR", { maximumFractionDigits: 2 })} %`;
44291
+ const importe = regla.importe ?? null;
44292
+ if (importe !== null && importe > 0) {
44293
+ if (regla.prof !== null) return { honorario: redondear5(importe * regla.prof / 100), fuente: "importe_porcentaje", detalle: `honorario ${pesos2(importe)} \xD7 ${pct(regla.prof)} del profesional` };
44294
+ if (regla.inst !== null) return { honorario: redondear5(importe * (1 - regla.inst / 100)), fuente: "importe", detalle: `honorario ${pesos2(importe)} menos ${pct(regla.inst)} de la instituci\xF3n` };
44295
+ return { honorario: importe, fuente: "importe", detalle: `honorario fijo ${pesos2(importe)}` };
44296
+ }
44297
+ if (regla.prof !== null) return { honorario: regla.prof, fuente: "monto_fijo", detalle: `\xABprof\xBB ${regla.prof.toLocaleString("es-AR", { maximumFractionDigits: 2 })} sin honorario base: el HIS liquida ${pesos2(regla.prof)} fijos` };
44298
+ if (regla.inst !== null) return { honorario: redondear5(valor * regla.inst / 100), fuente: "porcentaje", detalle: `${pct(regla.inst)} de lo que paga el convenio` };
44299
+ return null;
44300
+ }
44301
+ var HONORARIO_MINIMO_PCT = 1;
44284
44302
  function leerLecturaRentabilidad(v) {
44285
44303
  if (!v || typeof v !== "object" || Array.isArray(v)) return { error: "lectura: tiene que ser un objeto" };
44286
44304
  const r = v;
@@ -44313,9 +44331,19 @@ function leerLecturaRentabilidad(v) {
44313
44331
  const pid = texto5(x.profesional_id);
44314
44332
  if (!pid) return { error: `lectura.honorarios[${i}].profesional_id: falta` };
44315
44333
  if (x.regla !== null && x.regla !== void 0 && !leerRegla(x.regla)) return { error: `lectura.honorarios[${i}].regla: tiene que ser {id, prof, inst} o null` };
44316
- honorarios.push({ profesional_id: pid, profesional: texto5(x.profesional), regla: leerRegla(x.regla) });
44334
+ let liquidado = null;
44335
+ if (x.liquidado !== null && x.liquidado !== void 0) {
44336
+ const lq = x.liquidado ?? {};
44337
+ const h = num6(lq.honorario);
44338
+ const c2 = num6(lq.cantidad);
44339
+ if (h === null || h < 0 || c2 === null || c2 <= 0) return { error: `lectura.honorarios[${i}].liquidado: tiene que ser {honorario \u2265 0, cantidad > 0} o null` };
44340
+ liquidado = { honorario: redondear5(h), cantidad: c2 };
44341
+ }
44342
+ honorarios.push({ profesional_id: pid, profesional: texto5(x.profesional), regla: leerRegla(x.regla), ...liquidado ? { liquidado } : {} });
44317
44343
  }
44318
44344
  const volumen = num6(r.volumen_90d);
44345
+ const volumenPeriodo = num6(r.volumen);
44346
+ const dias = num6(r.dias);
44319
44347
  return {
44320
44348
  lectura: {
44321
44349
  prestacion_id,
@@ -44327,10 +44355,14 @@ function leerLecturaRentabilidad(v) {
44327
44355
  componentes,
44328
44356
  insumos,
44329
44357
  honorarios,
44330
- volumen_90d: volumen !== null && volumen > 0 ? Math.round(volumen) : 0
44358
+ volumen_90d: volumen !== null && volumen > 0 ? Math.round(volumen) : 0,
44359
+ // Un solo período: el «por mes» se calcula con el volumen de los mismos días que se midieron.
44360
+ volumen: volumenPeriodo !== null && dias !== null && dias >= 1 ? Math.max(0, Math.round(volumenPeriodo)) : volumen !== null && volumen > 0 ? Math.round(volumen) : 0,
44361
+ dias: volumenPeriodo !== null && dias !== null && dias >= 1 ? Math.round(dias) : 90
44331
44362
  }
44332
44363
  };
44333
44364
  }
44365
+ var esParticular = (financiador) => /\b(sin cobertura|particular(es)?)\b/i.test(financiador);
44334
44366
  function mesesEntre(desde, hasta) {
44335
44367
  const [y1, m1] = desde.split("-").map(Number);
44336
44368
  const [y2, m2] = hasta.split("-").map(Number);
@@ -44344,7 +44376,7 @@ function evaluarRentabilidad(input2) {
44344
44376
  const avisos = [];
44345
44377
  const insumosARevisar = [];
44346
44378
  const valor = redondear5(l.componentes.reduce((a, c) => a + c.monto, 0));
44347
- if (valor <= 0) faltas.push("sin_valor_convenio");
44379
+ if (valor <= 0) faltas.push(esParticular(l.financiador) ? "particular_sin_precio" : "sin_valor_convenio");
44348
44380
  let insumos = 0;
44349
44381
  for (const i of l.insumos) {
44350
44382
  if (i.precio === null || i.precio <= 0) {
@@ -44362,20 +44394,38 @@ function evaluarRentabilidad(input2) {
44362
44394
  if (l.insumos.length === 0) avisos.push("sin_insumos");
44363
44395
  const debajo = [];
44364
44396
  const honorarios = [];
44397
+ const detalle = [];
44398
+ const falta = (f) => {
44399
+ if (!faltas.includes(f)) faltas.push(f);
44400
+ };
44365
44401
  if (l.honorarios.length === 0) faltas.push("sin_profesional");
44366
44402
  for (const h of l.honorarios) {
44367
44403
  const nombre = h.profesional ?? h.profesional_id;
44368
- if (!h.regla) {
44369
- if (!faltas.includes("sin_regla_honorario")) faltas.push("sin_regla_honorario");
44404
+ const deRegla = h.regla ? honorarioDeRegla(h.regla, valor) : null;
44405
+ let hon;
44406
+ if (h.liquidado) {
44407
+ hon = redondear5(h.liquidado.honorario / h.liquidado.cantidad);
44408
+ if (deRegla && Math.abs(deRegla.honorario - hon) > Math.max(1, hon * 0.01) && !avisos.includes("regla_distinta_de_lo_liquidado")) avisos.push("regla_distinta_de_lo_liquidado");
44409
+ detalle.push({ profesional_id: h.profesional_id, profesional: nombre, honorario: hon, fuente: "liquidado", detalle: `liquidado por el HIS: ${pesos2(hon)} por pr\xE1ctica${deRegla ? ` (la regla da ${pesos2(deRegla.honorario)}: ${deRegla.detalle})` : ""}` });
44410
+ } else if (!h.regla) {
44411
+ falta("sin_regla_honorario");
44370
44412
  debajo.push(`${nombre} (sin regla)`);
44413
+ detalle.push({ profesional_id: h.profesional_id, profesional: nombre, honorario: null, fuente: null, detalle: "sin regla de honorarios en el HIS" });
44371
44414
  continue;
44372
- }
44373
- if (h.regla.prof === null && h.regla.inst === null) {
44374
- if (!faltas.includes("regla_sin_valor")) faltas.push("regla_sin_valor");
44415
+ } else if (!deRegla) {
44416
+ falta("regla_sin_valor");
44375
44417
  debajo.push(`${nombre} (regla sin valor)`);
44418
+ detalle.push({ profesional_id: h.profesional_id, profesional: nombre, honorario: null, fuente: null, detalle: `la regla ${h.regla.id} del HIS no tiene honorario, \xABprof\xBB ni \xABinst\xBB` });
44419
+ continue;
44420
+ } else if (valor > 0 && deRegla.honorario < valor * HONORARIO_MINIMO_PCT / 100) {
44421
+ falta("honorario_a_confirmar");
44422
+ debajo.push(`${nombre} (honorario de ${pesos2(deRegla.honorario)}: a confirmar)`);
44423
+ detalle.push({ profesional_id: h.profesional_id, profesional: nombre, honorario: deRegla.honorario, fuente: deRegla.fuente, detalle: `regla ${h.regla.id}: ${deRegla.detalle}. Es menos del ${HONORARIO_MINIMO_PCT} % de lo que paga el convenio: hay que confirmar si est\xE1 bien cargada` });
44376
44424
  continue;
44425
+ } else {
44426
+ hon = deRegla.honorario;
44427
+ detalle.push({ profesional_id: h.profesional_id, profesional: nombre, honorario: hon, fuente: deRegla.fuente, detalle: `regla ${h.regla.id}: ${deRegla.detalle}` });
44377
44428
  }
44378
- const hon = honorarioDe({ regla: h.regla, monto: valor, cantidad: 1 });
44379
44429
  honorarios.push(hon);
44380
44430
  const m = valor - insumos - hon;
44381
44431
  if (valor > 0 && m / valor * 100 < umbral) debajo.push(nombre);
@@ -44385,7 +44435,7 @@ function evaluarRentabilidad(input2) {
44385
44435
  const margenPeor = honorarioMax !== null && valor > 0 ? redondear5(valor - insumos - honorarioMax) : null;
44386
44436
  const margenMejor = honorarioMin !== null && valor > 0 ? redondear5(valor - insumos - honorarioMin) : null;
44387
44437
  const margenPct = margenPeor !== null && valor > 0 ? Math.round(margenPeor / valor * 1e3) / 10 : null;
44388
- const impacto = margenPeor !== null ? redondear5(margenPeor * l.volumen_90d / 3) : null;
44438
+ const impacto = margenPeor !== null ? redondear5(margenPeor * l.volumen * 30 / l.dias) : null;
44389
44439
  let clase2;
44390
44440
  if (faltas.length > 0 || margenPct === null) clase2 = "datos_incompletos";
44391
44441
  else if (margenPeor < 0) clase2 = "perdida";
@@ -44402,6 +44452,9 @@ function evaluarRentabilidad(input2) {
44402
44452
  margen_pct: margenPct,
44403
44453
  umbral,
44404
44454
  impacto_mensual: impacto,
44455
+ volumen: l.volumen,
44456
+ dias: l.dias,
44457
+ honorarios: detalle,
44405
44458
  faltas,
44406
44459
  avisos,
44407
44460
  insumos_a_revisar: insumosARevisar,
@@ -44410,6 +44463,8 @@ function evaluarRentabilidad(input2) {
44410
44463
  }
44411
44464
  var NOMBRE_FALTA = {
44412
44465
  sin_valor_convenio: "el convenio no tiene valor para esta pr\xE1ctica",
44466
+ particular_sin_precio: "es un particular y la pr\xE1ctica no tiene precio de lista",
44467
+ honorario_a_confirmar: "el honorario que sale de la regla del HIS es menor al 1 % de lo que paga el convenio (hay que confirmar si la regla est\xE1 bien cargada)",
44413
44468
  insumo_sin_precio: "hay insumos sin precio",
44414
44469
  sin_regla_honorario: "hay profesionales sin regla de honorarios",
44415
44470
  sin_profesional: "se factur\xF3 sin un profesional (si es un medicamento o un descartable, no corresponde medirla como pr\xE1ctica)",
@@ -44420,7 +44475,7 @@ function oracionRentabilidad(l, e) {
44420
44475
  const practica = `${l.nombre} (${l.codigo}) con ${l.financiador}`;
44421
44476
  if (e.clase === "datos_incompletos") return `${practica}: no se puede calcular la rentabilidad porque ${e.faltas.map((f) => NOMBRE_FALTA[f]).join(" y ")}.`;
44422
44477
  const rango = e.margen_mejor !== null && e.margen_mejor !== e.margen_peor ? ` (entre ${pesos2(e.margen_peor)} y ${pesos2(e.margen_mejor)} seg\xFAn el profesional)` : "";
44423
- const mensual = e.impacto_mensual !== null && e.impacto_mensual !== 0 ? ` Con el volumen de los \xFAltimos 90 d\xEDas, ${e.impacto_mensual < 0 ? "se pierden" : "deja"} unos ${pesos2(Math.abs(e.impacto_mensual))} por mes.` : "";
44478
+ const mensual = e.impacto_mensual !== null && e.impacto_mensual !== 0 ? ` Con el volumen de los \xFAltimos ${e.dias ?? 90} d\xEDas, ${e.impacto_mensual < 0 ? "se pierden" : "deja"} unos ${pesos2(Math.abs(e.impacto_mensual))} por mes.` : "";
44424
44479
  if (e.clase === "perdida") return `${practica} da p\xE9rdida: el convenio paga ${pesos2(e.valor)} y el honorario m\xE1s los insumos cuestan m\xE1s${rango}.${mensual}`;
44425
44480
  if (e.clase === "margen_bajo") return `${practica} deja un margen de ${porcentaje(e.margen_pct)}, debajo del ${porcentaje(e.umbral)} de la instituci\xF3n${rango}.${mensual}`;
44426
44481
  return `${practica} deja un margen de ${porcentaje(e.margen_pct)}${rango}.`;
@@ -44455,6 +44510,7 @@ function leerFilasInsumos(v) {
44455
44510
  const errores = [];
44456
44511
  const filas = [];
44457
44512
  const vistas = /* @__PURE__ */ new Set();
44513
+ const repetidas = [];
44458
44514
  for (const [i, x] of v.entries()) {
44459
44515
  const r = x ?? {};
44460
44516
  const n = i + 1;
@@ -44472,13 +44528,13 @@ function leerFilasInsumos(v) {
44472
44528
  if (!original || !codigo || !articulo || cantidad2 === null || cantidad2 <= 0) continue;
44473
44529
  const k = `${codigo}|${articulo.toLowerCase()}`;
44474
44530
  if (vistas.has(k)) {
44475
- errores.push(`fila ${n}: el insumo \xAB${articulo}\xBB est\xE1 repetido para la pr\xE1ctica ${original}`);
44531
+ repetidas.push(`fila ${n}: \xAB${articulo}\xBB en la pr\xE1ctica ${original}${texto6(r.practica) ? ` (${texto6(r.practica)})` : ""}`);
44476
44532
  continue;
44477
44533
  }
44478
44534
  vistas.add(k);
44479
44535
  filas.push({ codigo, codigo_original: original, practica: texto6(r.practica)?.slice(0, 200) ?? null, articulo, cantidad: cantidad2, precio, precio_actualizado: leerFecha(r.precio_actualizado) });
44480
44536
  }
44481
- return errores.length ? { errores } : { filas };
44537
+ return errores.length ? { errores } : { filas, repetidas };
44482
44538
  }
44483
44539
  async function guardarInsumosPractica(ctx, input2) {
44484
44540
  if (input2.actor.tipo !== "humano") throw new OpsError("actor_no_permitido", "los insumos de cada pr\xE1ctica los carga una persona de la instituci\xF3n desde el portal");
@@ -44493,7 +44549,7 @@ async function guardarInsumosPractica(ctx, input2) {
44493
44549
  [randomUUID2(), input2.companyId, f.codigo, f.codigo_original, f.practica, f.articulo, f.cantidad, f.precio, f.precio === null ? null : f.precio_actualizado ?? input2.hoy, input2.actor.tipo, input2.actor.id]
44494
44550
  );
44495
44551
  }
44496
- return { practicas: codigos.length, insumos: l.filas.length };
44552
+ return { practicas: codigos.length, insumos: l.filas.length, repetidas: l.repetidas.slice(0, 200) };
44497
44553
  }
44498
44554
  function aFila(r) {
44499
44555
  const n = (v) => v === null || v === void 0 ? null : Number(v);
@@ -44647,8 +44703,9 @@ function evaluarCostoInternacion(input2) {
44647
44703
  let sinRegla = 0;
44648
44704
  for (const f of l.facturado) {
44649
44705
  if (!f.profesional_id || f.liquidable === false) continue;
44650
- if (!f.regla || f.regla.prof === null && f.regla.inst === null) sinRegla += 1;
44651
- honorarios += honorarioDe({ regla: f.regla, monto: f.monto, cantidad: f.cantidad });
44706
+ const h = f.regla ? honorarioDeRegla(f.regla, f.monto) : null;
44707
+ if (!h) sinRegla += 1;
44708
+ else honorarios += h.honorario * f.cantidad;
44652
44709
  }
44653
44710
  honorarios = redondear6(honorarios);
44654
44711
  if (sinRegla > 0) avisos.push("prestaciones_sin_regla");
@@ -46246,6 +46303,9 @@ async function registrarContacto(ctx, input2) {
46246
46303
  function finalesOk() {
46247
46304
  return [...new Set(listarWorkflows().flatMap((w) => w.finalesOk))];
46248
46305
  }
46306
+ function finalesSinTrabajo() {
46307
+ return new Set(listarWorkflows().flatMap((w) => w.finalesSinTrabajo ?? []));
46308
+ }
46249
46309
  function finalesNoOk() {
46250
46310
  return [...new Set(listarWorkflows().flatMap((w) => w.finalesNoOk))];
46251
46311
  }
@@ -46262,8 +46322,8 @@ async function tablero(ctx, companyId, diasInput) {
46262
46322
  p2
46263
46323
  );
46264
46324
  const totalRows = await ctx.db.query(
46265
- `SELECT coalesce(sum(minutos_absorbidos), 0)::int AS minutos FROM ${t3(ctx, "caso")} WHERE company_id = $1`,
46266
- p1
46325
+ `SELECT coalesce(sum(minutos_absorbidos), 0)::int AS minutos FROM ${t3(ctx, "caso")} WHERE company_id = $1 AND NOT (estado = ANY($2::text[]))`,
46326
+ [companyId, [...finalesSinTrabajo()]]
46267
46327
  );
46268
46328
  const accionesRows = await ctx.db.query(
46269
46329
  `SELECT tipo, count(*)::int AS cantidad FROM ${t3(ctx, "accion")} WHERE company_id = $1 AND created_at >= now() - ($2::int * interval '1 day') GROUP BY tipo`,
@@ -46275,12 +46335,15 @@ async function tablero(ctx, companyId, diasInput) {
46275
46335
  listarPoliticas(ctx, companyId)
46276
46336
  ]);
46277
46337
  const okSet = new Set(finalesOk());
46338
+ const sinTrabajo = finalesSinTrabajo();
46278
46339
  const noOkSet = new Set(finalesNoOk());
46279
46340
  const porEstado = porEstadoRows.map((r) => ({ workflow: String(r.workflow), estado: String(r.estado), cantidad: Number(r.cantidad ?? 0) }));
46280
46341
  const abiertosPor = /* @__PURE__ */ new Map();
46281
46342
  const pendientesPor = /* @__PURE__ */ new Map();
46282
46343
  let conDiferencias = 0;
46344
+ let faltaUnDato = 0;
46283
46345
  for (const r of porEstado) {
46346
+ if (r.estado === "datos_incompletos") faltaUnDato += r.cantidad;
46284
46347
  if (!okSet.has(r.estado) && !noOkSet.has(r.estado)) abiertosPor.set(r.workflow, (abiertosPor.get(r.workflow) ?? 0) + r.cantidad);
46285
46348
  if (r.estado === "pendiente_aprobacion") pendientesPor.set(r.workflow, (pendientesPor.get(r.workflow) ?? 0) + r.cantidad);
46286
46349
  if (r.estado === "con_diferencias") conDiferencias += r.cantidad;
@@ -46291,6 +46354,7 @@ async function tablero(ctx, companyId, diasInput) {
46291
46354
  for (const r of cerradosRows) {
46292
46355
  const wf = String(r.workflow);
46293
46356
  const estado = String(r.estado);
46357
+ if (sinTrabajo.has(estado)) continue;
46294
46358
  if (okSet.has(estado)) {
46295
46359
  cerradosOkPor.set(wf, (cerradosOkPor.get(wf) ?? 0) + Number(r.cantidad ?? 0));
46296
46360
  minutosPor.set(wf, (minutosPor.get(wf) ?? 0) + Number(r.minutos ?? 0));
@@ -46311,6 +46375,8 @@ async function tablero(ctx, companyId, diasInput) {
46311
46375
  cerradosOkPeriodo: sum(cerradosOkPor),
46312
46376
  rechazadosPeriodo,
46313
46377
  conDiferencias,
46378
+ // Abiertos que no avanzan hasta que el equipo cargue un dato en el HIS (no están «en curso»).
46379
+ faltaUnDato,
46314
46380
  horasAbsorbidasPeriodo: Math.round(sum(minutosPor) / 60 * 10) / 10,
46315
46381
  horasAbsorbidasTotal: Math.round(Number(totalRows[0]?.minutos ?? 0) / 60 * 10) / 10,
46316
46382
  accionesPeriodo,
@@ -46351,6 +46417,7 @@ async function serie(ctx, companyId, diasInput) {
46351
46417
  )
46352
46418
  ]);
46353
46419
  const okSet = new Set(finalesOk());
46420
+ const sinTrabajo = finalesSinTrabajo();
46354
46421
  const puntos = /* @__PURE__ */ new Map();
46355
46422
  const desde = new Date(Date.now() - (dias - 1) * 864e5);
46356
46423
  for (let i = 0; i < dias; i += 1) {
@@ -46367,6 +46434,7 @@ async function serie(ctx, companyId, diasInput) {
46367
46434
  for (const r of abiertos) punto(r.dia).abiertos += Number(r.cantidad ?? 0);
46368
46435
  for (const r of cerrados) {
46369
46436
  const pt = punto(r.dia);
46437
+ if (sinTrabajo.has(String(r.estado))) continue;
46370
46438
  if (okSet.has(String(r.estado))) {
46371
46439
  pt.cerradosOk += Number(r.cantidad ?? 0);
46372
46440
  pt.minutos += Number(r.minutos ?? 0);
@@ -47003,6 +47071,21 @@ var MAX_LARGO_LECTURA = 900;
47003
47071
  var MAX_LARGO_PERIODO = 60;
47004
47072
  var MAX_LARGO_DESCRIPCION = 160;
47005
47073
  var MAX_LARGO_CONTEXTO = 120;
47074
+ var MAX_LARGO_TITULO = 60;
47075
+ var MAX_PRIMERA_ORACION = 160;
47076
+ var MESES_RE = "(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|setiembre|octubre|noviembre|diciembre) \\d{4}";
47077
+ var PERIODO_RE = new RegExp(`^(${MESES_RE}( a ${MESES_RE})?|al \\d{2}/\\d{2}/\\d{4})$`, "i");
47078
+ function primeraOracion(texto10) {
47079
+ const re = /\.\s+(?=[A-ZÁÉÍÓÚÑ¿¡«(])/g;
47080
+ let m;
47081
+ while (m = re.exec(texto10)) {
47082
+ const ultima = texto10.slice(0, m.index).split(/\s/).pop() ?? "";
47083
+ if (/^(dr|dra|sr|sra|lic|ing|aprox|etc|nro|art|av|[a-záéíóúñ])$/i.test(ultima) || m.index < 20) continue;
47084
+ return texto10.slice(0, m.index + 1);
47085
+ }
47086
+ return texto10;
47087
+ }
47088
+ var sinApertura = (c) => JSON.stringify(Object.fromEntries(Object.entries(c.parametros).filter(([k]) => k !== "por").sort(([a], [b]) => a.localeCompare(b))));
47006
47089
  function marcaDelSistemaEn(texto10) {
47007
47090
  const m = /\bAlephoo\b|\b[Ii][Dd] \d+\b/.exec(texto10);
47008
47091
  return m ? m[0] : null;
@@ -47059,6 +47142,7 @@ function validarWidgets(entrada) {
47059
47142
  return { x: texto9(po.x, 40, `${ref}.puntos[${j}].x`, problemas, true) ?? "", y: numero2(po.y, `${ref}.puntos[${j}].y`, problemas) };
47060
47143
  });
47061
47144
  if (unidad === "%" && !ES_VARIACION.test(titulo)) serie2.forEach((p, j) => controlarPorcentaje(p.y, `${ref}.puntos[${j}].y`, problemas));
47145
+ if (!unidad) problemas.push(`${ref}.unidad: una curva lleva su unidad (\xABturnos\xBB, \xAB$\xBB, \xAB%\xBB)`);
47062
47146
  const { calculo, origen } = leerCalculo(o.calculo, ref, problemas);
47063
47147
  salida.push({ tipo, titulo, unidad, puntos: serie2, ancho: a, ...calculo ? { calculo } : {}, origen });
47064
47148
  break;
@@ -47085,7 +47169,10 @@ function validarWidgets(entrada) {
47085
47169
  break;
47086
47170
  }
47087
47171
  case "texto": {
47088
- salida.push({ tipo, titulo: texto9(o.titulo, 120, `${ref}.titulo`, problemas), texto: texto9(o.texto, 4e3, `${ref}.texto`, problemas, true) ?? "", ancho: a });
47172
+ const lectura = (texto9(o.texto, 4e3, `${ref}.texto`, problemas, true) ?? "").replace(/\*\*/g, "");
47173
+ const primera = primeraOracion(lectura.split(/\n{2,}/)[0]?.trim() ?? "");
47174
+ if (primera.length > MAX_PRIMERA_ORACION) problemas.push(`${ref}.texto: la primera oraci\xF3n tiene ${primera.length} caracteres: hasta ${MAX_PRIMERA_ORACION} (una acci\xF3n para la direcci\xF3n con un n\xFAmero; lo que la sostiene, despu\xE9s)`);
47175
+ salida.push({ tipo, titulo: texto9(o.titulo, 120, `${ref}.titulo`, problemas), texto: lectura, ancho: a });
47089
47176
  break;
47090
47177
  }
47091
47178
  }
@@ -47116,6 +47203,15 @@ function validarWidgets(entrada) {
47116
47203
  const marca = nombres.map(marcaDelSistemaEn).find(Boolean);
47117
47204
  if (marca) problemas.push(`widgets[${k}]: \xAB${marca}\xBB no es un dato de la instituci\xF3n (un usuario o la marca del sistema): sac\xE1 esa fila o agrupala en \xABSin asignar\xBB`);
47118
47205
  }
47206
+ for (const [k, w] of salida.entries()) {
47207
+ if (w.tipo !== "barras" || !w.calculo || w.unidad && /%|promedio|tasa/i.test(w.unidad)) continue;
47208
+ const metrica = w.calculo.metricas[0]?.id;
47209
+ const ind = salida.find((x) => x.tipo === "indicador" && typeof x.valor === "number" && !!x.calculo && x.calculo.metricas[0]?.id === metrica && sinApertura(x.calculo) === sinApertura(w.calculo));
47210
+ if (!ind) continue;
47211
+ const suma = w.series.reduce((a, s) => a + s.valor, 0);
47212
+ const valor = ind.valor;
47213
+ if (Math.abs(suma - valor) > Math.max(0.5, Math.abs(valor) * 5e-3)) problemas.push(`widgets[${k}]: las barras suman ${Math.round(suma * 100) / 100} y el indicador \xAB${ind.etiqueta}\xBB (mismo c\xE1lculo) dice ${valor}: agreg\xE1 la barra que falta (\xABSin \u2026\xBB u \xABOtros\xBB) o us\xE1 el mismo per\xEDodo`);
47214
+ }
47119
47215
  const textos = salida.filter((w) => w.tipo === "texto");
47120
47216
  if (textos.length > MAX_TEXTOS) problemas.push(`un solo widget de texto (\xABLectura\xBB): lo que cambi\xF3 va en una frase al final de ella, el resto en el comentario de la tarea`);
47121
47217
  for (const t6 of textos) if (t6.texto.length > MAX_LARGO_LECTURA) problemas.push(`la Lectura tiene ${t6.texto.length} caracteres: hasta ${MAX_LARGO_LECTURA} (2 o 3 p\xE1rrafos breves; el primero, qu\xE9 conviene hacer)`);
@@ -47171,6 +47267,12 @@ async function publicarTablero(ctx, input2) {
47171
47267
  if (typeof input2.periodo === "string" && input2.periodo.trim().length > MAX_LARGO_PERIODO) {
47172
47268
  throw new OpsError("tablero_invalido", `periodo: hasta ${MAX_LARGO_PERIODO} caracteres (p. ej. \xABoctubre 2025 a septiembre 2026\xBB); las aclaraciones van en la descripci\xF3n`);
47173
47269
  }
47270
+ if (typeof input2.periodo === "string" && input2.periodo.trim() && !PERIODO_RE.test(input2.periodo.trim())) {
47271
+ throw new OpsError("tablero_invalido", `periodo: \xAB${input2.periodo.trim()}\xBB no tiene la forma de siempre: \xABseptiembre 2026\xBB, \xABoctubre 2025 a septiembre 2026\xBB o \xABal 24/10/2026\xBB (lo dem\xE1s va en la descripci\xF3n)`);
47272
+ }
47273
+ if (titulo.length > MAX_LARGO_TITULO) {
47274
+ throw new OpsError("tablero_invalido", `titulo: hasta ${MAX_LARGO_TITULO} caracteres (entra en dos renglones de la tarjeta); el detalle va en la descripci\xF3n`);
47275
+ }
47174
47276
  const widgets = validarWidgets(input2.widgets);
47175
47277
  const fuentes = arr(input2.fuentes).filter((f) => typeof f === "string" && f.trim()).map((f) => String(f).trim().slice(0, 120)).slice(0, 12);
47176
47278
  const limpio = (v, max) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : null;
@@ -0,0 +1,3 @@
1
+ -- Una medición que da «rentable» es una lectura, no un trabajo hecho: no ahorra minutos al equipo (prueba LALCEC-QA).
2
+ UPDATE plugin_autopilot_ops_1a68482ede.caso SET minutos_absorbidos = 0
3
+ WHERE workflow IN ('rentabilidad_practica', 'rentabilidad_internacion') AND estado = 'rentable';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-plugin-autopilot-ops",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
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": [