pixelon 0.4.3 → 0.5.1

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/index.js CHANGED
@@ -10945,6 +10945,7 @@ const Drawer = ({
10945
10945
  "data-testid": dataTestId,
10946
10946
  scrollIndicatorOffset = 1
10947
10947
  }) => {
10948
+ const idTitulo = React.useId();
10948
10949
  const [isMobile, setIsMobile] = React.useState(false);
10949
10950
  const contentRef = React.useRef(null);
10950
10951
  const [showScrollIndicator, setShowScrollIndicator] = React.useState(false);
@@ -11068,14 +11069,14 @@ const Drawer = ({
11068
11069
  style: getDrawerStyles(),
11069
11070
  role: "dialog",
11070
11071
  "aria-modal": "true",
11071
- "aria-labelledby": title ? "drawer-title" : void 0,
11072
+ "aria-labelledby": title ? idTitulo : void 0,
11072
11073
  "data-testid": dataTestId,
11073
11074
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${COMPONENT_PREFIX}drawer__viewport`, children: [
11074
11075
  (title || showCloseButton || headerExtra) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${COMPONENT_PREFIX}drawer__header`, children: [
11075
11076
  title && /* @__PURE__ */ jsxRuntime.jsx(
11076
11077
  "h2",
11077
11078
  {
11078
- id: "drawer-title",
11079
+ id: idTitulo,
11079
11080
  className: `${COMPONENT_PREFIX}drawer__title`,
11080
11081
  children: title
11081
11082
  }
@@ -12838,29 +12839,22 @@ const FormDrawer = ({
12838
12839
  onClose();
12839
12840
  methods.reset();
12840
12841
  }, [onClose, onBeforeClose, methods]);
12841
- const [invalidMsg, setInvalidMsg] = React.useState(null);
12842
- React.useEffect(() => {
12843
- if (!isOpen) setInvalidMsg(null);
12844
- }, [isOpen]);
12845
- React.useEffect(() => {
12846
- const sub = methods.watch(() => setInvalidMsg(null));
12847
- return () => sub.unsubscribe();
12848
- }, [methods.watch]);
12849
12842
  const handleFormSubmit = React.useCallback(
12850
12843
  async (data) => {
12851
- setInvalidMsg(null);
12852
12844
  await onSubmit(data);
12853
12845
  },
12854
12846
  [onSubmit]
12855
12847
  );
12856
12848
  const handleInvalid = React.useCallback(
12857
12849
  (formErrors) => {
12858
- const faltan = Object.keys(formErrors).map((name) => fields.find((f) => f.name === name)?.label ?? name);
12859
- setInvalidMsg(
12860
- faltan.length > 0 ? `Falta completar: ${faltan.join(", ")}.` : "Revisa los campos marcados."
12861
- );
12850
+ const primero = Object.keys(formErrors)[0];
12851
+ if (!primero) return;
12852
+ const nodo = document.querySelector(`[name="${CSS.escape(primero)}"]`);
12853
+ if (!nodo) return;
12854
+ nodo.scrollIntoView({ block: "center", behavior: "smooth" });
12855
+ window.setTimeout(() => nodo.focus({ preventScroll: true }), 300);
12862
12856
  },
12863
- [fields]
12857
+ []
12864
12858
  );
12865
12859
  const handleSubmitClick = methods.handleSubmit(handleFormSubmit, handleInvalid);
12866
12860
  const drawerTitle = title || DEFAULT_TITLES[mode];
@@ -12918,7 +12912,6 @@ const FormDrawer = ({
12918
12912
  ) }) }),
12919
12913
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${baseClass}__footer`, children: [
12920
12914
  footerExtra && /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${baseClass}__footer-extra`, children: footerExtra }),
