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.esm.js CHANGED
@@ -10924,6 +10924,7 @@ const Drawer = ({
10924
10924
  "data-testid": dataTestId,
10925
10925
  scrollIndicatorOffset = 1
10926
10926
  }) => {
10927
+ const idTitulo = useId();
10927
10928
  const [isMobile, setIsMobile] = useState(false);
10928
10929
  const contentRef = useRef(null);
10929
10930
  const [showScrollIndicator, setShowScrollIndicator] = useState(false);
@@ -11047,14 +11048,14 @@ const Drawer = ({
11047
11048
  style: getDrawerStyles(),
11048
11049
  role: "dialog",
11049
11050
  "aria-modal": "true",
11050
- "aria-labelledby": title ? "drawer-title" : void 0,
11051
+ "aria-labelledby": title ? idTitulo : void 0,
11051
11052
  "data-testid": dataTestId,
11052
11053
  children: /* @__PURE__ */ jsxs("div", { className: `${COMPONENT_PREFIX}drawer__viewport`, children: [
11053
11054
  (title || showCloseButton || headerExtra) && /* @__PURE__ */ jsxs("div", { className: `${COMPONENT_PREFIX}drawer__header`, children: [
11054
11055
  title && /* @__PURE__ */ jsx(
11055
11056
  "h2",
11056
11057
  {
11057
- id: "drawer-title",
11058
+ id: idTitulo,
11058
11059
  className: `${COMPONENT_PREFIX}drawer__title`,
11059
11060
  children: title
11060
11061
  }
@@ -12817,29 +12818,22 @@ const FormDrawer = ({
12817
12818
  onClose();
12818
12819
  methods.reset();
12819
12820
  }, [onClose, onBeforeClose, methods]);
12820
- const [invalidMsg, setInvalidMsg] = useState(null);
12821
- useEffect(() => {
12822
- if (!isOpen) setInvalidMsg(null);
12823
- }, [isOpen]);
12824
- useEffect(() => {
12825
- const sub = methods.watch(() => setInvalidMsg(null));
12826
- return () => sub.unsubscribe();
12827
- }, [methods.watch]);
12828
12821
  const handleFormSubmit = useCallback(
12829
12822
  async (data) => {
12830
- setInvalidMsg(null);
12831
12823
  await onSubmit(data);
12832
12824
  },
12833
12825
  [onSubmit]
12834
12826
  );
12835
12827
  const handleInvalid = useCallback(
12836
12828
  (formErrors) => {
12837
- const faltan = Object.keys(formErrors).map((name) => fields.find((f) => f.name === name)?.label ?? name);
12838
- setInvalidMsg(
12839
- faltan.length > 0 ? `Falta completar: ${faltan.join(", ")}.` : "Revisa los campos marcados."
12840
- );
12829
+ const primero = Object.keys(formErrors)[0];
12830
+ if (!primero) return;
12831
+ const nodo = document.querySelector(`[name="${CSS.escape(primero)}"]`);
12832
+ if (!nodo) return;
12833
+ nodo.scrollIntoView({ block: "center", behavior: "smooth" });
12834
+ window.setTimeout(() => nodo.focus({ preventScroll: true }), 300);
12841
12835
  },
12842
- [fields]
12836
+ []
12843
12837
  );
12844
12838
  const handleSubmitClick = methods.handleSubmit(handleFormSubmit, handleInvalid);
12845
12839
  const drawerTitle = title || DEFAULT_TITLES[mode];
@@ -12897,7 +12891,6 @@ const FormDrawer = ({
12897
12891
  ) }) }),
12898
12892
  /* @__PURE__ */ jsxs("div", { className: `${baseClass}__footer`, children: [
12899
12893
  footerExtra && /* @__PURE__ */ jsx("div", { className: `${baseClass}__footer-extra`, children: footerExtra }),
12900
- invalidMsg && mode !== "view" && /* @__PURE__ */ jsx("p", { className: `${baseClass}__invalid`, role: "alert", children: invalidMsg }),
12901
12894
  /* @__PURE__ */ jsxs("div", { className: `${baseClass}__actions`, children: [
12902
12895
  /* @__PURE__ */ jsx(
12903
12896
  Button,
@@ -13175,7 +13168,9 @@ function mapFieldsToFormFields(fields, mode) {
13175
13168
  readOnly: field.readOnly,
13176
13169
  placeholder: field.placeholder,
13177
13170
  uiProps: field.uiProps,
13178
- transformOnSubmit: field.parseValue
13171
+ transformOnSubmit: field.parseValue,
13172
+ gridSpan: field.gridSpan
13173
+ // SPEC-131 T7
13179
13174
  }));
13180
13175
  }
13181
13176
 
@@ -13355,6 +13350,47 @@ function calculateTotalPages(totalRecords, limit) {
13355
13350
  return Math.ceil(totalRecords / limit);
13356
13351
  }
13357
13352
 
13353
+ function etiquetaDeOpcion(valor, uiProps) {
13354
+ const opciones = uiProps?.options;
13355
+ if (!Array.isArray(opciones)) return null;
13356
+ const encontrada = opciones.find((o) => o && String(o.value) === String(valor));
13357
+ return encontrada?.label != null ? String(encontrada.label) : null;
13358
+ }
13359
+ function esNumerico(moleculeType) {
13360
+ return moleculeType === "INPUT_CURRENCY" || moleculeType === "INPUT_NUMBER";
13361
+ }
13362
+ function aTexto(valor, uiProps, moleculeType) {
13363
+ if (valor == null || valor === "") return "";
13364
+ if (typeof valor === "boolean") return valor ? "S\xED" : "No";
13365
+ if (esNumerico(moleculeType)) {
13366
+ const n = Number(String(valor).replace(/[^\d.-]/g, ""));
13367
+ if (Number.isFinite(n)) return n.toLocaleString();
13368
+ }
13369
+ if (typeof File !== "undefined" && valor instanceof File) return valor.name;
13370
+ if (Array.isArray(valor)) {
13371
+ const partes = valor.map((v) => aTexto(v, uiProps, moleculeType)).filter(Boolean);
13372
+ return partes.join(", ");
13373
+ }
13374
+ if (typeof valor === "object") {
13375
+ const o = valor;
13376
+ for (const clave of ["label", "address", "name", "text", "value"]) {
13377
+ if (typeof o[clave] === "string" && o[clave]) return o[clave];
13378
+ }
13379
+ return "";
13380
+ }
13381
+ return etiquetaDeOpcion(valor, uiProps) ?? String(valor);
13382
+ }
13383
+ function resumenParaConfirmar(campos, datos) {
13384
+ if (!datos) return [];
13385
+ const lineas = [];
13386
+ for (const campo of campos) {
13387
+ const valor = aTexto(datos[campo.name], campo.uiProps, campo.moleculeType);
13388
+ if (!valor) continue;
13389
+ lineas.push({ label: campo.label ?? campo.name, valor });
13390
+ }
13391
+ return lineas;
13392
+ }
13393
+
13358
13394
  const useAnchoPropio = () => {
13359
13395
  const ref = useRef(null);
13360
13396
  const [ancho, setAncho] = useState(null);
@@ -13799,6 +13835,10 @@ function Crud({
13799
13835
  return "Formulario";
13800
13836
  }
13801
13837
  };
13838
+ const resumenConfirmacion = useMemo(
13839
+ () => confirmAction === "create" || confirmAction === "edit" ? resumenParaConfirmar(formFields, pendingFormData) : [],
13840
+ [confirmAction, formFields, pendingFormData]
13841
+ );
13802
13842
  const getConfirmMessage = () => {
13803
13843
  switch (confirmAction) {
13804
13844
  case "create":
@@ -13938,6 +13978,10 @@ function Crud({
13938
13978
  className: `${COMPONENT_PREFIX}crud__confirm-drawer`,
13939
13979
  children: /* @__PURE__ */ jsxs("div", { className: `${COMPONENT_PREFIX}crud__confirm-content`, children: [
13940
13980
  /* @__PURE__ */ jsx("p", { className: `${COMPONENT_PREFIX}crud__confirm-message`, children: getConfirmMessage() }),
13981
+ resumenConfirmacion.length > 0 && /* @__PURE__ */ jsx("dl", { className: `${COMPONENT_PREFIX}crud__confirm-summary`, children: resumenConfirmacion.map((linea) => /* @__PURE__ */ jsxs("div", { className: `${COMPONENT_PREFIX}crud__confirm-summary-row`, children: [
13982
+ /* @__PURE__ */ jsx("dt", { children: linea.label }),
13983
+ /* @__PURE__ */ jsx("dd", { children: linea.valor })
13984
+ ] }, linea.label)) }),
13941
13985
  /* @__PURE__ */ jsxs("div", { className: `${COMPONENT_PREFIX}crud__confirm-actions`, children: [
13942
13986
  /* @__PURE__ */ jsx(
13943
13987
  Button,
@@ -14112,7 +14156,7 @@ function getFormatter(key) {
14112
14156
  }
14113
14157
 
14114
14158
  const OPTIONS_LOAD_ERROR = "No se pudieron cargar las opciones. Int\xE9ntalo de nuevo.";
14115
- function unwrap$3(raw) {
14159
+ function unwrap$4(raw) {
14116
14160
  if (raw && typeof raw === "object" && "response" in raw) {
14117
14161
  return raw.response;
14118
14162
  }
@@ -14133,7 +14177,7 @@ function resolveBody(body, ctx) {
14133
14177
  }
14134
14178
  async function loadOptions(eventBus, ep, ctx) {
14135
14179
  const raw = await fetchViaEventBus(eventBus, ep.service, ep.url, ep.method, resolveBody(ep.body, ctx));
14136
- let data = unwrap$3(raw);
14180
+ let data = unwrap$4(raw);
14137
14181
  if (ep.response_path && data && typeof data === "object" && !Array.isArray(data)) {
14138
14182
  data = data[ep.response_path];
14139
14183
  }
@@ -14171,7 +14215,7 @@ const mensajeDeError = (e, respaldo) => {
14171
14215
  };
14172
14216
 
14173
14217
  const REFRESH_INTERVAL_MS = 2e4;
14174
- function unwrap$2(raw) {
14218
+ function unwrap$3(raw) {
14175
14219
  if (raw && typeof raw === "object" && "response" in raw) {
14176
14220
  return raw.response;
14177
14221
  }
@@ -14220,7 +14264,7 @@ function useReactiveCrud(config, eventBus) {
14220
14264
  filters
14221
14265
  });
14222
14266
  assertOk(raw);
14223
- const data = unwrap$2(raw);
14267
+ const data = unwrap$3(raw);
14224
14268
  let rows = [];
14225
14269
  let total = 0;
14226
14270
  if (Array.isArray(data)) {
@@ -14424,6 +14468,20 @@ const FileField = ({
14424
14468
  ] });
14425
14469
  };
14426
14470
 
14471
+ const RESPALDO = { miles: ".", decimal: "," };
14472
+ function separadoresDelLocale(locale) {
14473
+ if (!locale) return RESPALDO;
14474
+ try {
14475
+ const partes = new Intl.NumberFormat(locale).formatToParts(123456789e-2);
14476
+ const miles = partes.find((p) => p.type === "group")?.value;
14477
+ const decimal = partes.find((p) => p.type === "decimal")?.value;
14478
+ if (!miles || !decimal) return RESPALDO;
14479
+ return { miles, decimal };
14480
+ } catch {
14481
+ return RESPALDO;
14482
+ }
14483
+ }
14484
+
14427
14485
  const CRUD_TO_FORM_MOLECULE = {
14428
14486
  INPUT: "INPUT",
14429
14487
  INPUT_NUMBER: "INPUT_NUMBER",
@@ -14480,6 +14538,8 @@ function ActionFormDrawer({ action, row, idField, eventBus, onExecute, onClose }
14480
14538
  alive = false;
14481
14539
  };
14482
14540
  }, [eventBus, action.key, normalFields]);
14541
+ const { locale } = useChatConfig();
14542
+ const separadores = useMemo(() => separadoresDelLocale(locale), [locale]);
14483
14543
  const fields = useMemo(
14484
14544
  () => normalFields.map((f) => ({
14485
14545
  name: f.name,
@@ -14490,9 +14550,19 @@ function ActionFormDrawer({ action, row, idField, eventBus, onExecute, onClose }
14490
14550
  options: f.options ?? optionsByField[f.name],
14491
14551
  // SPEC-106 T8 — `uiProps` llega tal cual a la molécula, así que el desplegable dice lo
14492
14552
  // que pasó de verdad en vez del «no hay» de siempre. Sin error, se queda el de siempre.
14493
- ...optionsErrorByField[f.name] ? { uiProps: { notFoundContent: optionsErrorByField[f.name] } } : {}
14553
+ ...optionsErrorByField[f.name] ? { uiProps: { notFoundContent: optionsErrorByField[f.name] } } : {},
14554
+ // SPEC-128 — el dinero se escribe como se escribe AQUÍ.
14555
+ //
14556
+ // Los campos de moneda caían a `thousandSeparator ?? ','`, un valor por defecto en formato
14557
+ // inglés: al teclear 180000 el campo mostraba «180,000» mientras la tarjeta que recibe el
14558
+ // cliente decía «$ 180.000,00 COP». El mismo importe escrito de una manera y leído de otra.
14559
+ //
14560
+ // Se deriva del locale que el host ya declara en la config del chat, no de un valor fijo ni
14561
+ // del navegador: `navigator.language` puede ser `es-419` —español, pero con coma para los
14562
+ // miles— y entonces el resultado dependería del portátil de cada quien.
14563
+ ...f.moleculeType === "INPUT_CURRENCY" ? { uiProps: { thousandSeparator: separadores.miles, decimalSeparator: separadores.decimal } } : {}
14494
14564
  })),
14495
- [normalFields, optionsByField, optionsErrorByField]
14565
+ [normalFields, optionsByField, optionsErrorByField, separadores]
14496
14566
  );
14497
14567
  const validationSchema = useMemo(() => buildCrudYupSchema(normalFields), [normalFields]);
14498
14568
  const uploadOne = async (f, file, idx) => {
@@ -15151,6 +15221,95 @@ const FileViewer = ({
15151
15221
  ] });
15152
15222
  };
15153
15223
 
15224
+ function unwrap$2(raw) {
15225
+ if (raw && typeof raw === "object" && "response" in raw) {
15226
+ return raw.response;
15227
+ }
15228
+ return raw;
15229
+ }
15230
+ function hora(iso) {
15231
+ if (!iso) return "";
15232
+ const d = new Date(iso);
15233
+ if (Number.isNaN(d.getTime())) return "";
15234
+ return d.toLocaleString(void 0, { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
15235
+ }
15236
+ function TranscriptSection({ transcript, values, eventBus }) {
15237
+ const chatId = values[transcript.chatIdField];
15238
+ const id = chatId == null || chatId === "" ? "" : String(chatId);
15239
+ const [datos, setDatos] = useState(null);
15240
+ const [cargando, setCargando] = useState(true);
15241
+ const [fallo, setFallo] = useState(false);
15242
+ useEffect(() => {
15243
+ if (!eventBus || !id) {
15244
+ setCargando(false);
15245
+ return;
15246
+ }
15247
+ let vivo = true;
15248
+ setCargando(true);
15249
+ setFallo(false);
15250
+ void (async () => {
15251
+ try {
15252
+ const raw = await fetchViaEventBus(
15253
+ eventBus,
15254
+ transcript.get.service,
15255
+ transcript.get.url,
15256
+ transcript.get.method ?? "POST",
15257
+ { [transcript.param ?? "chat_id"]: id }
15258
+ );
15259
+ if (vivo) {
15260
+ setDatos(unwrap$2(raw) ?? {});
15261
+ setCargando(false);
15262
+ }
15263
+ } catch {
15264
+ if (vivo) {
15265
+ setFallo(true);
15266
+ setCargando(false);
15267
+ }
15268
+ }
15269
+ })();
15270
+ return () => {
15271
+ vivo = false;
15272
+ };
15273
+ }, [eventBus, id, transcript]);
15274
+ if (!id) return null;
15275
+ const lineas = datos?.lines ?? [];
15276
+ const dijoAlgo = (datos?.from_user ?? 0) > 0;
15277
+ const nombres = {
15278
+ user: transcript.labels?.user ?? "Cliente",
15279
+ assistant: transcript.labels?.assistant ?? "Asistente"
15280
+ };
15281
+ return /* @__PURE__ */ jsxs("div", { className: "px-chat-crud-transcript", children: [
15282
+ /* @__PURE__ */ jsx("h4", { className: "px-chat-crud-transcript__title", children: transcript.title ?? "Conversaci\xF3n" }),
15283
+ cargando && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-transcript__note", children: "Cargando\u2026" }),
15284
+ !cargando && fallo && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-transcript__note", children: "No se pudo cargar la conversaci\xF3n." }),
15285
+ !cargando && !fallo && !dijoAlgo && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-transcript__note", children: transcript.silentText ?? "Todav\xEDa no ha escrito nada." }),
15286
+ !cargando && !fallo && lineas.length > 0 && /* @__PURE__ */ jsx("ol", { className: "px-chat-crud-transcript__lines", children: lineas.map((l, i) => /* @__PURE__ */ jsxs(
15287
+ "li",
15288
+ {
15289
+ className: [
15290
+ "px-chat-crud-transcript__line",
15291
+ l.rol === "user" ? "is-user" : "is-assistant"
15292
+ ].join(" "),
15293
+ children: [
15294
+ /* @__PURE__ */ jsxs("span", { className: "px-chat-crud-transcript__who", children: [
15295
+ l.rol === "user" ? nombres.user : nombres.assistant,
15296
+ hora(l.created_date) && /* @__PURE__ */ jsx("span", { className: "px-chat-crud-transcript__when", children: hora(l.created_date) })
15297
+ ] }),
15298
+ /* @__PURE__ */ jsx("span", { className: "px-chat-crud-transcript__text", children: l.text })
15299
+ ]
15300
+ },
15301
+ `${i}-${l.created_date ?? ""}`
15302
+ )) }),
15303
+ !cargando && !fallo && (datos?.total ?? 0) > lineas.length && /* @__PURE__ */ jsxs("div", { className: "px-chat-crud-transcript__note", children: [
15304
+ "Se muestran los \xFAltimos ",
15305
+ lineas.length,
15306
+ " de ",
15307
+ datos?.total,
15308
+ " mensajes."
15309
+ ] })
15310
+ ] });
15311
+ }
15312
+
15154
15313
  function unwrap$1(raw) {
15155
15314
  if (raw && typeof raw === "object" && "response" in raw) {
15156
15315
  return raw.response;
@@ -15279,6 +15438,7 @@ function DetailDrawer({ row, config, eventBus, onClose, loadedOptions }) {
15279
15438
  ] })
15280
15439
  ] }, f.name);
15281
15440
  }) }),
15441
+ config.transcript && /* @__PURE__ */ jsx(TranscriptSection, { transcript: config.transcript, values, eventBus }),
15282
15442
  /* @__PURE__ */ jsxs("div", { className: "px-chat-reactive-crud-detail__photos", children: [
15283
15443
  /* @__PURE__ */ jsx("h4", { className: "px-chat-reactive-crud-detail__photos-title", children: "Fotos y documentos" }),
15284
15444
  loading ? /* @__PURE__ */ jsx("div", { className: "px-chat-reactive-crud-detail__empty", children: "Cargando\u2026" }) : files.length ? /* @__PURE__ */ jsx(FileViewer, { files }) : /* @__PURE__ */ jsx("div", { className: "px-chat-reactive-crud-detail__empty", children: "Este servicio no tiene archivos." })
@@ -15767,7 +15927,8 @@ function usePlaceSuggestions(query, enabled) {
15767
15927
  return {
15768
15928
  lat: typeof loc.lat === "function" ? loc.lat() : loc.lat,
15769
15929
  lng: typeof loc.lng === "function" ? loc.lng() : loc.lng,
15770
- address: String(place.formattedAddress ?? "")
15930
+ address: limpiarDireccion(String(place.formattedAddress ?? ""))
15931
+ // SPEC-131 T6
15771
15932
  };
15772
15933
  } catch {
15773
15934
  return null;
@@ -15777,13 +15938,27 @@ function usePlaceSuggestions(query, enabled) {
15777
15938
  );
15778
15939
  return { suggestions, resolve, clear: () => setSuggestions([]) };
15779
15940
  }
15941
+ function limpiarDireccion(address) {
15942
+ const normal = (s) => s.trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
15943
+ const vistos = /* @__PURE__ */ new Set();
15944
+ const tramos = [];
15945
+ for (const tramo of address.split(",")) {
15946
+ const limpio = tramo.trim();
15947
+ if (!limpio) continue;
15948
+ const clave = normal(limpio);
15949
+ if (vistos.has(clave)) continue;
15950
+ vistos.add(clave);
15951
+ tramos.push(limpio);
15952
+ }
15953
+ return tramos.join(", ");
15954
+ }
15780
15955
  function reverseGeocode(lat, lng) {
15781
15956
  return new Promise((resolve) => {
15782
15957
  const maps = window.google?.maps;
15783
15958
  if (!maps?.Geocoder) return resolve(null);
15784
15959
  new maps.Geocoder().geocode({ location: { lat, lng } }, (results, status) => {
15785
15960
  if (status !== "OK" || !results?.length) return resolve(null);
15786
- resolve(String(results[0].formatted_address ?? "") || null);
15961
+ resolve(limpiarDireccion(String(results[0].formatted_address ?? "")) || null);
15787
15962
  });
15788
15963
  });
15789
15964
  }
@@ -15940,6 +16115,10 @@ function toRenderField(f, loaded, eventBus, mapsApiKey, loadError) {
15940
16115
  showInModes: f.showInModes,
15941
16116
  required: f.required,
15942
16117
  placeholder: f.placeholder,
16118
+ // SPEC-131 T7 — la fila entera: al elegir la dirección se despliega el mapa de verificación
16119
+ // y, a media fila, su campo hermano se quedaba con ~400px de blanco al lado. Medido entre
16120
+ // «¿En qué situación está?» y «¿A dónde lo llevamos?» del formulario de solicitud.
16121
+ gridSpan: 2,
15943
16122
  uiProps: {
15944
16123
  // SPEC-074 — `error` viene del render de CUSTOM (DynamicForm ya lo pasaba) y aquí se
15945
16124
  // desechaba: el campo obligatorio más importante era el único mudo.
@@ -15985,6 +16164,10 @@ function matchesVisibleWhen(a, row) {
15985
16164
  if (!a.visibleWhen) return true;
15986
16165
  return cumplenTodas(a.visibleWhen, row);
15987
16166
  }
16167
+ function laQueTocaAhora(actions, row) {
16168
+ const candidatas = actions.filter((a) => a.ahoraToca && cumplenTodas(a.ahoraToca, row));
16169
+ return candidatas.length === 1 ? candidatas[0] : void 0;
16170
+ }
15988
16171
  function condicionesDeshabilitado(a) {
15989
16172
  if (!a.disabledWhen) return [];
15990
16173
  return Array.isArray(a.disabledWhen) ? a.disabledWhen : [a.disabledWhen];
@@ -16305,7 +16488,7 @@ function DeadlineCountdown({ deadline, label, expiredLabel }) {
16305
16488
  }
16306
16489
 
16307
16490
  function ReactiveCrudItemChat({ component, eventBus }) {
16308
- const { resolveModuleConfig } = useChatConfig();
16491
+ const { resolveModuleConfig, resolveMomento } = useChatConfig();
16309
16492
  const props = component.props ?? {};
16310
16493
  const top = component;
16311
16494
  const pick = (key) => props[key] !== void 0 ? props[key] : top[key];
@@ -16313,7 +16496,11 @@ function ReactiveCrudItemChat({ component, eventBus }) {
16313
16496
  const recordId = pick("record_id") ?? pick("recordId") ?? pick("id") ?? pick("service_id");
16314
16497
  const recordLabel = pick("record_label") ?? pick("label");
16315
16498
  const allowedRaw = pick("actions");
16316
- const allowed = Array.isArray(allowedRaw) && allowedRaw.length > 0 ? allowedRaw : void 0;
16499
+ const allowedLegacy = Array.isArray(allowedRaw) && allowedRaw.length > 0 ? allowedRaw : void 0;
16500
+ const momento = pick("momento");
16501
+ const delMomento = momento && resolveMomento ? resolveMomento(momento) : void 0;
16502
+ const allowed = delMomento ?? allowedLegacy;
16503
+ const sinAcciones = delMomento?.length === 0;
16317
16504
  const config = typeof moduleKey === "string" && resolveModuleConfig ? resolveModuleConfig(moduleKey) : void 0;
16318
16505
  if (!config || recordId == null || recordId === "") return null;
16319
16506
  return /* @__PURE__ */ jsx(
@@ -16323,11 +16510,12 @@ function ReactiveCrudItemChat({ component, eventBus }) {
16323
16510
  recordId: String(recordId),
16324
16511
  recordLabel,
16325
16512
  allowed: Array.isArray(allowed) ? allowed.map(String) : void 0,
16513
+ sinAcciones,
16326
16514
  eventBus
16327
16515
  }
16328
16516
  );
16329
16517
  }
16330
- function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus }) {
16518
+ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, sinAcciones, eventBus }) {
16331
16519
  const { chatIsForeign } = useChatConfig();
16332
16520
  const crud = useReactiveCrud(config, eventBus);
16333
16521
  const [activeAction, setActiveAction] = useState(null);
@@ -16376,9 +16564,26 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16376
16564
  }, [recordId]);
16377
16565
  const gastada = !esUltima;
16378
16566
  const actions = useMemo(() => {
16379
- if (!row || gastada) return [];
16380
- return (config.rowActions ?? []).filter((a) => !allowed || allowed.includes(a.key)).filter((a) => matchesVisibleWhen(a, row));
16567
+ if (!row) return [];
16568
+ if (gastada) {
16569
+ const hechas = (config.rowActions ?? []).filter((a) => !allowed || allowed.includes(a.key)).filter((a) => matchesDoneWhen(a, row));
16570
+ const vistas = /* @__PURE__ */ new Set();
16571
+ return hechas.filter((a) => {
16572
+ const linea = a.doneLabel ?? a.label;
16573
+ if (vistas.has(linea)) return false;
16574
+ vistas.add(linea);
16575
+ return true;
16576
+ });
16577
+ }
16578
+ const visibles = (config.rowActions ?? []).filter((a) => !allowed || allowed.includes(a.key)).filter((a) => matchesVisibleWhen(a, row));
16579
+ const toca = laQueTocaAhora(visibles, row);
16580
+ if (!toca) return visibles;
16581
+ return [toca, ...visibles.filter((a) => a.key !== toca.key)];
16381
16582
  }, [config.rowActions, row, allowed, gastada]);
16583
+ const claveQueToca = useMemo(
16584
+ () => row && !gastada ? laQueTocaAhora(actions, row)?.key : void 0,
16585
+ [actions, row, gastada]
16586
+ );
16382
16587
  const title = recordLabel ?? config.title ?? "Servicio";
16383
16588
  return /* @__PURE__ */ jsxs("div", { className: "px-chat-crud-item", ref: raiz, "data-panel-record": recordId, children: [
16384
16589
  /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__header", children: title }),
@@ -16389,7 +16594,7 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16389
16594
  crud.loading && !row && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__hint", children: "Cargando\u2026" }),
16390
16595
  !crud.loading && !row && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__hint", children: "No se encontr\xF3 el servicio." }),
16391
16596
  gastada && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__hint", children: "Este paso ya se gestion\xF3. Contin\xFAa en el mensaje m\xE1s reciente." }),
16392
- row && config.deadline && typeof row[config.deadline.field] === "string" && /* @__PURE__ */ jsx(
16597
+ row && !gastada && config.deadline && typeof row[config.deadline.field] === "string" && /* @__PURE__ */ jsx(
16393
16598
  DeadlineCountdown,
16394
16599
  {
16395
16600
  deadline: row[config.deadline.field],
@@ -16400,7 +16605,7 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16400
16605
  row && /* @__PURE__ */ jsxs("div", { className: "px-chat-crud-item__actions", children: [
16401
16606
  config.detail && /* @__PURE__ */ jsx("button", { type: "button", className: "px-chat-crud-item__action", onClick: () => setDetailRow(row), children: "Ver" }),
16402
16607
  actions.map((a) => {
16403
- const disabled = Boolean(chatIsForeign) || matchesDisabledWhen(a, row);
16608
+ const disabled = gastada || Boolean(chatIsForeign) || matchesDisabledWhen(a, row);
16404
16609
  const motivo = chatIsForeign ? "Esta conversaci\xF3n es de otra persona" : disabledReasonFor(a, row);
16405
16610
  const done = matchesDoneWhen(a, row);
16406
16611
  return /* @__PURE__ */ jsxs(
@@ -16410,13 +16615,18 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16410
16615
  className: [
16411
16616
  "px-chat-crud-item__action",
16412
16617
  a.intent === "danger" ? "is-danger" : "",
16413
- disabled && motivo ? "is-con-motivo" : ""
16618
+ disabled && motivo ? "is-con-motivo" : "",
16619
+ // SPEC-129 T6 — lo hecho se pinta como un acta, no como un botón apagado.
16620
+ gastada ? "is-hecho" : "",
16621
+ // SPEC-127 T4 — señalada, pero solo si de verdad se puede pulsar: destacar una
16622
+ // acción apagada sería dirigir a alguien a una puerta cerrada.
16623
+ a.key === claveQueToca && !disabled ? "is-ahora" : ""
16414
16624
  ].filter(Boolean).join(" "),
16415
16625
  disabled,
16416
16626
  onClick: () => a.form ? setActiveAction({ action: a, row }) : crud.runEndpoints(a.endpoints ?? [a.endpoint], { ...row, id: row[config.idField] }),
16417
16627
  children: [
16418
16628
  /* @__PURE__ */ jsxs("span", { className: "px-chat-crud-item__action-texts", children: [
16419
- /* @__PURE__ */ jsx("span", { children: a.label }),
16629
+ /* @__PURE__ */ jsx("span", { children: gastada && a.doneLabel ? a.doneLabel : a.label }),
16420
16630
  disabled && motivo && /* @__PURE__ */ jsx("span", { className: "px-chat-crud-item__action-reason", children: motivo })
16421
16631
  ] }),
16422
16632
  done && /* @__PURE__ */ jsx(Icon$1, { type: "check-circle", width: 18, height: 18, className: "px-chat-crud-item__done", "aria-label": "Hecho" })
@@ -16425,7 +16635,7 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16425
16635
  a.key
16426
16636
  );
16427
16637
  }),
16428
- actions.length === 0 && !config.detail && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__hint", children: "Sin acciones disponibles." })
16638
+ actions.length === 0 && !config.detail && !sinAcciones && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__hint", children: "Sin acciones disponibles." })
16429
16639
  ] }),
16430
16640
  activeAction && /* @__PURE__ */ jsx(
16431
16641
  ActionFormDrawer,
@@ -16533,6 +16743,8 @@ const ServicesMap = ({ config, eventBus, apiKey, filter, fill }) => {
16533
16743
  const { service, url, method, payloadMap } = config.data.list;
16534
16744
  void fetchViaEventBus(eventBus, service, url, method ?? "POST", {
16535
16745
  ...payloadMap,
16746
+ // SPEC-131 T6 — el del config primero, el de la instancia después: quien acota manda.
16747
+ ...config.defaultFilter ?? {},
16536
16748
  ...filter ?? {}
16537
16749
  }).then((res) => {
16538
16750
  if (!alive) return;
@@ -16645,7 +16857,7 @@ const ServicesMap = ({ config, eventBus, apiKey, filter, fill }) => {
16645
16857
  ),
16646
16858
  simulated > 0 && ` \xB7 ${config.live?.labels?.simulatedText ?? "incluye posiciones simuladas"}`
16647
16859
  ] }),
16648
- missing > 0 && /* @__PURE__ */ jsx("p", { className: `${CLASS$6}__note`, children: (labels.missingText ?? "{n} sin ubicaci\xF3n registrada").replace("{n}", String(missing)) }),
16860
+ missing > 0 && /* @__PURE__ */ jsx("p", { className: `${CLASS$6}__note`, children: ((missing === 1 ? labels.missingTextOne : void 0) ?? labels.missingText ?? "{n} sin ubicaci\xF3n registrada").replace("{n}", String(missing)) }),
16649
16861
  outside > 0 && /* @__PURE__ */ jsx("p", { className: `${CLASS$6}__note`, children: (labels.outsideText ?? "{n} fuera de la zona (aleja el mapa para verlos)").replace(
16650
16862
  "{n}",
16651
16863
  String(outside)
@@ -18686,6 +18898,10 @@ const useInputFocus = ({
18686
18898
  };
18687
18899
  };
18688
18900
 
18901
+ function reservaNecesaria(clientHeight, scrollHeight, targetTop) {
18902
+ const debajoDelObjetivo = scrollHeight - targetTop;
18903
+ return Math.max(0, clientHeight - debajoDelObjetivo);
18904
+ }
18689
18905
  const useAutoScroll = ({
18690
18906
  messagesEndRef,
18691
18907
  messagesContainerRef
@@ -18704,11 +18920,12 @@ const useAutoScroll = ({
18704
18920
  const container = messagesContainerRef.current;
18705
18921
  const spacer = messagesEndRef.current;
18706
18922
  if (!container || !spacer || !target) return;
18707
- spacer.style.minHeight = `${container.clientHeight}px`;
18923
+ spacer.style.minHeight = "0px";
18708
18924
  const containerRect = container.getBoundingClientRect();
18709
18925
  const msgRect = target.getBoundingClientRect();
18710
18926
  const padTop = parseFloat(getComputedStyle(container).paddingTop) || 0;
18711
18927
  const targetTop = msgRect.top - containerRect.top + container.scrollTop - padTop;
18928
+ spacer.style.minHeight = `${reservaNecesaria(container.clientHeight, container.scrollHeight, targetTop)}px`;
18712
18929
  container.scrollTo({ top: targetTop, behavior: "smooth" });
18713
18930
  }, [messagesContainerRef, messagesEndRef]);
18714
18931
  const last = useCallback((selector) => {
@@ -18750,15 +18967,33 @@ const useAutoScroll = ({
18750
18967
  });
18751
18968
  }, 150);
18752
18969
  }, [doScroll]);
18753
- const scrollToBottom = useCallback(() => {
18970
+ const aterrizarAlFinal = useCallback(() => {
18971
+ const container = messagesContainerRef.current;
18972
+ if (!container) return;
18973
+ resetSpacer();
18974
+ const hasta = Date.now() + 1e3;
18975
+ let ultimoAlto = -1;
18976
+ const pegar = () => {
18977
+ const c = messagesContainerRef.current;
18978
+ if (!c) return;
18979
+ if (c.scrollHeight !== ultimoAlto) {
18980
+ ultimoAlto = c.scrollHeight;
18981
+ c.scrollTop = c.scrollHeight;
18982
+ }
18983
+ if (Date.now() < hasta) requestAnimationFrame(pegar);
18984
+ };
18985
+ requestAnimationFrame(pegar);
18986
+ }, [messagesContainerRef, resetSpacer]);
18987
+ const scrollToBottom = useCallback((suave = true) => {
18754
18988
  cancelPending();
18755
18989
  resetSpacer();
18756
18990
  requestAnimationFrame(() => {
18991
+ const comportamiento = suave ? "smooth" : "auto";
18757
18992
  const container = messagesContainerRef.current;
18758
18993
  if (container) {
18759
- container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
18994
+ container.scrollTo({ top: container.scrollHeight, behavior: comportamiento });
18760
18995
  } else {
18761
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
18996
+ messagesEndRef.current?.scrollIntoView({ behavior: comportamiento, block: "end" });
18762
18997
  }
18763
18998
  });
18764
18999
  }, [cancelPending, resetSpacer, messagesContainerRef, messagesEndRef]);
@@ -18766,7 +19001,9 @@ const useAutoScroll = ({
18766
19001
  scrollToLastUserMessage,
18767
19002
  pinLastUserMessage,
18768
19003
  scrollToProactive,
18769
- scrollToBottom
19004
+ scrollToBottom,
19005
+ aterrizarAlFinal
19006
+ // SPEC-131 T3
18770
19007
  };
18771
19008
  };
18772
19009
 
@@ -19319,7 +19556,7 @@ const Chat = ({
19319
19556
  usePreventOverscroll();
19320
19557
  const { showThinking, scrollButtonState, scrollToTopPending } = useChatStore();
19321
19558
  const { attachments, handleAttachmentsChange, clearAttachments } = useAttachments();
19322
- const { scrollToBottom, scrollToLastUserMessage, pinLastUserMessage, scrollToProactive } = useAutoScroll({
19559
+ const { scrollToBottom, scrollToLastUserMessage, pinLastUserMessage, scrollToProactive, aterrizarAlFinal } = useAutoScroll({
19323
19560
  messagesEndRef,
19324
19561
  messagesContainerRef
19325
19562
  });
@@ -19347,11 +19584,23 @@ const Chat = ({
19347
19584
  }
19348
19585
  }, [scrollToTopPending, scrollToLastUserMessage]);
19349
19586
  useEffect(() => {
19587
+ if (messages[messages.length - 1]?.proactive) return;
19350
19588
  const spacer = messagesEndRef.current;
19351
19589
  if (spacer && spacer.style.minHeight && spacer.style.minHeight !== "0px") {
19352
19590
  pinLastUserMessage();
19353
19591
  }
19354
19592
  }, [messages]);
19593
+ const yaAterrizo = useRef(false);
19594
+ useEffect(() => {
19595
+ if (messages.length === 0) {
19596
+ yaAterrizo.current = false;
19597
+ return;
19598
+ }
19599
+ if (yaAterrizo.current) return;
19600
+ yaAterrizo.current = true;
19601
+ if (messages[messages.length - 1]?.proactive) return;
19602
+ aterrizarAlFinal();
19603
+ }, [messages, aterrizarAlFinal]);
19355
19604
  const lastProactiveIdRef = useRef(void 0);
19356
19605
  useEffect(() => {
19357
19606
  const last = messages[messages.length - 1];
@@ -19469,7 +19718,7 @@ const Chat = ({
19469
19718
  {
19470
19719
  className: `${COMPONENT_PREFIX$1}chat__scroll-to-bottom`,
19471
19720
  variant: "circle",
19472
- onClick: scrollToBottom,
19721
+ onClick: () => scrollToBottom(),
19473
19722
  "aria-label": "Ir al final",
19474
19723
  icon: /* @__PURE__ */ 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: [
19475
19724
  /* @__PURE__ */ jsx("path", { d: "M12 8v8", strokeWidth: "2", strokeLinecap: "round" }),