paperclip-plugin-autopilot-ops 0.1.18 → 0.1.20

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.18";
3
+ var PLUGIN_VERSION = "0.1.20";
4
4
  var DB_NAMESPACE_SLUG = "autopilot_ops";
5
5
  var PAGE_ROUTE = "autopilot";
6
6
  var TOOL_NAMES = {
@@ -113,7 +113,7 @@ var manifest = {
113
113
  {
114
114
  name: TOOL_NAMES.abrirCaso,
115
115
  displayName: "Abrir caso",
116
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin 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), `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.',
116
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin 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.',
117
117
  parametersSchema: {
118
118
  type: "object",
119
119
  properties: {
@@ -370,6 +370,46 @@ var manifest = {
370
370
  capability: "api.routes.register",
371
371
  companyResolution: { from: "body", key: "companyId" }
372
372
  },
373
+ {
374
+ routeKey: "contactos",
375
+ method: "GET",
376
+ path: "/contactos",
377
+ auth: "board",
378
+ capability: "api.routes.register",
379
+ companyResolution: { from: "query", key: "companyId" }
380
+ },
381
+ {
382
+ routeKey: "guardar-contacto",
383
+ method: "POST",
384
+ path: "/contactos",
385
+ auth: "board",
386
+ capability: "api.routes.register",
387
+ companyResolution: { from: "body", key: "companyId" }
388
+ },
389
+ {
390
+ routeKey: "avisos-pendientes",
391
+ method: "GET",
392
+ path: "/avisos/pendientes",
393
+ auth: "board",
394
+ capability: "api.routes.register",
395
+ companyResolution: { from: "query", key: "companyId" }
396
+ },
397
+ {
398
+ routeKey: "avisos",
399
+ method: "GET",
400
+ path: "/avisos",
401
+ auth: "board",
402
+ capability: "api.routes.register",
403
+ companyResolution: { from: "query", key: "companyId" }
404
+ },
405
+ {
406
+ routeKey: "resultado-aviso",
407
+ method: "POST",
408
+ path: "/avisos/:avisoId/resultado",
409
+ auth: "board",
410
+ capability: "api.routes.register",
411
+ companyResolution: { from: "body", key: "companyId" }
412
+ },
373
413
  {
374
414
  routeKey: "contacto",
375
415
  method: "POST",
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.18";
11
+ var PLUGIN_VERSION = "0.1.20";
12
12
  var DB_NAMESPACE_SLUG = "autopilot_ops";
13
13
  var PAGE_ROUTE = "autopilot";
14
14
  var TOOL_NAMES = {
@@ -121,7 +121,7 @@ var manifest = {
121
121
  {
122
122
  name: TOOL_NAMES.abrirCaso,
123
123
  displayName: "Abrir caso",
124
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin 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), `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.',
124
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin 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.',
125
125
  parametersSchema: {
126
126
  type: "object",
127
127
  properties: {
@@ -378,6 +378,46 @@ var manifest = {
378
378
  capability: "api.routes.register",
379
379
  companyResolution: { from: "body", key: "companyId" }
380
380
  },
381
+ {
382
+ routeKey: "contactos",
383
+ method: "GET",
384
+ path: "/contactos",
385
+ auth: "board",
386
+ capability: "api.routes.register",
387
+ companyResolution: { from: "query", key: "companyId" }
388
+ },
389
+ {
390
+ routeKey: "guardar-contacto",
391
+ method: "POST",
392
+ path: "/contactos",
393
+ auth: "board",
394
+ capability: "api.routes.register",
395
+ companyResolution: { from: "body", key: "companyId" }
396
+ },
397
+ {
398
+ routeKey: "avisos-pendientes",
399
+ method: "GET",
400
+ path: "/avisos/pendientes",
401
+ auth: "board",
402
+ capability: "api.routes.register",
403
+ companyResolution: { from: "query", key: "companyId" }
404
+ },
405
+ {
406
+ routeKey: "avisos",
407
+ method: "GET",
408
+ path: "/avisos",
409
+ auth: "board",
410
+ capability: "api.routes.register",
411
+ companyResolution: { from: "query", key: "companyId" }
412
+ },
413
+ {
414
+ routeKey: "resultado-aviso",
415
+ method: "POST",
416
+ path: "/avisos/:avisoId/resultado",
417
+ auth: "board",
418
+ capability: "api.routes.register",
419
+ companyResolution: { from: "body", key: "companyId" }
420
+ },
381
421
  {
382
422
  routeKey: "contacto",
383
423
  method: "POST",
package/dist/worker.js CHANGED
@@ -913,8 +913,8 @@ function numKeys(data) {
913
913
  return keyCount;
914
914
  }
915
915
  var getParsedType = (data) => {
916
- const t4 = typeof data;
917
- switch (t4) {
916
+ const t5 = typeof data;
917
+ switch (t5) {
918
918
  case "undefined":
919
919
  return "undefined";
920
920
  case "string":
@@ -953,7 +953,7 @@ var getParsedType = (data) => {
953
953
  }
954
954
  return "object";
955
955
  default:
956
- throw new Error(`Unknown data type: ${t4}`);
956
+ throw new Error(`Unknown data type: ${t5}`);
957
957
  }
958
958
  };
959
959
  var propertyKeyTypes = /* @__PURE__ */ new Set(["string", "number", "symbol"]);
@@ -1253,8 +1253,8 @@ function getLengthableOrigin(input2) {
1253
1253
  return "unknown";
1254
1254
  }
1255
1255
  function parsedType(data) {
1256
- const t4 = typeof data;
1257
- switch (t4) {
1256
+ const t5 = typeof data;
1257
+ switch (t5) {
1258
1258
  case "number": {
1259
1259
  return Number.isNaN(data) ? "nan" : "number";
1260
1260
  }
@@ -1271,7 +1271,7 @@ function parsedType(data) {
1271
1271
  }
1272
1272
  }
1273
1273
  }
1274
- return t4;
1274
+ return t5;
1275
1275
  }
1276
1276
  function issue(...args) {
1277
1277
  const [iss, input2, inst] = args;
@@ -3495,7 +3495,7 @@ function handleCatchall(proms, input2, payload, ctx, def2, inst, abortEarly) {
3495
3495
  const unrecognized = [];
3496
3496
  const keySet = def2.keySet;
3497
3497
  const _catchall = def2.catchall._zod;
3498
- const t4 = _catchall.def.type;
3498
+ const t5 = _catchall.def.type;
3499
3499
  const optin = _catchall.optin;
3500
3500
  const optout = _catchall.optout;
3501
3501
  let seen = 0;
@@ -3508,11 +3508,11 @@ function handleCatchall(proms, input2, payload, ctx, def2, inst, abortEarly) {
3508
3508
  if (keySet.has(key))
3509
3509
  continue;
3510
3510
  if (key === "__proto__") {
3511
- if (t4 === "never")
3511
+ if (t5 === "never")
3512
3512
  unrecognized.push(key);
3513
3513
  continue;
3514
3514
  }
3515
- if (t4 === "never") {
3515
+ if (t5 === "never") {
3516
3516
  unrecognized.push(key);
3517
3517
  continue;
3518
3518
  }
@@ -7748,16 +7748,16 @@ var error20 = () => {
7748
7748
  number: { unit: "", shortLabel: "\u05E7\u05D8\u05DF", longLabel: "\u05D2\u05D3\u05D5\u05DC" }
7749
7749
  // no unit
7750
7750
  };
7751
- const typeEntry = (t4) => t4 ? TypeNames[t4] : void 0;
7752
- const typeLabel = (t4) => {
7753
- const e = typeEntry(t4);
7751
+ const typeEntry = (t5) => t5 ? TypeNames[t5] : void 0;
7752
+ const typeLabel = (t5) => {
7753
+ const e = typeEntry(t5);
7754
7754
  if (e)
7755
7755
  return e.label;
7756
- return t4 ?? TypeNames.unknown.label;
7756
+ return t5 ?? TypeNames.unknown.label;
7757
7757
  };
7758
- const withDefinite = (t4) => `\u05D4${typeLabel(t4)}`;
7759
- const verbFor = (t4) => {
7760
- const e = typeEntry(t4);
7758
+ const withDefinite = (t5) => `\u05D4${typeLabel(t5)}`;
7759
+ const verbFor = (t5) => {
7760
+ const e = typeEntry(t5);
7761
7761
  const gender = e?.gender ?? "m";
7762
7762
  return gender === "f" ? "\u05E6\u05E8\u05D9\u05DB\u05D4 \u05DC\u05D4\u05D9\u05D5\u05EA" : "\u05E6\u05E8\u05D9\u05DA \u05DC\u05D4\u05D9\u05D5\u05EA";
7763
7763
  };
@@ -16638,7 +16638,7 @@ function stringifyKeyNames(bySchema, json2, visited) {
16638
16638
  json2 = { ...json2, [keyword]: mapped };
16639
16639
  }
16640
16640
  const types = Array.isArray(json2.type) ? json2.type : [json2.type];
16641
- const numericType = !types.includes("string") && types.some((t4) => t4 === "number" || t4 === "integer");
16641
+ const numericType = !types.includes("string") && types.some((t5) => t5 === "number" || t5 === "integer");
16642
16642
  const values = json2.enum ?? (json2.const !== void 0 ? [json2.const] : void 0);
16643
16643
  if (!numericType && !values?.some((v) => typeof v === "number"))
16644
16644
  return json2;
@@ -19141,8 +19141,8 @@ function convertBaseSchema(schema, ctx) {
19141
19141
  }
19142
19142
  const type = schema.type;
19143
19143
  if (Array.isArray(type)) {
19144
- const typeSchemas = type.map((t4) => {
19145
- const typeSchema = { ...schema, type: t4 };
19144
+ const typeSchemas = type.map((t5) => {
19145
+ const typeSchema = { ...schema, type: t5 };
19146
19146
  return convertBaseSchema(typeSchema, ctx);
19147
19147
  });
19148
19148
  if (typeSchemas.length === 0) {
@@ -31178,7 +31178,7 @@ var pluginManifestV1Schema = external_exports.object({
31178
31178
  }
31179
31179
  }
31180
31180
  if (manifest2.tools) {
31181
- const toolNames = manifest2.tools.map((t4) => t4.name);
31181
+ const toolNames = manifest2.tools.map((t5) => t5.name);
31182
31182
  const duplicates = toolNames.filter((name, i) => toolNames.indexOf(name) !== i);
31183
31183
  if (duplicates.length > 0) {
31184
31184
  ctx.addIssue({
@@ -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.18";
39935
+ var PLUGIN_VERSION = "0.1.20";
39936
39936
  var DB_NAMESPACE_SLUG = "autopilot_ops";
39937
39937
  var PAGE_ROUTE = "autopilot";
39938
39938
  var TOOL_NAMES = {
@@ -40045,7 +40045,7 @@ var manifest = {
40045
40045
  {
40046
40046
  name: TOOL_NAMES.abrirCaso,
40047
40047
  displayName: "Abrir caso",
40048
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin 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), `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.',
40048
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin 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.',
40049
40049
  parametersSchema: {
40050
40050
  type: "object",
40051
40051
  properties: {
@@ -40302,6 +40302,46 @@ var manifest = {
40302
40302
  capability: "api.routes.register",
40303
40303
  companyResolution: { from: "body", key: "companyId" }
40304
40304
  },
40305
+ {
40306
+ routeKey: "contactos",
40307
+ method: "GET",
40308
+ path: "/contactos",
40309
+ auth: "board",
40310
+ capability: "api.routes.register",
40311
+ companyResolution: { from: "query", key: "companyId" }
40312
+ },
40313
+ {
40314
+ routeKey: "guardar-contacto",
40315
+ method: "POST",
40316
+ path: "/contactos",
40317
+ auth: "board",
40318
+ capability: "api.routes.register",
40319
+ companyResolution: { from: "body", key: "companyId" }
40320
+ },
40321
+ {
40322
+ routeKey: "avisos-pendientes",
40323
+ method: "GET",
40324
+ path: "/avisos/pendientes",
40325
+ auth: "board",
40326
+ capability: "api.routes.register",
40327
+ companyResolution: { from: "query", key: "companyId" }
40328
+ },
40329
+ {
40330
+ routeKey: "avisos",
40331
+ method: "GET",
40332
+ path: "/avisos",
40333
+ auth: "board",
40334
+ capability: "api.routes.register",
40335
+ companyResolution: { from: "query", key: "companyId" }
40336
+ },
40337
+ {
40338
+ routeKey: "resultado-aviso",
40339
+ method: "POST",
40340
+ path: "/avisos/:avisoId/resultado",
40341
+ auth: "board",
40342
+ capability: "api.routes.register",
40343
+ companyResolution: { from: "body", key: "companyId" }
40344
+ },
40305
40345
  {
40306
40346
  routeKey: "contacto",
40307
40347
  method: "POST",
@@ -40536,8 +40576,8 @@ function fechaAISO(v) {
40536
40576
  if (fechaISO(s.slice(0, 10))) return s.slice(0, 10);
40537
40577
  const m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})/.exec(s);
40538
40578
  if (!m) return null;
40539
- const iso4 = `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`;
40540
- return fechaISO(iso4) ? iso4 : null;
40579
+ const iso5 = `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`;
40580
+ return fechaISO(iso5) ? iso5 : null;
40541
40581
  }
40542
40582
  function leerCola(v) {
40543
40583
  if (!Array.isArray(v) || v.length === 0) return null;
@@ -40828,6 +40868,17 @@ var REGLAS_CUENTA = {
40828
40868
  };
40829
40869
  var BONO_POR_FACTURAR = ["no_confirmado", "confirmado", "prefacturado"];
40830
40870
  var BONO_FACTURADO = ["facturado", "refacturado"];
40871
+ function leerResponsable(v) {
40872
+ if (!v || typeof v !== "object") return null;
40873
+ const r = v;
40874
+ const tipo = texto(r.tipo);
40875
+ if (tipo !== "profesional" && tipo !== "rol") return null;
40876
+ const id = refExterna(r.id);
40877
+ const nombre = texto(r.nombre)?.slice(0, 120) ?? null;
40878
+ if (tipo === "profesional" && !id) return null;
40879
+ if (tipo === "rol" && !id) return null;
40880
+ return { tipo, id, nombre };
40881
+ }
40831
40882
  function leerFaltantes(v) {
40832
40883
  if (!Array.isArray(v)) return null;
40833
40884
  const out = [];
@@ -40837,17 +40888,18 @@ function leerFaltantes(v) {
40837
40888
  const regla = texto(r.regla);
40838
40889
  const detalle = texto(r.detalle);
40839
40890
  if (!regla || !REGLAS_CUENTA[regla] || !detalle) return null;
40840
- out.push({ regla, detalle, item: refExterna(r.item) });
40891
+ const responsable = leerResponsable(r.responsable);
40892
+ out.push(responsable ? { regla, detalle, item: refExterna(r.item), responsable } : { regla, detalle, item: refExterna(r.item) });
40841
40893
  }
40842
40894
  return out;
40843
40895
  }
40844
40896
  function validarFaltantes(campo, v, alMenosUno) {
40845
- const lista = leerFaltantes(v);
40846
- if (!lista) {
40897
+ const lista2 = leerFaltantes(v);
40898
+ if (!lista2) {
40847
40899
  return falla("faltantes_validos", `${campo} tiene que ser una lista [{regla, detalle, item?}] con reglas del cat\xE1logo: ${Object.keys(REGLAS_CUENTA).join(", ")}`);
40848
40900
  }
40849
- if (alMenosUno && lista.length === 0) return falla("faltantes_validos", `${campo} est\xE1 vac\xEDo: una cuenta sin faltantes no se abre`);
40850
- return ok("faltantes_validos", lista.length === 0 ? "sin faltantes" : lista.length === 1 ? "1 faltante" : `${lista.length} faltantes`);
40901
+ if (alMenosUno && lista2.length === 0) return falla("faltantes_validos", `${campo} est\xE1 vac\xEDo: una cuenta sin faltantes no se abre`);
40902
+ return ok("faltantes_validos", lista2.length === 0 ? "sin faltantes" : lista2.length === 1 ? "1 faltante" : `${lista2.length} faltantes`);
40851
40903
  }
40852
40904
  function estadoBono(v) {
40853
40905
  const s = texto(v);
@@ -41133,9 +41185,9 @@ function diasEntre(desde, hasta) {
41133
41185
  function horaDentro(hora, desde, hasta) {
41134
41186
  const h = horaHM(hora);
41135
41187
  const d = horaHM(desde);
41136
- const t4 = horaHM(hasta);
41137
- if (!h || !d || !t4) return false;
41138
- return h >= d && h <= t4;
41188
+ const t5 = horaHM(hasta);
41189
+ if (!h || !d || !t5) return false;
41190
+ return h >= d && h <= t5;
41139
41191
  }
41140
41192
  function codigoContacto(v) {
41141
41193
  const s = texto(v);
@@ -41295,9 +41347,9 @@ function evaluarTransicion(input2) {
41295
41347
  if (!wf) {
41296
41348
  return { ok: false, codigo: "workflow_desconocido", motivo: `workflow desconocido: ${caso.workflow}`, validaciones: [] };
41297
41349
  }
41298
- const def2 = wf.transiciones.find((t4) => t4.via === via);
41350
+ const def2 = wf.transiciones.find((t5) => t5.via === via);
41299
41351
  if (!def2) {
41300
- const disponibles = wf.transiciones.filter((t4) => t4.de.includes(caso.estado)).map((t4) => t4.via);
41352
+ const disponibles = wf.transiciones.filter((t5) => t5.de.includes(caso.estado)).map((t5) => t5.via);
41301
41353
  return { ok: false, codigo: "via_desconocida", motivo: `la transici\xF3n "${via}" no existe en ${wf.clave}; desde "${caso.estado}" se puede: ${disponibles.join(", ") || "ninguna"}`, validaciones: [] };
41302
41354
  }
41303
41355
  if (!def2.de.includes(caso.estado)) {
@@ -41340,10 +41392,10 @@ function evaluarTransicion(input2) {
41340
41392
  function transicionesDisponibles(caso, actor) {
41341
41393
  const wf = getWorkflow(caso.workflow);
41342
41394
  if (!wf) return [];
41343
- return wf.transiciones.filter((t4) => t4.de.includes(caso.estado) && (!actor || t4.actor.includes(actor)));
41395
+ return wf.transiciones.filter((t5) => t5.de.includes(caso.estado) && (!actor || t5.actor.includes(actor)));
41344
41396
  }
41345
41397
  function transicionCargaManual(caso) {
41346
- return transicionesDisponibles(caso, "humano").find((t4) => t4.cargaManual) ?? null;
41398
+ return transicionesDisponibles(caso, "humano").find((t5) => t5.cargaManual) ?? null;
41347
41399
  }
41348
41400
  function esModo(v) {
41349
41401
  return typeof v === "string" && MODOS.includes(v);
@@ -41438,10 +41490,10 @@ function poblacion(problemas, p) {
41438
41490
  else problemas.push("sexo: debe ser F o M");
41439
41491
  }
41440
41492
  if (p.obras_sociales !== void 0 && p.obras_sociales !== null) {
41441
- const lista = Array.isArray(p.obras_sociales) ? p.obras_sociales.map((x) => str(x)).filter((x) => x !== null) : [];
41493
+ const lista2 = Array.isArray(p.obras_sociales) ? p.obras_sociales.map((x) => str(x)).filter((x) => x !== null) : [];
41442
41494
  if (!Array.isArray(p.obras_sociales)) problemas.push("obras_sociales: debe ser una lista de nombres");
41443
- else if (lista.length > 20) problemas.push("obras_sociales: como m\xE1ximo 20");
41444
- else if (lista.length > 0) out.obras_sociales = lista.map((x) => x.slice(0, 80));
41495
+ else if (lista2.length > 20) problemas.push("obras_sociales: como m\xE1ximo 20");
41496
+ else if (lista2.length > 0) out.obras_sociales = lista2.map((x) => x.slice(0, 80));
41445
41497
  }
41446
41498
  return out;
41447
41499
  }
@@ -41670,8 +41722,8 @@ async function requerirRegla(ctx, companyId, id) {
41670
41722
  if (!r) throw new OpsError("regla_inexistente", `no existe la regla ${id} en esta instituci\xF3n`);
41671
41723
  return r;
41672
41724
  }
41673
- function cambio(version2, actor, texto3) {
41674
- return { version: version2, fecha: (/* @__PURE__ */ new Date()).toISOString(), actorTipo: actor.tipo, actorId: actor.id, cambio: texto3 };
41725
+ function cambio(version2, actor, texto4) {
41726
+ return { version: version2, fecha: (/* @__PURE__ */ new Date()).toISOString(), actorTipo: actor.tipo, actorId: actor.id, cambio: texto4 };
41675
41727
  }
41676
41728
  function soloPersonas(actor, que) {
41677
41729
  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`);
@@ -41903,8 +41955,8 @@ function describirOpciones(antes, despues) {
41903
41955
  const plazosAntes = JSON.stringify(leerPlazos(antes.plazos));
41904
41956
  const plazosDespues = leerPlazos(despues.plazos);
41905
41957
  if (plazosAntes !== JSON.stringify(plazosDespues)) {
41906
- const lista = Object.entries(plazosDespues).map(([os, d]) => `${os} ${d} d\xEDas`);
41907
- partes.push(`plazos de presentaci\xF3n: ${lista.length ? lista.join(", ") : "ninguno fijado (se usa el habitual)"}`);
41958
+ const lista2 = Object.entries(plazosDespues).map(([os, d]) => `${os} ${d} d\xEDas`);
41959
+ partes.push(`plazos de presentaci\xF3n: ${lista2.length ? lista2.join(", ") : "ninguno fijado (se usa el habitual)"}`);
41908
41960
  }
41909
41961
  return partes.length ? partes.join(" \xB7 ") : null;
41910
41962
  }
@@ -42275,8 +42327,8 @@ async function transicionar(ctx, input2) {
42275
42327
  }
42276
42328
  };
42277
42329
  }
42278
- var TIPOS_ACCION = ["lectura", "escritura", "verificacion"];
42279
- var CONECTORES = ["api", "base", "archivo", "pantalla", "manual"];
42330
+ var TIPOS_ACCION = ["lectura", "escritura", "verificacion", "aviso"];
42331
+ var CONECTORES = ["api", "base", "archivo", "pantalla", "manual", "mail", "whatsapp"];
42280
42332
  var RESULTADOS_ACCION = ["ok", "error", "pendiente"];
42281
42333
  async function registrarAccion(ctx, input2) {
42282
42334
  const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
@@ -42328,10 +42380,10 @@ function numero(v) {
42328
42380
  }
42329
42381
  function motivoSinCarga(caso) {
42330
42382
  const wf = getWorkflow(caso.workflow);
42331
- if (!wf || !wf.transiciones.some((t4) => t4.cargaManual)) return `el workflow ${caso.workflow} no tiene carga manual en el sistema de gesti\xF3n`;
42383
+ if (!wf || !wf.transiciones.some((t5) => t5.cargaManual)) return `el workflow ${caso.workflow} no tiene carga manual en el sistema de gesti\xF3n`;
42332
42384
  if (wf.finalesOk.includes(caso.estado)) return "ya est\xE1 cargado y comprobado";
42333
42385
  if (wf.finalesNoOk.includes(caso.estado)) return `est\xE1 ${caso.estado}: no se carga`;
42334
- if (wf.transiciones.some((t4) => t4.cargaManual && t4.a === caso.estado)) return "ya figura como cargado; falta la comprobaci\xF3n del puesto";
42386
+ if (wf.transiciones.some((t5) => t5.cargaManual && t5.a === caso.estado)) return "ya figura como cargado; falta la comprobaci\xF3n del puesto";
42335
42387
  return "todav\xEDa no est\xE1 aprobado: no hay nada para cargar";
42336
42388
  }
42337
42389
  async function registrarCargaManual(ctx, input2) {
@@ -42596,8 +42648,8 @@ async function serie(ctx, companyId, diasInput) {
42596
42648
  puntos.set(d, { dia: d, abiertos: 0, cerradosOk: 0, rechazados: 0, minutos: 0, acciones: 0 });
42597
42649
  }
42598
42650
  const descartado = { dia: "", abiertos: 0, cerradosOk: 0, rechazados: 0, minutos: 0, acciones: 0 };
42599
- const punto = (dia) => {
42600
- const k = typeof dia === "string" ? dia.slice(0, 10) : "";
42651
+ const punto = (dia2) => {
42652
+ const k = typeof dia2 === "string" ? dia2.slice(0, 10) : "";
42601
42653
  if (!/^\d{4}-\d{2}-\d{2}$/.test(k)) return descartado;
42602
42654
  if (!puntos.has(k)) puntos.set(k, { dia: k, abiertos: 0, cerradosOk: 0, rechazados: 0, minutos: 0, acciones: 0 });
42603
42655
  return puntos.get(k);
@@ -42654,6 +42706,322 @@ function errorParaAgente(err) {
42654
42706
  return { error: `error_interno: ${message2}`, data: { codigo: "error_interno" } };
42655
42707
  }
42656
42708
 
42709
+ // src/avisos.ts
42710
+ import { randomUUID as randomUUID3 } from "node:crypto";
42711
+ function t3(ctx, table) {
42712
+ return `${ctx.db.namespace}.${table}`;
42713
+ }
42714
+ function texto2(v) {
42715
+ return typeof v === "string" && v.trim().length > 0 ? v.trim() : typeof v === "number" ? String(v) : null;
42716
+ }
42717
+ function iso3(v) {
42718
+ if (v instanceof Date) return v.toISOString();
42719
+ if (typeof v === "string" && v) return v;
42720
+ return null;
42721
+ }
42722
+ function dia(v) {
42723
+ if (v instanceof Date) return v.toISOString().slice(0, 10);
42724
+ return String(v ?? "").slice(0, 10);
42725
+ }
42726
+ function lista(v) {
42727
+ if (Array.isArray(v)) return v;
42728
+ if (typeof v === "string") {
42729
+ try {
42730
+ const p = JSON.parse(v);
42731
+ return Array.isArray(p) ? p : [];
42732
+ } catch {
42733
+ return [];
42734
+ }
42735
+ }
42736
+ return [];
42737
+ }
42738
+ var ROLES = {
42739
+ admision: "Admisi\xF3n",
42740
+ aranceles: "Aranceles y convenios",
42741
+ facturacion: "Facturaci\xF3n",
42742
+ internacion: "Internaci\xF3n",
42743
+ personal: "Administraci\xF3n de personal"
42744
+ };
42745
+ var ESPERAN_PERSONA = {
42746
+ cuenta_completa: ["para_completar", "con_faltantes"],
42747
+ facturacion_pendiente: ["por_presentar"],
42748
+ credencial_profesional: ["observada"]
42749
+ };
42750
+ var CANALES_AVISO = ["mail", "whatsapp"];
42751
+ var HORARIO_AVISOS = { desde: "08:00", hasta: "18:00" };
42752
+ var DIAS_ENTRE_AVISOS = 3;
42753
+ var MAX_AVISOS = 2;
42754
+ var MAX_INTENTOS_ENVIO = 3;
42755
+ var DIAS_VENTANA = 45;
42756
+ var OBSERVACIONES_MATRICULA = {
42757
+ sin_matricula: "falta cargar la matr\xEDcula",
42758
+ no_figura: "la matr\xEDcula no figura en el registro nacional",
42759
+ otra_persona: "la matr\xEDcula figura a nombre de otra persona",
42760
+ inhabilitada: "la matr\xEDcula est\xE1 inhabilitada",
42761
+ sancion: "la matr\xEDcula tiene una sanci\xF3n registrada"
42762
+ };
42763
+ var rol = (clave) => ({ clave: `rol:${clave}`, nombre: ROLES[clave] ?? clave, reserva: null });
42764
+ function pendientesDe(caso) {
42765
+ const estados = ESPERAN_PERSONA[caso.workflow];
42766
+ if (!estados || !estados.includes(caso.estado)) return [];
42767
+ const d = caso.datos;
42768
+ const bono = `Bono ${texto2(d.bono_numero) ?? texto2(d.bono_id) ?? "sin n\xFAmero"}${texto2(d.obra_social) ? ` \xB7 ${texto2(d.obra_social)}` : ""}`;
42769
+ const base = { casoId: caso.id, workflow: caso.workflow, titulo: caso.titulo };
42770
+ if (caso.workflow === "cuenta_completa") {
42771
+ const out = [];
42772
+ for (const f of leerFaltantes(d.faltantes) ?? []) {
42773
+ const nombreRegla = REGLAS_CUENTA[f.regla]?.nombre ?? f.regla;
42774
+ if (f.regla === "epicrisis_faltante") {
42775
+ const p = f.responsable?.tipo === "profesional" && f.responsable.id ? f.responsable : null;
42776
+ const internacion = texto2(d.internacion_id) ? `Internaci\xF3n ${texto2(d.internacion_id)}` : bono;
42777
+ out.push({
42778
+ ...base,
42779
+ que: `${internacion}: falta la epicrisis`,
42780
+ responsable: p ? { clave: `profesional:${p.id}`, nombre: p.nombre ?? `Profesional ${p.id}`, reserva: "rol:internacion" } : rol("internacion")
42781
+ });
42782
+ } else if (f.regla === "prestacion_sin_valor") {
42783
+ out.push({ ...base, que: `${bono}: ${nombreRegla.toLowerCase()}`, responsable: rol("aranceles") });
42784
+ } else {
42785
+ out.push({ ...base, que: `${bono}: ${nombreRegla.toLowerCase()}`, responsable: rol("admision") });
42786
+ }
42787
+ }
42788
+ return out;
42789
+ }
42790
+ if (caso.workflow === "facturacion_pendiente") {
42791
+ const vence = texto2(d.vence_el);
42792
+ return [{ ...base, que: `${bono}: sin presentar${vence ? `, vence el ${vence.slice(8, 10)}/${vence.slice(5, 7)}` : ""}`, responsable: rol("facturacion") }];
42793
+ }
42794
+ if (caso.workflow === "credencial_profesional") {
42795
+ const id = texto2(d.profesional_id);
42796
+ const nombre = texto2(d.profesional) ?? (id ? `Profesional ${id}` : "Profesional");
42797
+ const obs = texto2(d.observacion);
42798
+ const que = `${nombre}: ${(obs && OBSERVACIONES_MATRICULA[obs]) ?? "la matr\xEDcula tiene una observaci\xF3n del registro nacional"}`;
42799
+ return [{ ...base, que, responsable: id ? { clave: `profesional:${id}`, nombre, reserva: "rol:personal" } : rol("personal") }];
42800
+ }
42801
+ return [];
42802
+ }
42803
+ var CLAVE_RE = /^(profesional:[A-Za-z0-9_-]{1,40}|rol:(admision|aranceles|facturacion|internacion|personal))$/;
42804
+ var MAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
42805
+ var COLS_CONTACTO = "id, clave, nombre, mail, whatsapp, activo, updated_at";
42806
+ function mapContacto(r) {
42807
+ return { id: String(r.id), clave: String(r.clave), nombre: String(r.nombre), mail: texto2(r.mail), whatsapp: texto2(r.whatsapp), activo: r.activo !== false, actualizadoEn: iso3(r.updated_at) };
42808
+ }
42809
+ async function listarContactos(ctx, companyId) {
42810
+ const rows = await ctx.db.query(`SELECT ${COLS_CONTACTO} FROM ${t3(ctx, "contacto")} WHERE company_id = $1 ORDER BY nombre ASC`, [companyId]);
42811
+ return rows.map(mapContacto);
42812
+ }
42813
+ function leerWhatsapp(v) {
42814
+ const s = typeof v === "string" ? v.replace(/[\s()+.-]/g, "") : "";
42815
+ if (!s) return null;
42816
+ if (!/^\d{8,15}$/.test(s)) throw new OpsError("validacion", "whatsapp: tiene que ser un n\xFAmero de 8 a 15 d\xEDgitos (con c\xF3digo de pa\xEDs, sin espacios)");
42817
+ return s;
42818
+ }
42819
+ async function guardarContacto(ctx, input2) {
42820
+ if (input2.actor.tipo !== "humano") throw new OpsError("actor_no_permitido", "los contactos los carga una persona de la instituci\xF3n desde el portal");
42821
+ const clave = texto2(input2.clave) ?? "";
42822
+ if (!CLAVE_RE.test(clave)) throw new OpsError("validacion", "clave: tiene que ser profesional:<id> o rol:<admision|aranceles|facturacion|internacion|personal>");
42823
+ const mail = texto2(input2.mail)?.toLowerCase() ?? null;
42824
+ if (mail && !MAIL_RE.test(mail)) throw new OpsError("validacion", "mail: no tiene formato de correo");
42825
+ const whatsapp = leerWhatsapp(input2.whatsapp);
42826
+ const activo = input2.activo !== false;
42827
+ if (activo && !mail && !whatsapp) throw new OpsError("validacion", "hace falta un mail o un WhatsApp para poder avisar");
42828
+ const existente = (await ctx.db.query(`SELECT ${COLS_CONTACTO} FROM ${t3(ctx, "contacto")} WHERE company_id = $1 AND clave = $2 LIMIT 1`, [input2.companyId, clave]))[0];
42829
+ const nombre = texto2(input2.nombre)?.slice(0, 120) ?? (existente ? String(existente.nombre) : clave.startsWith("rol:") ? ROLES[clave.slice(4)] ?? clave : null);
42830
+ if (!nombre) throw new OpsError("validacion", "nombre: es obligatorio para un profesional nuevo");
42831
+ if (existente) {
42832
+ await ctx.db.execute(
42833
+ `UPDATE ${t3(ctx, "contacto")} SET nombre = $3, mail = $4, whatsapp = $5, activo = $6, actor_tipo = $7, actor_id = $8, updated_at = now() WHERE company_id = $1 AND clave = $2`,
42834
+ [input2.companyId, clave, nombre, mail, whatsapp, activo, input2.actor.tipo, input2.actor.id]
42835
+ );
42836
+ return { id: String(existente.id), clave, nombre, mail, whatsapp, activo, actualizadoEn: (/* @__PURE__ */ new Date()).toISOString() };
42837
+ }
42838
+ const id = randomUUID3();
42839
+ await ctx.db.execute(
42840
+ `INSERT INTO ${t3(ctx, "contacto")} (id, company_id, clave, nombre, mail, whatsapp, activo, actor_tipo, actor_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
42841
+ [id, input2.companyId, clave, nombre, mail, whatsapp, activo, input2.actor.tipo, input2.actor.id]
42842
+ );
42843
+ return { id, clave, nombre, mail, whatsapp, activo, actualizadoEn: (/* @__PURE__ */ new Date()).toISOString() };
42844
+ }
42845
+ var COLS_AVISO = "id, clave, nombre, canal, destino, dia, casos, asunto, texto, estado, intentos_envio, error, proveedor_id, enviado_en, created_at";
42846
+ function mapAviso(r) {
42847
+ return {
42848
+ id: String(r.id),
42849
+ clave: String(r.clave),
42850
+ nombre: String(r.nombre),
42851
+ canal: String(r.canal) === "whatsapp" ? "whatsapp" : "mail",
42852
+ destino: String(r.destino),
42853
+ dia: dia(r.dia),
42854
+ casos: lista(r.casos).map((c) => ({ casoId: String(c.casoId), workflow: String(c.workflow), que: String(c.que), intento: Number(c.intento) || 1 })),
42855
+ asunto: String(r.asunto),
42856
+ texto: String(r.texto),
42857
+ estado: ["pendiente", "enviado", "fallido", "cancelado"].includes(String(r.estado)) ? String(r.estado) : "pendiente",
42858
+ intentosEnvio: Number(r.intentos_envio) || 0,
42859
+ error: texto2(r.error),
42860
+ proveedorId: texto2(r.proveedor_id),
42861
+ enviadoEn: iso3(r.enviado_en),
42862
+ creadoEn: iso3(r.created_at)
42863
+ };
42864
+ }
42865
+ async function listarAvisos(ctx, companyId, opciones = {}) {
42866
+ const desde = opciones.desde ?? sumarDias(ahoraEnHospital().hoy, -DIAS_VENTANA);
42867
+ const rows = await ctx.db.query(`SELECT ${COLS_AVISO} FROM ${t3(ctx, "aviso")} WHERE company_id = $1 AND dia >= $2 ORDER BY created_at ASC`, [companyId, desde]);
42868
+ return rows.map(mapAviso);
42869
+ }
42870
+ async function casosQueEsperan(ctx, companyId, workflows) {
42871
+ const out = [];
42872
+ for (const [workflow, estados] of Object.entries(ESPERAN_PERSONA)) {
42873
+ if (workflows && workflows.length > 0 && !workflows.includes(workflow)) continue;
42874
+ out.push(...await listarCasos(ctx, { companyId, workflow, estados, limit: 100 }));
42875
+ }
42876
+ return out;
42877
+ }
42878
+ async function responsablesPendientes(ctx, companyId, workflows) {
42879
+ const contactos = await listarContactos(ctx, companyId);
42880
+ const con = new Map(contactos.filter((c) => c.activo && (c.mail || c.whatsapp)).map((c) => [c.clave, c]));
42881
+ const acumulado = /* @__PURE__ */ new Map();
42882
+ for (const caso of await casosQueEsperan(ctx, companyId, workflows)) {
42883
+ for (const p of pendientesDe(caso)) {
42884
+ const r = acumulado.get(p.responsable.clave) ?? { clave: p.responsable.clave, nombre: p.responsable.nombre, pendientes: 0, tieneContacto: con.has(p.responsable.clave), reserva: p.responsable.reserva };
42885
+ r.pendientes += 1;
42886
+ acumulado.set(r.clave, r);
42887
+ }
42888
+ }
42889
+ const responsables = [...acumulado.values()].sort((a, b) => Number(a.tieneContacto) - Number(b.tieneContacto) || b.pendientes - a.pendientes || a.nombre.localeCompare(b.nombre));
42890
+ return { contactos, responsables };
42891
+ }
42892
+ function esDiaHabil(hoy) {
42893
+ const d = (/* @__PURE__ */ new Date(`${hoy}T12:00:00Z`)).getUTCDay();
42894
+ return d >= 1 && d <= 5;
42895
+ }
42896
+ function enHorarioDeAvisos(hoy, hora) {
42897
+ return esDiaHabil(hoy) && hora >= HORARIO_AVISOS.desde && hora < HORARIO_AVISOS.hasta;
42898
+ }
42899
+ function redactarAviso(nombre, institucion, pendientes) {
42900
+ const n = pendientes.length;
42901
+ const repetidos = pendientes.filter((p) => p.intento > 1).length;
42902
+ const asunto = `${n === 1 ? "1 pendiente" : `${n} pendientes`} para resolver en el HIS \xB7 ${institucion}`;
42903
+ const lineas = pendientes.map((p) => `- ${p.que}${p.intento > 1 ? " (segundo aviso)" : ""}`);
42904
+ const texto4 = [
42905
+ `Hola ${nombre}:`,
42906
+ "",
42907
+ `Desde ${institucion}, Autopilot Opera te avisa que ${n === 1 ? "ten\xE9s 1 pendiente" : `ten\xE9s ${n} pendientes`} para resolver en el HIS:`,
42908
+ "",
42909
+ ...lineas,
42910
+ "",
42911
+ repetidos > 0 ? "Los marcados como segundo aviso ya se avisaron antes; si no se resuelven, pasan a la direcci\xF3n." : "Cuando lo cargues en el HIS, el puesto lo comprueba solo: no hace falta responder este mensaje.",
42912
+ "",
42913
+ "Autopilot Opera \xB7 avisos autom\xE1ticos de la administraci\xF3n"
42914
+ ].join("\n");
42915
+ return { asunto, texto: texto4 };
42916
+ }
42917
+ async function generarAvisos(ctx, input2) {
42918
+ const { hoy, hora } = ahoraEnHospital(input2.ahora);
42919
+ const canales = input2.canales.filter((c) => CANALES_AVISO.includes(c));
42920
+ const todos = await listarAvisos(ctx, input2.companyId);
42921
+ const pendientesHoy = () => todos.filter((a) => a.estado === "pendiente");
42922
+ if (!enHorarioDeAvisos(hoy, hora) || canales.length === 0) return { avisos: pendientesHoy(), generados: 0, sinContacto: 0, fueraDeHorario: true };
42923
+ const contactos = await listarContactos(ctx, input2.companyId);
42924
+ const con = new Map(contactos.filter((c) => c.activo && (c.mail || c.whatsapp)).map((c) => [c.clave, c]));
42925
+ const historial = /* @__PURE__ */ new Map();
42926
+ for (const a of todos) {
42927
+ if (a.estado !== "enviado") continue;
42928
+ for (const c of a.casos) {
42929
+ const k = `${c.casoId}|${a.clave}`;
42930
+ const h = historial.get(k) ?? { veces: 0, ultimo: "" };
42931
+ h.veces += 1;
42932
+ if (a.dia > h.ultimo) h.ultimo = a.dia;
42933
+ historial.set(k, h);
42934
+ }
42935
+ }
42936
+ const yaHoy = new Set(todos.filter((a) => a.dia === hoy && a.estado !== "cancelado").map((a) => a.clave));
42937
+ const porDestinatario = /* @__PURE__ */ new Map();
42938
+ let sinContacto = 0;
42939
+ for (const caso of await casosQueEsperan(ctx, input2.companyId)) {
42940
+ for (const p of pendientesDe(caso)) {
42941
+ const contacto = con.get(p.responsable.clave) ?? (p.responsable.reserva ? con.get(p.responsable.reserva) : void 0);
42942
+ if (!contacto) {
42943
+ sinContacto += 1;
42944
+ continue;
42945
+ }
42946
+ const h = historial.get(`${caso.id}|${contacto.clave}`) ?? { veces: 0, ultimo: "" };
42947
+ if (h.veces >= MAX_AVISOS) continue;
42948
+ if (h.veces > 0 && sumarDias(h.ultimo, DIAS_ENTRE_AVISOS) > hoy) continue;
42949
+ if (yaHoy.has(contacto.clave)) continue;
42950
+ const g = porDestinatario.get(contacto.clave) ?? { contacto, pendientes: [] };
42951
+ g.pendientes.push({ casoId: caso.id, workflow: caso.workflow, que: p.que, intento: h.veces + 1, titulo: caso.titulo });
42952
+ porDestinatario.set(contacto.clave, g);
42953
+ }
42954
+ }
42955
+ let generados = 0;
42956
+ for (const g of porDestinatario.values()) {
42957
+ const canal = g.contacto.mail && canales.includes("mail") ? "mail" : g.contacto.whatsapp && canales.includes("whatsapp") ? "whatsapp" : null;
42958
+ if (!canal) continue;
42959
+ const destino = canal === "mail" ? String(g.contacto.mail) : String(g.contacto.whatsapp);
42960
+ const { asunto, texto: cuerpo } = redactarAviso(g.contacto.nombre, input2.institucion, g.pendientes);
42961
+ const casos = g.pendientes.map(({ casoId, workflow, que, intento }) => ({ casoId, workflow, que, intento }));
42962
+ const id = randomUUID3();
42963
+ await ctx.db.execute(
42964
+ `INSERT INTO ${t3(ctx, "aviso")} (id, company_id, clave, nombre, canal, destino, dia, casos, asunto, texto) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10)`,
42965
+ [id, input2.companyId, g.contacto.clave, g.contacto.nombre, canal, destino, hoy, JSON.stringify(casos), asunto, cuerpo]
42966
+ );
42967
+ todos.push({ id, clave: g.contacto.clave, nombre: g.contacto.nombre, canal, destino, dia: hoy, casos, asunto, texto: cuerpo, estado: "pendiente", intentosEnvio: 0, error: null, proveedorId: null, enviadoEn: null, creadoEn: (/* @__PURE__ */ new Date()).toISOString() });
42968
+ generados += 1;
42969
+ }
42970
+ return { avisos: pendientesHoy(), generados, sinContacto, fueraDeHorario: false };
42971
+ }
42972
+ async function informarResultadoAviso(ctx, input2) {
42973
+ const fila = (await ctx.db.query(`SELECT ${COLS_AVISO} FROM ${t3(ctx, "aviso")} WHERE company_id = $1 AND id = $2 LIMIT 1`, [input2.companyId, input2.avisoId]))[0];
42974
+ if (!fila) throw new OpsError("aviso_inexistente", `no existe el aviso ${input2.avisoId} en esta instituci\xF3n`);
42975
+ const aviso = mapAviso(fila);
42976
+ if (aviso.estado !== "pendiente") throw new OpsError("aviso_cerrado", `el aviso ya est\xE1 ${aviso.estado}`);
42977
+ const actor = { tipo: "sistema", id: input2.actor.id ?? "portal" };
42978
+ if (input2.resultado === "enviado") {
42979
+ await ctx.db.execute(
42980
+ `UPDATE ${t3(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`,
42981
+ [input2.companyId, aviso.id, "enviado", texto2(input2.proveedorId)]
42982
+ );
42983
+ for (const c of aviso.casos) {
42984
+ await registrarAccion(ctx, {
42985
+ companyId: input2.companyId,
42986
+ casoId: c.casoId,
42987
+ tipo: "aviso",
42988
+ conector: aviso.canal,
42989
+ objetivo: `Aviso a ${aviso.nombre} (${aviso.canal === "mail" ? "correo" : "WhatsApp"})`,
42990
+ resultado: "ok",
42991
+ evidencia: { aviso_id: aviso.id, intento: c.intento, destinatario: aviso.clave, pendientes_en_el_aviso: aviso.casos.length, que: c.que },
42992
+ actor
42993
+ });
42994
+ }
42995
+ return { ...aviso, estado: "enviado", proveedorId: texto2(input2.proveedorId), enviadoEn: (/* @__PURE__ */ new Date()).toISOString(), intentosEnvio: aviso.intentosEnvio + 1, error: null };
42996
+ }
42997
+ const intentos = aviso.intentosEnvio + 1;
42998
+ const estado = intentos >= MAX_INTENTOS_ENVIO ? "fallido" : "pendiente";
42999
+ const error62 = texto2(input2.error)?.slice(0, 300) ?? "no se pudo enviar";
43000
+ await ctx.db.execute(
43001
+ `UPDATE ${t3(ctx, "aviso")} SET estado = $3, error = $4, intentos_envio = $5, updated_at = now() WHERE company_id = $1 AND id = $2`,
43002
+ [input2.companyId, aviso.id, estado, error62, intentos]
43003
+ );
43004
+ if (estado === "fallido") {
43005
+ for (const c of aviso.casos) {
43006
+ await registrarAccion(ctx, {
43007
+ companyId: input2.companyId,
43008
+ casoId: c.casoId,
43009
+ tipo: "aviso",
43010
+ conector: aviso.canal,
43011
+ objetivo: `Aviso a ${aviso.nombre} (${aviso.canal === "mail" ? "correo" : "WhatsApp"})`,
43012
+ resultado: "error",
43013
+ evidencia: { aviso_id: aviso.id, intento: c.intento, destinatario: aviso.clave, error: error62 },
43014
+ detalle: `No se pudo enviar despu\xE9s de ${intentos} intentos: ${error62}`,
43015
+ actor
43016
+ });
43017
+ }
43018
+ }
43019
+ return { ...aviso, estado, error: error62, intentosEnvio: intentos };
43020
+ }
43021
+ function hoyEnLaInstitucion() {
43022
+ return ahoraEnHospital().hoy;
43023
+ }
43024
+
42657
43025
  // src/metricas.ts
42658
43026
  var CATALOGO_VERSION = "2026-10-15.3";
42659
43027
  var METRICAS = [
@@ -42681,14 +43049,14 @@ function metricaPorId(id) {
42681
43049
  }
42682
43050
 
42683
43051
  // src/tableros.ts
42684
- import { randomUUID as randomUUID3 } from "node:crypto";
43052
+ import { randomUUID as randomUUID4 } from "node:crypto";
42685
43053
  var TIPOS = ["indicador", "barras", "linea", "tabla", "texto"];
42686
43054
  var TONOS = ["neutro", "ambar", "senal", "corte"];
42687
43055
  var MAX_WIDGETS = 24;
42688
- function t3(ctx, table) {
43056
+ function t4(ctx, table) {
42689
43057
  return `${ctx.db.namespace}.${table}`;
42690
43058
  }
42691
- function iso3(v) {
43059
+ function iso4(v) {
42692
43060
  if (v instanceof Date) return v.toISOString();
42693
43061
  if (typeof v === "string") return v;
42694
43062
  return "";
@@ -42705,7 +43073,7 @@ function arr(v) {
42705
43073
  }
42706
43074
  return [];
42707
43075
  }
42708
- function texto2(v, max, campo, problemas, obligatorio = false) {
43076
+ function texto3(v, max, campo, problemas, obligatorio = false) {
42709
43077
  if (v === void 0 || v === null || v === "") {
42710
43078
  if (obligatorio) problemas.push(`${campo} es obligatorio`);
42711
43079
  return void 0;
@@ -42749,11 +43117,11 @@ function leerCalculo(v, ref, problemas) {
42749
43117
  return { origen: "exploratorio" };
42750
43118
  }
42751
43119
  const o = v;
42752
- const lista = arr(o.metricas);
42753
- if (lista.length === 0) problemas.push(`${ref}.calculo.metricas est\xE1 vac\xEDo`);
43120
+ const lista2 = arr(o.metricas);
43121
+ if (lista2.length === 0) problemas.push(`${ref}.calculo.metricas est\xE1 vac\xEDo`);
42754
43122
  let propuesta = false;
42755
43123
  const metricas = [];
42756
- lista.slice(0, 12).forEach((m, j) => {
43124
+ lista2.slice(0, 12).forEach((m, j) => {
42757
43125
  const mo = m && typeof m === "object" ? m : {};
42758
43126
  const id = typeof mo.id === "string" ? mo.id : "";
42759
43127
  const version2 = Number(mo.version);
@@ -42801,11 +43169,11 @@ function controlarTabla(ref, columnas, filas, problemas) {
42801
43169
  }
42802
43170
  function validarWidgets(entrada) {
42803
43171
  const problemas = [];
42804
- const lista = Array.isArray(entrada) ? entrada : [];
42805
- if (!Array.isArray(entrada) || lista.length === 0) problemas.push("widgets debe ser una lista con al menos un elemento");
42806
- if (lista.length > MAX_WIDGETS) problemas.push(`widgets admite hasta ${MAX_WIDGETS} elementos`);
43172
+ const lista2 = Array.isArray(entrada) ? entrada : [];
43173
+ if (!Array.isArray(entrada) || lista2.length === 0) problemas.push("widgets debe ser una lista con al menos un elemento");
43174
+ if (lista2.length > MAX_WIDGETS) problemas.push(`widgets admite hasta ${MAX_WIDGETS} elementos`);
42807
43175
  const salida = [];
42808
- lista.slice(0, MAX_WIDGETS).forEach((w, i) => {
43176
+ lista2.slice(0, MAX_WIDGETS).forEach((w, i) => {
42809
43177
  const ref = `widgets[${i}]`;
42810
43178
  const o = w && typeof w === "object" && !Array.isArray(w) ? w : {};
42811
43179
  const tipo = o.tipo;
@@ -42816,24 +43184,24 @@ function validarWidgets(entrada) {
42816
43184
  const a = ancho(o.ancho);
42817
43185
  switch (tipo) {
42818
43186
  case "indicador": {
42819
- const etiqueta = texto2(o.etiqueta, 80, `${ref}.etiqueta`, problemas, true) ?? "";
42820
- const valor = typeof o.valor === "number" && Number.isFinite(o.valor) ? o.valor : texto2(o.valor, 40, `${ref}.valor`, problemas, true) ?? "";
43187
+ const etiqueta = texto3(o.etiqueta, 80, `${ref}.etiqueta`, problemas, true) ?? "";
43188
+ const valor = typeof o.valor === "number" && Number.isFinite(o.valor) ? o.valor : texto3(o.valor, 40, `${ref}.valor`, problemas, true) ?? "";
42821
43189
  const tono = typeof o.tono === "string" && TONOS.includes(o.tono) ? o.tono : void 0;
42822
43190
  if (o.tono !== void 0 && !tono) problemas.push(`${ref}.tono debe ser uno de ${TONOS.join(", ")}`);
42823
- const unidad = texto2(o.unidad, 16, `${ref}.unidad`, problemas);
43191
+ const unidad = texto3(o.unidad, 16, `${ref}.unidad`, problemas);
42824
43192
  if (unidad === "%" && !ES_VARIACION.test(etiqueta)) controlarPorcentaje(valor, `${ref}.valor`, problemas);
42825
43193
  const { calculo, origen } = leerCalculo(o.calculo, ref, problemas);
42826
- salida.push({ tipo, etiqueta, valor, unidad, contexto: texto2(o.contexto, 160, `${ref}.contexto`, problemas), tono, ancho: a, ...calculo ? { calculo } : {}, origen });
43194
+ salida.push({ tipo, etiqueta, valor, unidad, contexto: texto3(o.contexto, 160, `${ref}.contexto`, problemas), tono, ancho: a, ...calculo ? { calculo } : {}, origen });
42827
43195
  break;
42828
43196
  }
42829
43197
  case "barras": {
42830
- const titulo = texto2(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
43198
+ const titulo = texto3(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
42831
43199
  const series = arr(o.series);
42832
43200
  if (series.length === 0 || series.length > 40) problemas.push(`${ref}.series debe tener entre 1 y 40 barras`);
42833
- const unidad = texto2(o.unidad, 16, `${ref}.unidad`, problemas);
43201
+ const unidad = texto3(o.unidad, 16, `${ref}.unidad`, problemas);
42834
43202
  const barras = series.slice(0, 40).map((s, j) => {
42835
43203
  const so = s && typeof s === "object" ? s : {};
42836
- return { etiqueta: texto2(so.etiqueta, 60, `${ref}.series[${j}].etiqueta`, problemas, true) ?? "", valor: numero2(so.valor, `${ref}.series[${j}].valor`, problemas) };
43204
+ return { etiqueta: texto3(so.etiqueta, 60, `${ref}.series[${j}].etiqueta`, problemas, true) ?? "", valor: numero2(so.valor, `${ref}.series[${j}].valor`, problemas) };
42837
43205
  });
42838
43206
  if (unidad === "%" && !ES_VARIACION.test(titulo)) barras.forEach((b, j) => controlarPorcentaje(b.valor, `${ref}.series[${j}].valor`, problemas));
42839
43207
  const { calculo, origen } = leerCalculo(o.calculo, ref, problemas);
@@ -42841,13 +43209,13 @@ function validarWidgets(entrada) {
42841
43209
  break;
42842
43210
  }
42843
43211
  case "linea": {
42844
- const titulo = texto2(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
43212
+ const titulo = texto3(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
42845
43213
  const puntos = arr(o.puntos);
42846
43214
  if (puntos.length === 0 || puntos.length > 400) problemas.push(`${ref}.puntos debe tener entre 1 y 400 puntos`);
42847
- const unidad = texto2(o.unidad, 16, `${ref}.unidad`, problemas);
43215
+ const unidad = texto3(o.unidad, 16, `${ref}.unidad`, problemas);
42848
43216
  const serie2 = puntos.slice(0, 400).map((p, j) => {
42849
43217
  const po = p && typeof p === "object" ? p : {};
42850
- return { x: texto2(po.x, 40, `${ref}.puntos[${j}].x`, problemas, true) ?? "", y: numero2(po.y, `${ref}.puntos[${j}].y`, problemas) };
43218
+ return { x: texto3(po.x, 40, `${ref}.puntos[${j}].x`, problemas, true) ?? "", y: numero2(po.y, `${ref}.puntos[${j}].y`, problemas) };
42851
43219
  });
42852
43220
  if (unidad === "%" && !ES_VARIACION.test(titulo)) serie2.forEach((p, j) => controlarPorcentaje(p.y, `${ref}.puntos[${j}].y`, problemas));
42853
43221
  const { calculo, origen } = leerCalculo(o.calculo, ref, problemas);
@@ -42855,12 +43223,12 @@ function validarWidgets(entrada) {
42855
43223
  break;
42856
43224
  }
42857
43225
  case "tabla": {
42858
- const titulo = texto2(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
43226
+ const titulo = texto3(o.titulo, 120, `${ref}.titulo`, problemas, true) ?? "";
42859
43227
  const columnas = arr(o.columnas);
42860
43228
  if (columnas.length === 0 || columnas.length > 12) problemas.push(`${ref}.columnas debe tener entre 1 y 12 columnas`);
42861
43229
  const filas = arr(o.filas);
42862
43230
  if (filas.length > 200) problemas.push(`${ref}.filas admite hasta 200 filas`);
42863
- const cols = columnas.slice(0, 12).map((c, j) => texto2(c, 60, `${ref}.columnas[${j}]`, problemas, true) ?? "");
43231
+ const cols = columnas.slice(0, 12).map((c, j) => texto3(c, 60, `${ref}.columnas[${j}]`, problemas, true) ?? "");
42864
43232
  const celdasTabla = filas.slice(0, 200).map((f, j) => {
42865
43233
  const celdas = arr(f);
42866
43234
  if (celdas.length !== cols.length) problemas.push(`${ref}.filas[${j}] tiene ${celdas.length} celdas y la tabla ${cols.length} columnas`);
@@ -42876,7 +43244,7 @@ function validarWidgets(entrada) {
42876
43244
  break;
42877
43245
  }
42878
43246
  case "texto": {
42879
- salida.push({ tipo, titulo: texto2(o.titulo, 120, `${ref}.titulo`, problemas), texto: texto2(o.texto, 4e3, `${ref}.texto`, problemas, true) ?? "", ancho: a });
43247
+ salida.push({ tipo, titulo: texto3(o.titulo, 120, `${ref}.titulo`, problemas), texto: texto3(o.texto, 4e3, `${ref}.texto`, problemas, true) ?? "", ancho: a });
42880
43248
  break;
42881
43249
  }
42882
43250
  }
@@ -42900,23 +43268,23 @@ function mapTablero(r) {
42900
43268
  publicadoPorTipo: String(r.publicado_por_tipo),
42901
43269
  publicadoPorId: r.publicado_por_id ?? null,
42902
43270
  versiones: Number(r.versiones ?? 1),
42903
- createdAt: iso3(r.created_at),
42904
- updatedAt: iso3(r.updated_at)
43271
+ createdAt: iso4(r.created_at),
43272
+ updatedAt: iso4(r.updated_at)
42905
43273
  };
42906
43274
  }
42907
43275
  var COLS2 = "id, company_id, clave, titulo, descripcion, pedido, periodo, fuentes, widgets, agente_id, issue_id, publicado_por_tipo, publicado_por_id, versiones, created_at, updated_at";
42908
- var CLAVE_RE = /^[a-z0-9][a-z0-9-]{1,60}$/;
43276
+ var CLAVE_RE2 = /^[a-z0-9][a-z0-9-]{1,60}$/;
42909
43277
  async function listarTableros(ctx, companyId) {
42910
- const rows = await ctx.db.query(`SELECT ${COLS2} FROM ${t3(ctx, "tablero")} WHERE company_id = $1 ORDER BY updated_at DESC`, [companyId]);
43278
+ const rows = await ctx.db.query(`SELECT ${COLS2} FROM ${t4(ctx, "tablero")} WHERE company_id = $1 ORDER BY updated_at DESC`, [companyId]);
42911
43279
  return rows.map(mapTablero);
42912
43280
  }
42913
43281
  async function leerTablero(ctx, companyId, clave) {
42914
- const rows = await ctx.db.query(`SELECT ${COLS2} FROM ${t3(ctx, "tablero")} WHERE company_id = $1 AND clave = $2 LIMIT 1`, [companyId, clave]);
43282
+ const rows = await ctx.db.query(`SELECT ${COLS2} FROM ${t4(ctx, "tablero")} WHERE company_id = $1 AND clave = $2 LIMIT 1`, [companyId, clave]);
42915
43283
  return rows[0] ? mapTablero(rows[0]) : null;
42916
43284
  }
42917
43285
  async function publicarTablero(ctx, input2) {
42918
43286
  const clave = typeof input2.clave === "string" ? input2.clave.trim().toLowerCase() : "";
42919
- if (!CLAVE_RE.test(clave)) throw new OpsError("clave_invalida", "clave debe ser un identificador estable en min\xFAsculas (letras, n\xFAmeros y guiones, 2 a 61 caracteres), p. ej. turnos-ausentismo");
43287
+ if (!CLAVE_RE2.test(clave)) throw new OpsError("clave_invalida", "clave debe ser un identificador estable en min\xFAsculas (letras, n\xFAmeros y guiones, 2 a 61 caracteres), p. ej. turnos-ausentismo");
42920
43288
  const titulo = typeof input2.titulo === "string" ? input2.titulo.trim().slice(0, 140) : "";
42921
43289
  if (!titulo) throw new OpsError("titulo_requerido", "titulo es obligatorio");
42922
43290
  const widgets = validarWidgets(input2.widgets);
@@ -42926,7 +43294,7 @@ async function publicarTablero(ctx, input2) {
42926
43294
  const now = (/* @__PURE__ */ new Date()).toISOString();
42927
43295
  if (previo) {
42928
43296
  await ctx.db.execute(
42929
- `UPDATE ${t3(ctx, "tablero")} SET titulo = $3, descripcion = $4, pedido = COALESCE($5, pedido), periodo = $6, fuentes = $7::jsonb, widgets = $8::jsonb, agente_id = COALESCE($9, agente_id), issue_id = COALESCE($10, issue_id), publicado_por_tipo = $11, publicado_por_id = $12, versiones = versiones + 1, updated_at = now() WHERE company_id = $1 AND clave = $2`,
43297
+ `UPDATE ${t4(ctx, "tablero")} SET titulo = $3, descripcion = $4, pedido = COALESCE($5, pedido), periodo = $6, fuentes = $7::jsonb, widgets = $8::jsonb, agente_id = COALESCE($9, agente_id), issue_id = COALESCE($10, issue_id), publicado_por_tipo = $11, publicado_por_id = $12, versiones = versiones + 1, updated_at = now() WHERE company_id = $1 AND clave = $2`,
42930
43298
  [input2.companyId, clave, titulo, limpio(input2.descripcion, 600), limpio(input2.pedido, 1e3), limpio(input2.periodo, 80), JSON.stringify(fuentes), JSON.stringify(widgets), input2.agenteId ?? null, input2.issueId ?? null, input2.actor.tipo, input2.actor.id]
42931
43299
  );
42932
43300
  await ctx.activity.log({ companyId: input2.companyId, message: `Autopilot Care: tablero \xAB${titulo}\xBB actualizado (${clave}, versi\xF3n ${previo.versiones + 1})`, metadata: { clave, widgets: widgets.length, agentId: input2.agenteId ?? null, issueId: input2.issueId ?? null } });
@@ -42935,9 +43303,9 @@ async function publicarTablero(ctx, input2) {
42935
43303
  tablero: { ...previo, titulo, descripcion: limpio(input2.descripcion, 600), pedido: limpio(input2.pedido, 1e3) ?? previo.pedido, periodo: limpio(input2.periodo, 80), fuentes, widgets, agenteId: input2.agenteId ?? previo.agenteId, issueId: input2.issueId ?? previo.issueId, publicadoPorTipo: input2.actor.tipo, publicadoPorId: input2.actor.id, versiones: previo.versiones + 1, updatedAt: now }
42936
43304
  };
42937
43305
  }
42938
- const id = randomUUID3();
43306
+ const id = randomUUID4();
42939
43307
  await ctx.db.execute(
42940
- `INSERT INTO ${t3(ctx, "tablero")} (id, company_id, clave, titulo, descripcion, pedido, periodo, fuentes, widgets, agente_id, issue_id, publicado_por_tipo, publicado_por_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, $10, $11, $12, $13)`,
43308
+ `INSERT INTO ${t4(ctx, "tablero")} (id, company_id, clave, titulo, descripcion, pedido, periodo, fuentes, widgets, agente_id, issue_id, publicado_por_tipo, publicado_por_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, $10, $11, $12, $13)`,
42941
43309
  [id, input2.companyId, clave, titulo, limpio(input2.descripcion, 600), limpio(input2.pedido, 1e3), limpio(input2.periodo, 80), JSON.stringify(fuentes), JSON.stringify(widgets), input2.agenteId ?? null, input2.issueId ?? null, input2.actor.tipo, input2.actor.id]
42942
43310
  );
42943
43311
  await ctx.activity.log({ companyId: input2.companyId, message: `Autopilot Care: tablero \xAB${titulo}\xBB publicado (${clave})`, metadata: { clave, widgets: widgets.length, agentId: input2.agenteId ?? null, issueId: input2.issueId ?? null } });
@@ -42968,7 +43336,7 @@ function companyFrom(params, fallback) {
42968
43336
  return companyId;
42969
43337
  }
42970
43338
  function toolSchema(name) {
42971
- const decl = manifest_default.tools?.find((t4) => t4.name === name);
43339
+ const decl = manifest_default.tools?.find((t5) => t5.name === name);
42972
43340
  return {
42973
43341
  displayName: decl?.displayName ?? name,
42974
43342
  description: decl?.description ?? "",
@@ -43129,7 +43497,7 @@ function registrarDatosYAcciones(ctx) {
43129
43497
  finalesOk: w.finalesOk,
43130
43498
  finalesNoOk: w.finalesNoOk,
43131
43499
  minutosAbsorbidos: w.minutosAbsorbidos,
43132
- transiciones: w.transiciones.map((t4) => ({ via: t4.via, de: t4.de, a: typeof t4.a === "string" ? t4.a : "(seg\xFAn datos)", actor: t4.actor, nivelRequerido: t4.nivelRequerido ?? null, descripcion: t4.descripcion }))
43500
+ transiciones: w.transiciones.map((t5) => ({ via: t5.via, de: t5.de, a: typeof t5.a === "string" ? t5.a : "(seg\xFAn datos)", actor: t5.actor, nivelRequerido: t5.nivelRequerido ?? null, descripcion: t5.descripcion }))
43133
43501
  })));
43134
43502
  ctx.data.register(DATA_KEYS.tablero, async (params) => tablero(ctx, companyFrom(params), numOrNull(params.dias)));
43135
43503
  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) }));
@@ -43217,7 +43585,7 @@ async function manejarApi(ctx, input2) {
43217
43585
  return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "no_corresponde", motivo: motivoConPersona(str3(body.motivo)), datos: conPersona(obj(body.datos)), idempotencyKey: str3(body.idempotencyKey), actor }));
43218
43586
  }
43219
43587
  case "version":
43220
- return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos", "ofrecer-primero", "cuenta-completa", "metricas-certificadas", "facturacion-pendiente", "credenciales", "demanda-pendiente", "reglas"], workflows: listarWorkflows().map((w) => w.clave) });
43588
+ return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos", "ofrecer-primero", "cuenta-completa", "metricas-certificadas", "facturacion-pendiente", "credenciales", "demanda-pendiente", "reglas", "avisos"], workflows: listarWorkflows().map((w) => w.clave) });
43221
43589
  case "politicas":
43222
43590
  return respuesta(200, { politicas: await listarPoliticas(ctx, companyId) });
43223
43591
  case "serie":
@@ -43253,6 +43621,21 @@ async function manejarApi(ctx, input2) {
43253
43621
  }
43254
43622
  case "contacto":
43255
43623
  return respuesta(200, await registrarContacto(ctx, { companyId, casoId: input2.params.casoId ?? "", canal: body.canal, respuesta: body.respuesta, comentario: str3(body.comentario), persona: decididoPor, idempotencyKey: str3(body.idempotencyKey), actor }));
43624
+ case "contactos":
43625
+ return respuesta(200, await responsablesPendientes(ctx, companyId, (qs("workflows") ?? "").split(",").map((w) => w.trim()).filter(Boolean)));
43626
+ case "guardar-contacto": {
43627
+ if (actor.tipo !== "humano") return respuesta(403, { error: "actor_no_permitido", motivo: "los contactos los carga una persona de la instituci\xF3n" });
43628
+ return respuesta(200, { contacto: await guardarContacto(ctx, { companyId, clave: str3(body.clave) ?? "", nombre: str3(body.nombre), mail: str3(body.mail), whatsapp: str3(body.whatsapp), activo: body.activo !== false, actor: { tipo: "humano", id: decididoPor ?? actor.id } }) });
43629
+ }
43630
+ case "avisos-pendientes":
43631
+ return respuesta(200, await generarAvisos(ctx, { companyId, institucion: qs("institucion") ?? "la instituci\xF3n", canales: (qs("canales") ?? "").split(",").map((c) => c.trim()).filter(Boolean) }));
43632
+ case "avisos":
43633
+ return respuesta(200, { avisos: await listarAvisos(ctx, companyId, { desde: qs("desde") }), hoy: hoyEnLaInstitucion() });
43634
+ case "resultado-aviso": {
43635
+ const resultado = body.resultado === "enviado" ? "enviado" : body.resultado === "fallido" ? "fallido" : null;
43636
+ if (!resultado) return respuesta(400, { error: "resultado_invalido", detalle: "resultado debe ser enviado o fallido" });
43637
+ return respuesta(200, { aviso: await informarResultadoAviso(ctx, { companyId, avisoId: input2.params.avisoId ?? "", resultado, proveedorId: str3(body.proveedorId), error: str3(body.error), actor }) });
43638
+ }
43256
43639
  case "cambiar-politica": {
43257
43640
  const workflow = str3(body.workflow);
43258
43641
  if (!workflow) return respuesta(400, { error: "workflow_requerido" });
@@ -0,0 +1,44 @@
1
+ -- Avisos a responsables: cada faltante que encuentra un puesto le llega por mensaje a quien lo
2
+ -- puede resolver (médico, admisión, facturación), con contactos que carga la institución en Opera.
3
+ -- Ver docs/AVISOS-A-RESPONSABLES.md.
4
+
5
+ -- Contactos por institución: una persona (profesional:<id del HIS>) o un rol (rol:admision).
6
+ CREATE TABLE plugin_autopilot_ops_1a68482ede.contacto (
7
+ id uuid PRIMARY KEY,
8
+ company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
9
+ clave text NOT NULL,
10
+ nombre text NOT NULL,
11
+ mail text,
12
+ whatsapp text,
13
+ activo boolean NOT NULL DEFAULT true,
14
+ actor_tipo text NOT NULL,
15
+ actor_id text,
16
+ created_at timestamptz NOT NULL DEFAULT now(),
17
+ updated_at timestamptz NOT NULL DEFAULT now(),
18
+ UNIQUE (company_id, clave)
19
+ );
20
+
21
+ -- Un aviso: un mensaje a un destinatario con todos sus pendientes del día. Lo genera el plugin,
22
+ -- lo envía el portal y el plugin registra el resultado en cada caso incluido.
23
+ CREATE TABLE plugin_autopilot_ops_1a68482ede.aviso (
24
+ id uuid PRIMARY KEY,
25
+ company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
26
+ clave text NOT NULL,
27
+ nombre text NOT NULL,
28
+ canal text NOT NULL,
29
+ destino text NOT NULL,
30
+ dia date NOT NULL,
31
+ casos jsonb NOT NULL DEFAULT '[]'::jsonb,
32
+ asunto text NOT NULL,
33
+ texto text NOT NULL,
34
+ estado text NOT NULL DEFAULT 'pendiente',
35
+ intentos_envio integer NOT NULL DEFAULT 0,
36
+ error text,
37
+ proveedor_id text,
38
+ enviado_en timestamptz,
39
+ created_at timestamptz NOT NULL DEFAULT now(),
40
+ updated_at timestamptz NOT NULL DEFAULT now()
41
+ );
42
+
43
+ CREATE INDEX aviso_company_dia_idx
44
+ ON plugin_autopilot_ops_1a68482ede.aviso (company_id, dia, estado);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-plugin-autopilot-ops",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
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": [