12921
- invalidMsg && mode !== "view" && /* @__PURE__ */ jsxRuntime.jsx("p", { className: `${baseClass}__invalid`, role: "alert", children: invalidMsg }),
12922
12915
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${baseClass}__actions`, children: [
12923
12916
  /* @__PURE__ */ jsxRuntime.jsx(
12924
12917
  Button,
@@ -13196,7 +13189,9 @@ function mapFieldsToFormFields(fields, mode) {
13196
13189
  readOnly: field.readOnly,
13197
13190
  placeholder: field.placeholder,
13198
13191
  uiProps: field.uiProps,
13199
- transformOnSubmit: field.parseValue
13192
+ transformOnSubmit: field.parseValue,
13193
+ gridSpan: field.gridSpan
13194
+ // SPEC-131 T7
13200
13195
  }));
13201
13196
  }
13202
13197
 
@@ -13376,6 +13371,47 @@ function calculateTotalPages(totalRecords, limit) {
13376
13371
  return Math.ceil(totalRecords / limit);
13377
13372
  }
13378
13373
 
13374
+ function etiquetaDeOpcion(valor, uiProps) {
13375
+ const opciones = uiProps?.options;
13376
+ if (!Array.isArray(opciones)) return null;
13377
+ const encontrada = opciones.find((o) => o && String(o.value) === String(valor));
13378
+ return encontrada?.label != null ? String(encontrada.label) : null;
13379
+ }
13380
+ function esNumerico(moleculeType) {
13381
+ return moleculeType === "INPUT_CURRENCY" || moleculeType === "INPUT_NUMBER";
13382
+ }
13383
+ function aTexto(valor, uiProps, moleculeType) {
13384
+ if (valor == null || valor === "") return "";
13385
+ if (typeof valor === "boolean") return valor ? "S\xED" : "No";
13386
+ if (esNumerico(moleculeType)) {
13387
+ const n = Number(String(valor).replace(/[^\d.-]/g, ""));
13388
+ if (Number.isFinite(n)) return n.toLocaleString();
13389
+ }
13390
+ if (typeof File !== "undefined" && valor instanceof File) return valor.name;
13391
+ if (Array.isArray(valor)) {
13392
+ const partes = valor.map((v) => aTexto(v, uiProps, moleculeType)).filter(Boolean);
13393
+ return partes.join(", ");
13394
+ }
13395
+ if (typeof valor === "object") {
13396
+ const o = valor;
13397
+ for (const clave of ["label", "address", "name", "text", "value"]) {
13398
+ if (typeof o[clave] === "string" && o[clave]) return o[clave];
13399
+ }
13400
+ return "";
13401
+ }
13402
+ return etiquetaDeOpcion(valor, uiProps) ?? String(valor);
13403
+ }
13404
+ function resumenParaConfirmar(campos, datos) {
13405
+ if (!datos) return [];
13406
+ const lineas = [];
13407
+ for (const campo of campos) {
13408
+ const valor = aTexto(datos[campo.name], campo.uiProps, campo.moleculeType);
13409
+ if (!valor) continue;
13410
+ lineas.push({ label: campo.label ?? campo.name, valor });
13411
+ }
13412
+ return lineas;
13413
+ }
13414
+
13379
13415
  const useAnchoPropio = () => {
13380
13416
  const ref = React.useRef(null);
13381
13417
  const [ancho, setAncho] = React.useState(null);
@@ -13820,6 +13856,10 @@ function Crud({
13820
13856
  return "Formulario";
13821
13857
  }
13822
13858
  };
13859
+ const resumenConfirmacion = React.useMemo(
13860
+ () => confirmAction === "create" || confirmAction === "edit" ? resumenParaConfirmar(formFields, pendingFormData) : [],
13861
+ [confirmAction, formFields, pendingFormData]
13862
+ );
13823
13863
  const getConfirmMessage = () => {
13824
13864
  switch (confirmAction) {
13825
13865
  case "create":
@@ -13959,6 +13999,10 @@ function Crud({
13959
13999
  className: `${COMPONENT_PREFIX}crud__confirm-drawer`,
13960
14000
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${COMPONENT_PREFIX}crud__confirm-content`, children: [
13961
14001
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: `${COMPONENT_PREFIX}crud__confirm-message`, children: getConfirmMessage() }),
14002
+ resumenConfirmacion.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("dl", { className: `${COMPONENT_PREFIX}crud__confirm-summary`, children: resumenConfirmacion.map((linea) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${COMPONENT_PREFIX}crud__confirm-summary-row`, children: [
14003
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: linea.label }),
14004
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: linea.valor })
14005
+ ] }, linea.label)) }),
13962
14006
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${COMPONENT_PREFIX}crud__confirm-actions`, children: [
13963
14007
  /* @__PURE__ */ jsxRuntime.jsx(
13964
14008
  Button,
@@ -14133,7 +14177,7 @@ function getFormatter(key) {
14133
14177
  }
14134
14178
 
14135
14179
  const OPTIONS_LOAD_ERROR = "No se pudieron cargar las opciones. Int\xE9ntalo de nuevo.";
14136
- function unwrap$3(raw) {
14180
+ function unwrap$4(raw) {
14137
14181
  if (raw && typeof raw === "object" && "response" in raw) {
14138
14182
  return raw.response;
14139
14183
  }
@@ -14154,7 +14198,7 @@ function resolveBody(body, ctx) {
14154
14198
  }
14155
14199
  async function loadOptions(eventBus, ep, ctx) {
14156
14200
  const raw = await fetchViaEventBus(eventBus, ep.service, ep.url, ep.method, resolveBody(ep.body, ctx));
14157
- let data = unwrap$3(raw);
14201
+ let data = unwrap$4(raw);
14158
14202
  if (ep.response_path && data && typeof data === "object" && !Array.isArray(data)) {
14159
14203
  data = data[ep.response_path];
14160
14204
  }
@@ -14192,7 +14236,7 @@ const mensajeDeError = (e, respaldo) => {
14192
14236
  };
14193
14237
 
14194
14238
  const REFRESH_INTERVAL_MS = 2e4;
14195
- function unwrap$2(raw) {
14239
+ function unwrap$3(raw) {
14196
14240
  if (raw && typeof raw === "object" && "response" in raw) {
14197
14241
  return raw.response;
14198
14242
  }
@@ -14241,7 +14285,7 @@ function useReactiveCrud(config, eventBus) {
14241
14285
  filters
14242
14286
  });
14243
14287
  assertOk(raw);
14244
- const data = unwrap$2(raw);
14288
+ const data = unwrap$3(raw);
14245
14289
  let rows = [];
14246
14290
  let total = 0;
14247
14291
  if (Array.isArray(data)) {
@@ -14445,6 +14489,20 @@ const FileField = ({
14445
14489
  ] });
14446
14490
  };
14447
14491
 
14492
+ const RESPALDO = { miles: ".", decimal: "," };
14493
+ function separadoresDelLocale(locale) {
14494
+ if (!locale) return RESPALDO;
14495
+ try {
14496
+ const partes = new Intl.NumberFormat(locale).formatToParts(123456789e-2);
14497
+ const miles = partes.find((p) => p.type === "group")?.value;
14498
+ const decimal = partes.find((p) => p.type === "decimal")?.value;
14499
+ if (!miles || !decimal) return RESPALDO;
14500
+ return { miles, decimal };
14501
+ } catch {
14502
+ return RESPALDO;
14503
+ }
14504
+ }
14505
+
14448
14506
  const CRUD_TO_FORM_MOLECULE = {
14449
14507
  INPUT: "INPUT",
14450
14508
  INPUT_NUMBER: "INPUT_NUMBER",
@@ -14501,6 +14559,8 @@ function ActionFormDrawer({ action, row, idField, eventBus, onExecute, onClose }
14501
14559
  alive = false;
14502
14560
  };
14503
14561
  }, [eventBus, action.key, normalFields]);
14562
+ const { locale } = useChatConfig();
14563
+ const separadores = React.useMemo(() => separadoresDelLocale(locale), [locale]);
14504
14564
  const fields = React.useMemo(
14505
14565
  () => normalFields.map((f) => ({
14506
14566
  name: f.name,
@@ -14511,9 +14571,19 @@ function ActionFormDrawer({ action, row, idField, eventBus, onExecute, onClose }
14511
14571
  options: f.options ?? optionsByField[f.name],
14512
14572
  // SPEC-106 T8 — `uiProps` llega tal cual a la molécula, así que el desplegable dice lo
14513
14573
  // que pasó de verdad en vez del «no hay» de siempre. Sin error, se queda el de siempre.
14514
- ...optionsErrorByField[f.name] ? { uiProps: { notFoundContent: optionsErrorByField[f.name] } } : {}
14574
+ ...optionsErrorByField[f.name] ? { uiProps: { notFoundContent: optionsErrorByField[f.name] } } : {},
14575
+ // SPEC-128 — el dinero se escribe como se escribe AQUÍ.
14576
+ //
14577
+ // Los campos de moneda caían a `thousandSeparator ?? ','`, un valor por defecto en formato
14578
+ // inglés: al teclear 180000 el campo mostraba «180,000» mientras la tarjeta que recibe el
14579
+ // cliente decía «$ 180.000,00 COP». El mismo importe escrito de una manera y leído de otra.
14580
+ //
14581
+ // Se deriva del locale que el host ya declara en la config del chat, no de un valor fijo ni
14582
+ // del navegador: `navigator.language` puede ser `es-419` —español, pero con coma para los
14583
+ // miles— y entonces el resultado dependería del portátil de cada quien.
14584
+ ...f.moleculeType === "INPUT_CURRENCY" ? { uiProps: { thousandSeparator: separadores.miles, decimalSeparator: separadores.decimal } } : {}
14515
14585
  })),
14516
- [normalFields, optionsByField, optionsErrorByField]
14586
+ [normalFields, optionsByField, optionsErrorByField, separadores]
14517
14587
  );
14518
14588
  const validationSchema = React.useMemo(() => buildCrudYupSchema(normalFields), [normalFields]);
14519
14589
  const uploadOne = async (f, file, idx) => {
@@ -15172,6 +15242,95 @@ const FileViewer = ({
15172
15242
  ] });
15173
15243
  };
15174
15244
 
15245
+ function unwrap$2(raw) {
15246
+ if (raw && typeof raw === "object" && "response" in raw) {
15247
+ return raw.response;
15248
+ }
15249
+ return raw;
15250
+ }
15251
+ function hora(iso) {
15252
+ if (!iso) return "";
15253
+ const d = new Date(iso);
15254
+ if (Number.isNaN(d.getTime())) return "";
15255
+ return d.toLocaleString(void 0, { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
15256
+ }
15257
+ function TranscriptSection({ transcript, values, eventBus }) {
15258
+ const chatId = values[transcript.chatIdField];
15259
+ const id = chatId == null || chatId === "" ? "" : String(chatId);
15260
+ const [datos, setDatos] = React.useState(null);
15261
+ const [cargando, setCargando] = React.useState(true);
15262
+ const [fallo, setFallo] = React.useState(false);
15263
+ React.useEffect(() => {
15264
+ if (!eventBus || !id) {
15265
+ setCargando(false);
15266
+ return;
15267
+ }
15268
+ let vivo = true;
15269
+ setCargando(true);
15270
+ setFallo(false);
15271
+ void (async () => {
15272
+ try {
15273
+ const raw = await fetchViaEventBus(
15274
+ eventBus,
15275
+ transcript.get.service,
15276
+ transcript.get.url,
15277
+ transcript.get.method ?? "POST",
15278
+ { [transcript.param ?? "chat_id"]: id }
15279
+ );
15280
+ if (vivo) {
15281
+ setDatos(unwrap$2(raw) ?? {});
15282
+ setCargando(false);
15283
+ }
15284
+ } catch {
15285
+ if (vivo) {
15286
+ setFallo(true);
15287
+ setCargando(false);
15288
+ }
15289
+ }
15290
+ })();
15291
+ return () => {
15292
+ vivo = false;
15293
+ };
15294
+ }, [eventBus, id, transcript]);
15295
+ if (!id) return null;
15296
+ const lineas = datos?.lines ?? [];
15297
+ const dijoAlgo = (datos?.from_user ?? 0) > 0;
15298
+ const nombres = {
15299
+ user: transcript.labels?.user ?? "Cliente",
15300
+ assistant: transcript.labels?.assistant ?? "Asistente"
15301
+ };
15302
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-crud-transcript", children: [
15303
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "px-chat-crud-transcript__title", children: transcript.title ?? "Conversaci\xF3n" }),
15304
+ cargando && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-crud-transcript__note", children: "Cargando\u2026" }),
15305
+ !cargando && fallo && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-crud-transcript__note", children: "No se pudo cargar la conversaci\xF3n." }),
15306
+ !cargando && !fallo && !dijoAlgo && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-crud-transcript__note", children: transcript.silentText ?? "Todav\xEDa no ha escrito nada." }),
15307
+ !cargando && !fallo && lineas.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "px-chat-crud-transcript__lines", children: lineas.map((l, i) => /* @__PURE__ */ jsxRuntime.jsxs(
15308
+ "li",
15309
+ {
15310
+ className: [
15311
+ "px-chat-crud-transcript__line",
15312
+ l.rol === "user" ? "is-user" : "is-assistant"
15313
+ ].join(" "),
15314
+ children: [
15315
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "px-chat-crud-transcript__who", children: [
15316
+ l.rol === "user" ? nombres.user : nombres.assistant,
15317
+ hora(l.created_date) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-crud-transcript__when", children: hora(l.created_date) })
15318
+ ] }),
15319
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-crud-transcript__text", children: l.text })
15320
+ ]
15321
+ },
15322
+ `${i}-${l.created_date ?? ""}`
15323
+ )) }),
15324
+ !cargando && !fallo && (datos?.total ?? 0) > lineas.length && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-crud-transcript__note", children: [
15325
+ "Se muestran los \xFAltimos ",
15326
+ lineas.length,
15327
+ " de ",
15328
+ datos?.total,
15329
+ " mensajes."
15330
+ ] })
15331
+ ] });
15332
+ }
15333
+
15175
15334
  function unwrap$1(raw) {
15176
15335
  if (raw && typeof raw === "object" && "response" in raw) {
15177
15336
  return raw.response;
@@ -15300,6 +15459,7 @@ function DetailDrawer({ row, config, eventBus, onClose, loadedOptions }) {
15300
15459
  ] })
15301
15460
  ] }, f.name);
15302
15461
  }) }),
15462
+ config.transcript && /* @__PURE__ */ jsxRuntime.jsx(TranscriptSection, { transcript: config.transcript, values, eventBus }),
15303
15463
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-reactive-crud-detail__photos", children: [
15304
15464
  /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "px-chat-reactive-crud-detail__photos-title", children: "Fotos y documentos" }),
15305
15465
  loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-reactive-crud-detail__empty", children: "Cargando\u2026" }) : files.length ? /* @__PURE__ */ jsxRuntime.jsx(FileViewer, { files }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-reactive-crud-detail__empty", children: "Este servicio no tiene archivos." })
@@ -15788,7 +15948,8 @@ function usePlaceSuggestions(query, enabled) {
15788
15948
  return {
15789
15949
  lat: typeof loc.lat === "function" ? loc.lat() : loc.lat,
15790
15950
  lng: typeof loc.lng === "function" ? loc.lng() : loc.lng,
15791
- address: String(place.formattedAddress ?? "")
15951
+ address: limpiarDireccion(String(place.formattedAddress ?? ""))
15952
+ // SPEC-131 T6
15792
15953
  };
15793
15954
  } catch {
15794
15955
  return null;
@@ -15798,13 +15959,27 @@ function usePlaceSuggestions(query, enabled) {
15798
15959
  );
15799
15960
  return { suggestions, resolve, clear: () => setSuggestions([]) };
15800
15961
  }
15962
+ function limpiarDireccion(address) {
15963
+ const normal = (s) => s.trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
15964
+ const vistos = /* @__PURE__ */ new Set();
15965
+ const tramos = [];
15966
+ for (const tramo of address.split(",")) {
15967
+ const limpio = tramo.trim();
15968
+ if (!limpio) continue;
15969
+ const clave = normal(limpio);
15970
+ if (vistos.has(clave)) continue;
15971
+ vistos.add(clave);
15972
+ tramos.push(limpio);
15973
+ }
15974
+ return tramos.join(", ");
15975
+ }
15801
15976
  function reverseGeocode(lat, lng) {
15802
15977
  return new Promise((resolve) => {
15803
15978
  const maps = window.google?.maps;
15804
15979
  if (!maps?.Geocoder) return resolve(null);
15805
15980
  new maps.Geocoder().geocode({ location: { lat, lng } }, (results, status) => {
15806
15981
  if (status !== "OK" || !results?.length) return resolve(null);
15807
- resolve(String(results[0].formatted_address ?? "") || null);
15982
+ resolve(limpiarDireccion(String(results[0].formatted_address ?? "")) || null);
15808
15983
  });
15809
15984
  });
15810
15985
  }
@@ -15961,6 +16136,10 @@ function toRenderField(f, loaded, eventBus, mapsApiKey, loadError) {
15961
16136
  showInModes: f.showInModes,
15962
16137
  required: f.required,
15963
16138
  placeholder: f.placeholder,
16139
+ // SPEC-131 T7 — la fila entera: al elegir la dirección se despliega el mapa de verificación
16140
+ // y, a media fila, su campo hermano se quedaba con ~400px de blanco al lado. Medido entre
16141
+ // «¿En qué situación está?» y «¿A dónde lo llevamos?» del formulario de solicitud.
16142
+ gridSpan: 2,
15964
16143
  uiProps: {
15965
16144
  // SPEC-074 — `error` viene del render de CUSTOM (DynamicForm ya lo pasaba) y aquí se
15966
16145
  // desechaba: el campo obligatorio más importante era el único mudo.
@@ -16006,6 +16185,10 @@ function matchesVisibleWhen(a, row) {
16006
16185
  if (!a.visibleWhen) return true;
16007
16186
  return cumplenTodas(a.visibleWhen, row);
16008
16187
  }
16188
+ function laQueTocaAhora(actions, row) {
16189
+ const candidatas = actions.filter((a) => a.ahoraToca && cumplenTodas(a.ahoraToca, row));
16190
+ return candidatas.length === 1 ? candidatas[0] : void 0;
16191
+ }
16009
16192
  function condicionesDeshabilitado(a) {
16010
16193
  if (!a.disabledWhen) return [];
16011
16194
  return Array.isArray(a.disabledWhen) ? a.disabledWhen : [a.disabledWhen];
@@ -16326,7 +16509,7 @@ function DeadlineCountdown({ deadline, label, expiredLabel }) {
16326
16509
  }
16327
16510
 
16328
16511
  function ReactiveCrudItemChat({ component, eventBus }) {
16329
- const { resolveModuleConfig } = useChatConfig();
16512
+ const { resolveModuleConfig, resolveMomento } = useChatConfig();
16330
16513
  const props = component.props ?? {};
16331
16514
  const top = component;
16332
16515
  const pick = (key) => props[key] !== void 0 ? props[key] : top[key];
@@ -16334,7 +16517,11 @@ function ReactiveCrudItemChat({ component, eventBus }) {
16334
16517
  const recordId = pick("record_id") ?? pick("recordId") ?? pick("id") ?? pick("service_id");
16335
16518
  const recordLabel = pick("record_label") ?? pick("label");
16336
16519
  const allowedRaw = pick("actions");
16337
- const allowed = Array.isArray(allowedRaw) && allowedRaw.length > 0 ? allowedRaw : void 0;
16520
+ const allowedLegacy = Array.isArray(allowedRaw) && allowedRaw.length > 0 ? allowedRaw : void 0;
16521
+ const momento = pick("momento");
16522
+ const delMomento = momento && resolveMomento ? resolveMomento(momento) : void 0;
16523
+ const allowed = delMomento ?? allowedLegacy;
16524
+ const sinAcciones = delMomento?.length === 0;
16338
16525
  const config = typeof moduleKey === "string" && resolveModuleConfig ? resolveModuleConfig(moduleKey) : void 0;
16339
16526
  if (!config || recordId == null || recordId === "") return null;
16340
16527
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -16344,11 +16531,12 @@ function ReactiveCrudItemChat({ component, eventBus }) {
16344
16531
  recordId: String(recordId),
16345
16532
  recordLabel,
16346
16533
  allowed: Array.isArray(allowed) ? allowed.map(String) : void 0,
16534
+ sinAcciones,
16347
16535
  eventBus
16348
16536
  }
16349
16537
  );
16350
16538
  }
16351
- function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus }) {
16539
+ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, sinAcciones, eventBus }) {
16352
16540
  const { chatIsForeign } = useChatConfig();
16353
16541
  const crud = useReactiveCrud(config, eventBus);
16354
16542
  const [activeAction, setActiveAction] = React.useState(null);
@@ -16397,9 +16585,26 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16397
16585
  }, [recordId]);
16398
16586
  const gastada = !esUltima;
16399
16587
  const actions = React.useMemo(() => {
16400
- if (!row || gastada) return [];
16401
- return (config.rowActions ?? []).filter((a) => !allowed || allowed.includes(a.key)).filter((a) => matchesVisibleWhen(a, row));
16588
+ if (!row) return [];
16589
+ if (gastada) {
16590
+ const hechas = (config.rowActions ?? []).filter((a) => !allowed || allowed.includes(a.key)).filter((a) => matchesDoneWhen(a, row));
16591
+ const vistas = /* @__PURE__ */ new Set();
16592
+ return hechas.filter((a) => {
16593
+ const linea = a.doneLabel ?? a.label;
16594
+ if (vistas.has(linea)) return false;
16595
+ vistas.add(linea);
16596
+ return true;
16597
+ });
16598
+ }
16599
+ const visibles = (config.rowActions ?? []).filter((a) => !allowed || allowed.includes(a.key)).filter((a) => matchesVisibleWhen(a, row));
16600
+ const toca = laQueTocaAhora(visibles, row);
16601
+ if (!toca) return visibles;
16602
+ return [toca, ...visibles.filter((a) => a.key !== toca.key)];
16402
16603
  }, [config.rowActions, row, allowed, gastada]);
16604
+ const claveQueToca = React.useMemo(
16605
+ () => row && !gastada ? laQueTocaAhora(actions, row)?.key : void 0,
16606
+ [actions, row, gastada]
16607
+ );
16403
16608
  const title = recordLabel ?? config.title ?? "Servicio";
16404
16609
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-crud-item", ref: raiz, "data-panel-record": recordId, children: [
16405
16610
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-crud-item__header", children: title }),
@@ -16410,7 +16615,7 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16410
16615
  crud.loading && !row && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-crud-item__hint", children: "Cargando\u2026" }),
16411
16616
  !crud.loading && !row && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-crud-item__hint", children: "No se encontr\xF3 el servicio." }),
16412
16617
  gastada && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-crud-item__hint", children: "Este paso ya se gestion\xF3. Contin\xFAa en el mensaje m\xE1s reciente." }),
16413
- row && config.deadline && typeof row[config.deadline.field] === "string" && /* @__PURE__ */ jsxRuntime.jsx(
16618
+ row && !gastada && config.deadline && typeof row[config.deadline.field] === "string" && /* @__PURE__ */ jsxRuntime.jsx(
16414
16619
  DeadlineCountdown,
16415
16620
  {
16416
16621
  deadline: row[config.deadline.field],
@@ -16421,7 +16626,7 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16421
16626
  row && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-crud-item__actions", children: [
16422
16627
  config.detail && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "px-chat-crud-item__action", onClick: () => setDetailRow(row), children: "Ver" }),
16423
16628
  actions.map((a) => {
16424
- const disabled = Boolean(chatIsForeign) || matchesDisabledWhen(a, row);
16629
+ const disabled = gastada || Boolean(chatIsForeign) || matchesDisabledWhen(a, row);
16425
16630
  const motivo = chatIsForeign ? "Esta conversaci\xF3n es de otra persona" : disabledReasonFor(a, row);
16426
16631
  const done = matchesDoneWhen(a, row);
16427
16632
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -16431,13 +16636,18 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16431
16636
  className: [
16432
16637
  "px-chat-crud-item__action",
16433
16638
  a.intent === "danger" ? "is-danger" : "",
16434
- disabled && motivo ? "is-con-motivo" : ""
16639
+ disabled && motivo ? "is-con-motivo" : "",
16640
+ // SPEC-129 T6 — lo hecho se pinta como un acta, no como un botón apagado.
16641
+ gastada ? "is-hecho" : "",
16642
+ // SPEC-127 T4 — señalada, pero solo si de verdad se puede pulsar: destacar una
16643
+ // acción apagada sería dirigir a alguien a una puerta cerrada.
16644
+ a.key === claveQueToca && !disabled ? "is-ahora" : ""
16435
16645
  ].filter(Boolean).join(" "),
16436
16646
  disabled,
16437
16647
  onClick: () => a.form ? setActiveAction({ action: a, row }) : crud.runEndpoints(a.endpoints ?? [a.endpoint], { ...row, id: row[config.idField] }),
16438
16648
  children: [
16439
16649
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "px-chat-crud-item__action-texts", children: [
16440
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: a.label }),
16650
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: gastada && a.doneLabel ? a.doneLabel : a.label }),
16441
16651
  disabled && motivo && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-crud-item__action-reason", children: motivo })
16442
16652
  ] }),
16443
16653
  done && /* @__PURE__ */ jsxRuntime.jsx(Icon$1, { type: "check-circle", width: 18, height: 18, className: "px-chat-crud-item__done", "aria-label": "Hecho" })
@@ -16446,7 +16656,7 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16446
16656
  a.key
16447
16657
  );
16448
16658
  }),
16449
- actions.length === 0 && !config.detail && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-crud-item__hint", children: "Sin acciones disponibles." })
16659
+ actions.length === 0 && !config.detail && !sinAcciones && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-crud-item__hint", children: "Sin acciones disponibles." })
16450
16660
  ] }),
16451
16661
  activeAction && /* @__PURE__ */ jsxRuntime.jsx(
16452
16662
  ActionFormDrawer,
@@ -16554,6 +16764,8 @@ const ServicesMap = ({ config, eventBus, apiKey, filter, fill }) => {
16554
16764
  const { service, url, method, payloadMap } = config.data.list;
16555
16765
  void fetchViaEventBus(eventBus, service, url, method ?? "POST", {
16556
16766
  ...payloadMap,
16767
+ // SPEC-131 T6 — el del config primero, el de la instancia después: quien acota manda.
16768
+ ...config.defaultFilter ?? {},
16557
16769
  ...filter ?? {}
16558
16770
  }).then((res) => {
16559
16771
  if (!alive) return;
@@ -16666,7 +16878,7 @@ const ServicesMap = ({ config, eventBus, apiKey, filter, fill }) => {
16666
16878
  ),
16667
16879
  simulated > 0 && ` \xB7 ${config.live?.labels?.simulatedText ?? "incluye posiciones simuladas"}`
16668
16880
  ] }),
16669
- missing > 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: `${CLASS$6}__note`, children: (labels.missingText ?? "{n} sin ubicaci\xF3n registrada").replace("{n}", String(missing)) }),
16881
+ missing > 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: `${CLASS$6}__note`, children: ((missing === 1 ? labels.missingTextOne : void 0) ?? labels.missingText ?? "{n} sin ubicaci\xF3n registrada").replace("{n}", String(missing)) }),
16670
16882
  outside > 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: `${CLASS$6}__note`, children: (labels.outsideText ?? "{n} fuera de la zona (aleja el mapa para verlos)").replace(
16671
16883
  "{n}",
16672
16884
  String(outside)
@@ -18707,6 +18919,10 @@ const useInputFocus = ({
18707
18919
  };
18708
18920
  };
18709
18921
 
18922
+ function reservaNecesaria(clientHeight, scrollHeight, targetTop) {
18923
+ const debajoDelObjetivo = scrollHeight - targetTop;
18924
+ return Math.max(0, clientHeight - debajoDelObjetivo);
18925
+ }
18710
18926
  const useAutoScroll = ({
18711
18927
  messagesEndRef,
18712
18928
  messagesContainerRef
@@ -18725,11 +18941,12 @@ const useAutoScroll = ({
18725
18941
  const container = messagesContainerRef.current;
18726
18942
  const spacer = messagesEndRef.current;
18727
18943
  if (!container || !spacer || !target) return;
18728
- spacer.style.minHeight = `${container.clientHeight}px`;
18944
+ spacer.style.minHeight = "0px";
18729
18945
  const containerRect = container.getBoundingClientRect();
18730
18946
  const msgRect = target.getBoundingClientRect();
18731
18947
  const padTop = parseFloat(getComputedStyle(container).paddingTop) || 0;
18732
18948
  const targetTop = msgRect.top - containerRect.top + container.scrollTop - padTop;
18949
+ spacer.style.minHeight = `${reservaNecesaria(container.clientHeight, container.scrollHeight, targetTop)}px`;
18733
18950
  container.scrollTo({ top: targetTop, behavior: "smooth" });
18734
18951
  }, [messagesContainerRef, messagesEndRef]);
18735
18952
  const last = React.useCallback((selector) => {
@@ -18771,15 +18988,33 @@ const useAutoScroll = ({
18771
18988
  });
18772
18989
  }, 150);
18773
18990
  }, [doScroll]);
18774
- const scrollToBottom = React.useCallback(() => {
18991
+ const aterrizarAlFinal = React.useCallback(() => {
18992
+ const container = messagesContainerRef.current;
18993
+ if (!container) return;
18994
+ resetSpacer();
18995
+ const hasta = Date.now() + 1e3;
18996
+ let ultimoAlto = -1;
18997
+ const pegar = () => {
18998
+ const c = messagesContainerRef.current;
18999
+ if (!c) return;
19000
+ if (c.scrollHeight !== ultimoAlto) {
19001
+ ultimoAlto = c.scrollHeight;
19002
+ c.scrollTop = c.scrollHeight;
19003
+ }
19004
+ if (Date.now() < hasta) requestAnimationFrame(pegar);
19005
+ };
19006
+ requestAnimationFrame(pegar);
19007
+ }, [messagesContainerRef, resetSpacer]);
19008
+ const scrollToBottom = React.useCallback((suave = true) => {
18775
19009
  cancelPending();
18776
19010
  resetSpacer();
18777
19011
  requestAnimationFrame(() => {
19012
+ const comportamiento = suave ? "smooth" : "auto";
18778
19013
  const container = messagesContainerRef.current;
18779
19014
  if (container) {
18780
- container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
19015
+ container.scrollTo({ top: container.scrollHeight, behavior: comportamiento });
18781
19016
  } else {
18782
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
19017
+ messagesEndRef.current?.scrollIntoView({ behavior: comportamiento, block: "end" });
18783
19018
  }
18784
19019
  });
18785
19020
  }, [cancelPending, resetSpacer, messagesContainerRef, messagesEndRef]);
@@ -18787,7 +19022,9 @@ const useAutoScroll = ({
18787
19022
  scrollToLastUserMessage,
18788
19023
  pinLastUserMessage,
18789
19024
  scrollToProactive,
18790
- scrollToBottom
19025
+ scrollToBottom,
19026
+ aterrizarAlFinal
19027
+ // SPEC-131 T3
18791
19028
  };
18792
19029
  };
18793
19030
 
@@ -19340,7 +19577,7 @@ const Chat = ({
19340
19577
  usePreventOverscroll();
19341
19578
  const { showThinking, scrollButtonState, scrollToTopPending } = useChatStore();
19342
19579
  const { attachments, handleAttachmentsChange, clearAttachments } = useAttachments();
19343
- const { scrollToBottom, scrollToLastUserMessage, pinLastUserMessage, scrollToProactive } = useAutoScroll({
19580
+ const { scrollToBottom, scrollToLastUserMessage, pinLastUserMessage, scrollToProactive, aterrizarAlFinal } = useAutoScroll({
19344
19581
  messagesEndRef,
19345
19582
  messagesContainerRef
19346
19583
  });
@@ -19368,11 +19605,23 @@ const Chat = ({
19368
19605
  }
19369
19606
  }, [scrollToTopPending, scrollToLastUserMessage]);
19370
19607
  React.useEffect(() => {
19608
+ if (messages[messages.length - 1]?.proactive) return;
19371
19609
  const spacer = messagesEndRef.current;
19372
19610
  if (spacer && spacer.style.minHeight && spacer.style.minHeight !== "0px") {
19373
19611
  pinLastUserMessage();
19374
19612
  }
19375
19613
  }, [messages]);
19614
+ const yaAterrizo = React.useRef(false);
19615
+ React.useEffect(() => {
19616
+ if (messages.length === 0) {
19617
+ yaAterrizo.current = false;
19618
+ return;
19619
+ }
19620
+ if (yaAterrizo.current) return;
19621
+ yaAterrizo.current = true;
19622
+ if (messages[messages.length - 1]?.proactive) return;
19623
+ aterrizarAlFinal();
19624
+ }, [messages, aterrizarAlFinal]);
19376
19625
  const lastProactiveIdRef = React.useRef(void 0);
19377
19626
  React.useEffect(() => {
19378
19627
  const last = messages[messages.length - 1];
@@ -19490,7 +19739,7 @@ const Chat = ({
19490
19739
  {
19491
19740
  className: `${COMPONENT_PREFIX$1}chat__scroll-to-bottom`,
19492
19741
  variant: "circle",
19493
- onClick: scrollToBottom,
19742
+ onClick: () => scrollToBottom(),
19494
19743
  "aria-label": "Ir al final",
19495
19744
  icon: /* @__PURE__ */ jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
19496
19745
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 8v8", strokeWidth: "2", strokeLinecap: "round" }),