pixelon 0.4.3 → 0.5.0

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.
@@ -14,6 +14,7 @@ export interface ReactiveCrudProps {
14
14
  onCreateClose?: () => void;
15
15
  }
16
16
  export declare function matchesVisibleWhen(a: RowActionDef, row: Record<string, unknown>): boolean;
17
+ export declare function laQueTocaAhora(actions: RowActionDef[], row: Record<string, unknown>): RowActionDef | undefined;
17
18
  export declare function matchesDisabledWhen(a: RowActionDef, row: Record<string, unknown>): boolean;
18
19
  export declare function disabledReasonFor(a: RowActionDef, row: Record<string, unknown>): string | undefined;
19
20
  export declare function matchesDoneWhen(a: RowActionDef, row: Record<string, unknown>): boolean;
@@ -0,0 +1,10 @@
1
+ import type { ChatEventBus } from '../../../../../../core/eventBus/chatEventBus';
2
+ import type { ReactiveCrudTranscript } from './types';
3
+ export interface TranscriptSectionProps {
4
+ transcript: ReactiveCrudTranscript;
5
+ /** Valores del registro (fila + detalle): de ahí sale el id de la conversación. */
6
+ values: Record<string, unknown>;
7
+ eventBus?: ChatEventBus;
8
+ }
9
+ export declare function TranscriptSection({ transcript, values, eventBus }: TranscriptSectionProps): import("react/jsx-runtime").JSX.Element | null;
10
+ export default TranscriptSection;
@@ -114,8 +114,28 @@ export interface RowActionDef {
114
114
  * SPEC-113: es el respaldo — una condición con `reason` propio manda sobre este. */
115
115
  disabledReason?: string;
116
116
  /** SPEC-007: muestra un ✓ al final cuando `String(row[field])` está en `in` (ej. inspección ya
117
- * registrada). Indicador de "hecho"; no bloquea reejecutar la acción. */
117
+ * registrada). Indicador de "hecho"; no bloquea reejecutar la acción.
118
+ *
119
+ * SPEC-129 T6: y es lo que decide qué queda escrito en una tarjeta ya gastada. Ahí solo se
120
+ * muestra lo que CUMPLE esta condición, porque es lo único que consta que se hizo: si el paso se
121
+ * cerró por otro camino —el barrido liberó la grúa, la operadora canceló— no hay nada que marcar
122
+ * y la tarjeta se queda con su frase, sin inventar un ✓. */
118
123
  doneWhen?: RowActionVisibleWhen;
124
+ /** SPEC-129 T6: cómo se llama esto una vez hecho. «Registrar inspección» → «Inspección
125
+ * registrada». En una tarjeta gastada el botón ya no es una oferta sino un acta, y un acta se
126
+ * escribe en pasado. Sin esto se cae a `label`, que sigue leyéndose bien con el ✓ al lado. */
127
+ doneLabel?: string;
128
+ /** SPEC-127 T4: cuándo esta acción es LA QUE TOCA AHORA. La que cumple va primera y destacada;
129
+ * el resto pasa a un segundo plano, disponible pero sin competir.
130
+ *
131
+ * No es lo mismo que `intent: 'primary'`, que describe la acción SIEMPRE («asignar es una acción
132
+ * importante»). Esto describe el MOMENTO: el mensaje que acompaña la tarjeta dice «Confírmalo
133
+ * para que la grúa salga», y entonces confirmar el pago es lo único que se le está pidiendo,
134
+ * aunque a su alrededor quepan siete cosas más.
135
+ *
136
+ * Si ninguna acción la cumple —o dos la cumplen— la tarjeta se comporta como antes: ninguna se
137
+ * destaca. Es preferible no señalar nada que señalar mal. */
138
+ ahoraToca?: RowActionVisibleWhen | RowActionVisibleWhen[];
119
139
  endpoint: EndpointRef;
120
140
  /** SPEC-002: varias llamadas en orden (ej. inspección = register-inspection foto + video). Si
121
141
  * se define, tiene prioridad sobre `endpoint`; todos comparten el mismo contexto del form. */
@@ -142,6 +162,23 @@ export interface ReactiveCrudDetail {
142
162
  /** Campo de la fila para el título del drawer (ej. 'service_number'). */
143
163
  titleField?: string;
144
164
  }
165
+ export interface ReactiveCrudTranscript {
166
+ /** Campo de la fila con el id de la conversación (ej. 'origin_chat_id'). */
167
+ chatIdField: string;
168
+ /** Endpoint que devuelve `{ lines, total, from_user }`; recibe `{ [param]: <chatIdField> }`. */
169
+ get: EndpointRef;
170
+ /** Clave del cuerpo donde viaja el id (default 'chat_id'). */
171
+ param?: string;
172
+ /** Título de la sección. Ej: 'Conversación con el cliente'. */
173
+ title?: string;
174
+ /** Qué decir cuando la otra parte no ha escrito nada. Es una respuesta, no un hueco. */
175
+ silentText?: string;
176
+ /** Con qué nombre se pinta cada lado. */
177
+ labels?: {
178
+ user?: string;
179
+ assistant?: string;
180
+ };
181
+ }
145
182
  export interface ReactiveCrudConfig {
146
183
  title?: string;
147
184
  /** Campo id de la fila (getRowKey de LumiCrud es obligatorio). */
@@ -154,6 +191,8 @@ export interface ReactiveCrudConfig {
154
191
  };
155
192
  /** Vista "Ver" con campos + galería de archivos. */
156
193
  detail?: ReactiveCrudDetail;
194
+ /** SPEC-128 T5 — conversación en solo lectura dentro de «Ver». Ver `ReactiveCrudTranscript`. */
195
+ transcript?: ReactiveCrudTranscript;
157
196
  rowActions?: RowActionDef[];
158
197
  filters?: FilterFieldConfig[];
159
198
  /** SPEC-014 — con qué filtros NACE la tabla. `filters` (arriba) dice qué se PUEDE filtrar; esto,
@@ -48,6 +48,9 @@ export interface ServicesMapConfig {
48
48
  emptyText?: string;
49
49
  /** Aviso de las filas que no se pudieron ubicar. `{n}` se reemplaza por el número. */
50
50
  missingText?: string;
51
+ /** SPEC-127 — el mismo aviso cuando falta UNA sola. Sin esto se lee «1 servicios sin
52
+ * ubicación registrada». Opcional: sin él se usa `missingText` para cualquier número. */
53
+ missingTextOne?: string;
51
54
  outsideText?: string;
52
55
  loadingText?: string;
53
56
  };
@@ -22,7 +22,6 @@ export { ServicesMap } from './ServicesMap/ServicesMap';
22
22
  export { ServicesMapChat } from './ServicesMap/ServicesMapChat';
23
23
  export type { ServicesMapConfig } from './ServicesMap/types';
24
24
  export { ServiceShortcuts } from './ServiceShortcuts/ServiceShortcuts';
25
- export { ServiceShortcutsChat } from './ServiceShortcuts/ServiceShortcutsChat';
26
25
  export { ChatNavigator } from './ChatNavigator/ChatNavigator';
27
26
  export { ChatNavigatorChat } from './ChatNavigator/ChatNavigatorChat';
28
27
  export type { ChatNavigatorConfig, ChatNavigatorItem, ChatNavigatorEndpoint } from './ChatNavigator/types';
@@ -19,6 +19,17 @@ export interface ChatConfig {
19
19
  * sola es preferible a un panel que se apaga por una conexión mala; el backend sigue rechazando
20
20
  * lo que no toca. */
21
21
  chatIsForeign?: boolean;
22
+ /** SPEC-128 — DE QUIÉN es esta conversación (el agente que la atiende).
23
+ *
24
+ * `chatIsForeign` dice «esto es de otro»; esto dice **de quién**. No es lo mismo, y hasta ahora
25
+ * solo existía lo primero: la operadora abría la conversación del solicitante y sus componentes
26
+ * se pintaban con el rol de ELLA, así que veía «Gestionar servicios» donde al cliente se le
27
+ * ofreció «Solicitar una grúa». Un mensaje del pasado no es una pregunta sobre quién mira: es el
28
+ * registro de lo que se dijo, y para pintarlo bien hay que saber a quién se le dijo.
29
+ *
30
+ * El host lo pone porque es quien sabe qué conversación está abriendo. Opcional: sin él, los
31
+ * componentes se comportan como antes (deciden por quien mira). */
32
+ chatAgentName?: string;
22
33
  /** IANA timezone identifier (e.g. "America/Bogota") */
23
34
  timezone: string;
24
35
  /** BCP 47 locale string (e.g. "es-CO") */
@@ -59,6 +70,20 @@ export interface ChatConfig {
59
70
  * Se tipa como bag genérico para no acoplar `core` a los tipos del componente.
60
71
  */
61
72
  resolveModuleConfig?: (module: string) => Record<string, unknown> | undefined;
73
+ /**
74
+ * SPEC-128: qué acciones toca mostrar en un MOMENTO del flujo. Lo provee el host, que es quien
75
+ * sabe qué significa «comprobante_subido» en su producto — aquí solo se sabe que hay momentos.
76
+ *
77
+ * Existe porque la tarjeta tenía que ADIVINAR en qué punto estaba, mirando campos de la fila y
78
+ * decidiendo con cuatro capas de condiciones cuáles de las quince acciones mostrar. Salían ocho
79
+ * botones para una decisión, y pasos futuros apagados ocupando tres cuartas partes. Y cuando dos
80
+ * momentos comparten los mismos datos, adivinar es imposible.
81
+ *
82
+ * Devolver `undefined` significa «no sé de ese momento»: entonces se decide por el estado, como
83
+ * siempre. Una lista VACÍA es distinto — es «en este momento no hay nada que hacer», y la tarjeta
84
+ * enseña solo «Ver».
85
+ */
86
+ resolveMomento?: (momento: string) => string[] | undefined;
62
87
  /**
63
88
  * SPEC-022: clave de Google Maps JavaScript API para el componente `map_view`. La provee el
64
89
  * HOST desde su entorno; no viaja en el payload del LLM (es configuración de despliegue, y el
@@ -0,0 +1,12 @@
1
+ export interface Separadores {
2
+ /** El que agrupa los miles: `.` en es-CO, `,` en en-US. */
3
+ miles: string;
4
+ /** El que abre los decimales: `,` en es-CO, `.` en en-US. */
5
+ decimal: string;
6
+ }
7
+ /** Cómo se escriben los números en ese locale, según el propio navegador.
8
+ *
9
+ * Se deriva en vez de tabularse: una tabla de locales se queda corta en cuanto alguien vende en
10
+ * otro país, y `Intl` ya lo sabe. Si el locale no se entiende, se responde el formato de la casa
11
+ * en vez de reventar — un número mal separado es feo; un formulario que no pinta, inservible. */
12
+ export declare function separadoresDelLocale(locale?: string): Separadores;
package/dist/index.esm.js CHANGED
@@ -12817,29 +12817,22 @@ const FormDrawer = ({
12817
12817
  onClose();
12818
12818
  methods.reset();
12819
12819
  }, [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
12820
  const handleFormSubmit = useCallback(
12829
12821
  async (data) => {
12830
- setInvalidMsg(null);
12831
12822
  await onSubmit(data);
12832
12823
  },
12833
12824
  [onSubmit]
12834
12825
  );
12835
12826
  const handleInvalid = useCallback(
12836
12827
  (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
- );
12828
+ const primero = Object.keys(formErrors)[0];
12829
+ if (!primero) return;
12830
+ const nodo = document.querySelector(`[name="${CSS.escape(primero)}"]`);
12831
+ if (!nodo) return;
12832
+ nodo.scrollIntoView({ block: "center", behavior: "smooth" });
12833
+ window.setTimeout(() => nodo.focus({ preventScroll: true }), 300);
12841
12834
  },
12842
- [fields]
12835
+ []
12843
12836
  );
12844
12837
  const handleSubmitClick = methods.handleSubmit(handleFormSubmit, handleInvalid);
12845
12838
  const drawerTitle = title || DEFAULT_TITLES[mode];
@@ -12897,7 +12890,6 @@ const FormDrawer = ({
12897
12890
  ) }) }),
12898
12891
  /* @__PURE__ */ jsxs("div", { className: `${baseClass}__footer`, children: [
12899
12892
  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
12893
  /* @__PURE__ */ jsxs("div", { className: `${baseClass}__actions`, children: [
12902
12894
  /* @__PURE__ */ jsx(
12903
12895
  Button,
@@ -14112,7 +14104,7 @@ function getFormatter(key) {
14112
14104
  }
14113
14105
 
14114
14106
  const OPTIONS_LOAD_ERROR = "No se pudieron cargar las opciones. Int\xE9ntalo de nuevo.";
14115
- function unwrap$3(raw) {
14107
+ function unwrap$4(raw) {
14116
14108
  if (raw && typeof raw === "object" && "response" in raw) {
14117
14109
  return raw.response;
14118
14110
  }
@@ -14133,7 +14125,7 @@ function resolveBody(body, ctx) {
14133
14125
  }
14134
14126
  async function loadOptions(eventBus, ep, ctx) {
14135
14127
  const raw = await fetchViaEventBus(eventBus, ep.service, ep.url, ep.method, resolveBody(ep.body, ctx));
14136
- let data = unwrap$3(raw);
14128
+ let data = unwrap$4(raw);
14137
14129
  if (ep.response_path && data && typeof data === "object" && !Array.isArray(data)) {
14138
14130
  data = data[ep.response_path];
14139
14131
  }
@@ -14171,7 +14163,7 @@ const mensajeDeError = (e, respaldo) => {
14171
14163
  };
14172
14164
 
14173
14165
  const REFRESH_INTERVAL_MS = 2e4;
14174
- function unwrap$2(raw) {
14166
+ function unwrap$3(raw) {
14175
14167
  if (raw && typeof raw === "object" && "response" in raw) {
14176
14168
  return raw.response;
14177
14169
  }
@@ -14220,7 +14212,7 @@ function useReactiveCrud(config, eventBus) {
14220
14212
  filters
14221
14213
  });
14222
14214
  assertOk(raw);
14223
- const data = unwrap$2(raw);
14215
+ const data = unwrap$3(raw);
14224
14216
  let rows = [];
14225
14217
  let total = 0;
14226
14218
  if (Array.isArray(data)) {
@@ -14424,6 +14416,20 @@ const FileField = ({
14424
14416
  ] });
14425
14417
  };
14426
14418
 
14419
+ const RESPALDO = { miles: ".", decimal: "," };
14420
+ function separadoresDelLocale(locale) {
14421
+ if (!locale) return RESPALDO;
14422
+ try {
14423
+ const partes = new Intl.NumberFormat(locale).formatToParts(123456789e-2);
14424
+ const miles = partes.find((p) => p.type === "group")?.value;
14425
+ const decimal = partes.find((p) => p.type === "decimal")?.value;
14426
+ if (!miles || !decimal) return RESPALDO;
14427
+ return { miles, decimal };
14428
+ } catch {
14429
+ return RESPALDO;
14430
+ }
14431
+ }
14432
+
14427
14433
  const CRUD_TO_FORM_MOLECULE = {
14428
14434
  INPUT: "INPUT",
14429
14435
  INPUT_NUMBER: "INPUT_NUMBER",
@@ -14480,6 +14486,8 @@ function ActionFormDrawer({ action, row, idField, eventBus, onExecute, onClose }
14480
14486
  alive = false;
14481
14487
  };
14482
14488
  }, [eventBus, action.key, normalFields]);
14489
+ const { locale } = useChatConfig();
14490
+ const separadores = useMemo(() => separadoresDelLocale(locale), [locale]);
14483
14491
  const fields = useMemo(
14484
14492
  () => normalFields.map((f) => ({
14485
14493
  name: f.name,
@@ -14490,9 +14498,19 @@ function ActionFormDrawer({ action, row, idField, eventBus, onExecute, onClose }
14490
14498
  options: f.options ?? optionsByField[f.name],
14491
14499
  // SPEC-106 T8 — `uiProps` llega tal cual a la molécula, así que el desplegable dice lo
14492
14500
  // 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] } } : {}
14501
+ ...optionsErrorByField[f.name] ? { uiProps: { notFoundContent: optionsErrorByField[f.name] } } : {},
14502
+ // SPEC-128 — el dinero se escribe como se escribe AQUÍ.
14503
+ //
14504
+ // Los campos de moneda caían a `thousandSeparator ?? ','`, un valor por defecto en formato
14505
+ // inglés: al teclear 180000 el campo mostraba «180,000» mientras la tarjeta que recibe el
14506
+ // cliente decía «$ 180.000,00 COP». El mismo importe escrito de una manera y leído de otra.
14507
+ //
14508
+ // Se deriva del locale que el host ya declara en la config del chat, no de un valor fijo ni
14509
+ // del navegador: `navigator.language` puede ser `es-419` —español, pero con coma para los
14510
+ // miles— y entonces el resultado dependería del portátil de cada quien.
14511
+ ...f.moleculeType === "INPUT_CURRENCY" ? { uiProps: { thousandSeparator: separadores.miles, decimalSeparator: separadores.decimal } } : {}
14494
14512
  })),
14495
- [normalFields, optionsByField, optionsErrorByField]
14513
+ [normalFields, optionsByField, optionsErrorByField, separadores]
14496
14514
  );
14497
14515
  const validationSchema = useMemo(() => buildCrudYupSchema(normalFields), [normalFields]);
14498
14516
  const uploadOne = async (f, file, idx) => {
@@ -15151,6 +15169,95 @@ const FileViewer = ({
15151
15169
  ] });
15152
15170
  };
15153
15171
 
15172
+ function unwrap$2(raw) {
15173
+ if (raw && typeof raw === "object" && "response" in raw) {
15174
+ return raw.response;
15175
+ }
15176
+ return raw;
15177
+ }
15178
+ function hora(iso) {
15179
+ if (!iso) return "";
15180
+ const d = new Date(iso);
15181
+ if (Number.isNaN(d.getTime())) return "";
15182
+ return d.toLocaleString(void 0, { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
15183
+ }
15184
+ function TranscriptSection({ transcript, values, eventBus }) {
15185
+ const chatId = values[transcript.chatIdField];
15186
+ const id = chatId == null || chatId === "" ? "" : String(chatId);
15187
+ const [datos, setDatos] = useState(null);
15188
+ const [cargando, setCargando] = useState(true);
15189
+ const [fallo, setFallo] = useState(false);
15190
+ useEffect(() => {
15191
+ if (!eventBus || !id) {
15192
+ setCargando(false);
15193
+ return;
15194
+ }
15195
+ let vivo = true;
15196
+ setCargando(true);
15197
+ setFallo(false);
15198
+ void (async () => {
15199
+ try {
15200
+ const raw = await fetchViaEventBus(
15201
+ eventBus,
15202
+ transcript.get.service,
15203
+ transcript.get.url,
15204
+ transcript.get.method ?? "POST",
15205
+ { [transcript.param ?? "chat_id"]: id }
15206
+ );
15207
+ if (vivo) {
15208
+ setDatos(unwrap$2(raw) ?? {});
15209
+ setCargando(false);
15210
+ }
15211
+ } catch {
15212
+ if (vivo) {
15213
+ setFallo(true);
15214
+ setCargando(false);
15215
+ }
15216
+ }
15217
+ })();
15218
+ return () => {
15219
+ vivo = false;
15220
+ };
15221
+ }, [eventBus, id, transcript]);
15222
+ if (!id) return null;
15223
+ const lineas = datos?.lines ?? [];
15224
+ const dijoAlgo = (datos?.from_user ?? 0) > 0;
15225
+ const nombres = {
15226
+ user: transcript.labels?.user ?? "Cliente",
15227
+ assistant: transcript.labels?.assistant ?? "Asistente"
15228
+ };
15229
+ return /* @__PURE__ */ jsxs("div", { className: "px-chat-crud-transcript", children: [
15230
+ /* @__PURE__ */ jsx("h4", { className: "px-chat-crud-transcript__title", children: transcript.title ?? "Conversaci\xF3n" }),
15231
+ cargando && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-transcript__note", children: "Cargando\u2026" }),
15232
+ !cargando && fallo && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-transcript__note", children: "No se pudo cargar la conversaci\xF3n." }),
15233
+ !cargando && !fallo && !dijoAlgo && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-transcript__note", children: transcript.silentText ?? "Todav\xEDa no ha escrito nada." }),
15234
+ !cargando && !fallo && lineas.length > 0 && /* @__PURE__ */ jsx("ol", { className: "px-chat-crud-transcript__lines", children: lineas.map((l, i) => /* @__PURE__ */ jsxs(
15235
+ "li",
15236
+ {
15237
+ className: [
15238
+ "px-chat-crud-transcript__line",
15239
+ l.rol === "user" ? "is-user" : "is-assistant"
15240
+ ].join(" "),
15241
+ children: [
15242
+ /* @__PURE__ */ jsxs("span", { className: "px-chat-crud-transcript__who", children: [
15243
+ l.rol === "user" ? nombres.user : nombres.assistant,
15244
+ hora(l.created_date) && /* @__PURE__ */ jsx("span", { className: "px-chat-crud-transcript__when", children: hora(l.created_date) })
15245
+ ] }),
15246
+ /* @__PURE__ */ jsx("span", { className: "px-chat-crud-transcript__text", children: l.text })
15247
+ ]
15248
+ },
15249
+ `${i}-${l.created_date ?? ""}`
15250
+ )) }),
15251
+ !cargando && !fallo && (datos?.total ?? 0) > lineas.length && /* @__PURE__ */ jsxs("div", { className: "px-chat-crud-transcript__note", children: [
15252
+ "Se muestran los \xFAltimos ",
15253
+ lineas.length,
15254
+ " de ",
15255
+ datos?.total,
15256
+ " mensajes."
15257
+ ] })
15258
+ ] });
15259
+ }
15260
+
15154
15261
  function unwrap$1(raw) {
15155
15262
  if (raw && typeof raw === "object" && "response" in raw) {
15156
15263
  return raw.response;
@@ -15279,6 +15386,7 @@ function DetailDrawer({ row, config, eventBus, onClose, loadedOptions }) {
15279
15386
  ] })
15280
15387
  ] }, f.name);
15281
15388
  }) }),
15389
+ config.transcript && /* @__PURE__ */ jsx(TranscriptSection, { transcript: config.transcript, values, eventBus }),
15282
15390
  /* @__PURE__ */ jsxs("div", { className: "px-chat-reactive-crud-detail__photos", children: [
15283
15391
  /* @__PURE__ */ jsx("h4", { className: "px-chat-reactive-crud-detail__photos-title", children: "Fotos y documentos" }),
15284
15392
  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." })
@@ -15985,6 +16093,10 @@ function matchesVisibleWhen(a, row) {
15985
16093
  if (!a.visibleWhen) return true;
15986
16094
  return cumplenTodas(a.visibleWhen, row);
15987
16095
  }
16096
+ function laQueTocaAhora(actions, row) {
16097
+ const candidatas = actions.filter((a) => a.ahoraToca && cumplenTodas(a.ahoraToca, row));
16098
+ return candidatas.length === 1 ? candidatas[0] : void 0;
16099
+ }
15988
16100
  function condicionesDeshabilitado(a) {
15989
16101
  if (!a.disabledWhen) return [];
15990
16102
  return Array.isArray(a.disabledWhen) ? a.disabledWhen : [a.disabledWhen];
@@ -16305,7 +16417,7 @@ function DeadlineCountdown({ deadline, label, expiredLabel }) {
16305
16417
  }
16306
16418
 
16307
16419
  function ReactiveCrudItemChat({ component, eventBus }) {
16308
- const { resolveModuleConfig } = useChatConfig();
16420
+ const { resolveModuleConfig, resolveMomento } = useChatConfig();
16309
16421
  const props = component.props ?? {};
16310
16422
  const top = component;
16311
16423
  const pick = (key) => props[key] !== void 0 ? props[key] : top[key];
@@ -16313,7 +16425,11 @@ function ReactiveCrudItemChat({ component, eventBus }) {
16313
16425
  const recordId = pick("record_id") ?? pick("recordId") ?? pick("id") ?? pick("service_id");
16314
16426
  const recordLabel = pick("record_label") ?? pick("label");
16315
16427
  const allowedRaw = pick("actions");
16316
- const allowed = Array.isArray(allowedRaw) && allowedRaw.length > 0 ? allowedRaw : void 0;
16428
+ const allowedLegacy = Array.isArray(allowedRaw) && allowedRaw.length > 0 ? allowedRaw : void 0;
16429
+ const momento = pick("momento");
16430
+ const delMomento = momento && resolveMomento ? resolveMomento(momento) : void 0;
16431
+ const allowed = delMomento ?? allowedLegacy;
16432
+ const sinAcciones = delMomento?.length === 0;
16317
16433
  const config = typeof moduleKey === "string" && resolveModuleConfig ? resolveModuleConfig(moduleKey) : void 0;
16318
16434
  if (!config || recordId == null || recordId === "") return null;
16319
16435
  return /* @__PURE__ */ jsx(
@@ -16323,11 +16439,12 @@ function ReactiveCrudItemChat({ component, eventBus }) {
16323
16439
  recordId: String(recordId),
16324
16440
  recordLabel,
16325
16441
  allowed: Array.isArray(allowed) ? allowed.map(String) : void 0,
16442
+ sinAcciones,
16326
16443
  eventBus
16327
16444
  }
16328
16445
  );
16329
16446
  }
16330
- function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus }) {
16447
+ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, sinAcciones, eventBus }) {
16331
16448
  const { chatIsForeign } = useChatConfig();
16332
16449
  const crud = useReactiveCrud(config, eventBus);
16333
16450
  const [activeAction, setActiveAction] = useState(null);
@@ -16376,9 +16493,26 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16376
16493
  }, [recordId]);
16377
16494
  const gastada = !esUltima;
16378
16495
  const actions = useMemo(() => {
16379
- if (!row || gastada) return [];
16380
- return (config.rowActions ?? []).filter((a) => !allowed || allowed.includes(a.key)).filter((a) => matchesVisibleWhen(a, row));
16496
+ if (!row) return [];
16497
+ if (gastada) {
16498
+ const hechas = (config.rowActions ?? []).filter((a) => !allowed || allowed.includes(a.key)).filter((a) => matchesDoneWhen(a, row));
16499
+ const vistas = /* @__PURE__ */ new Set();
16500
+ return hechas.filter((a) => {
16501
+ const linea = a.doneLabel ?? a.label;
16502
+ if (vistas.has(linea)) return false;
16503
+ vistas.add(linea);
16504
+ return true;
16505
+ });
16506
+ }
16507
+ const visibles = (config.rowActions ?? []).filter((a) => !allowed || allowed.includes(a.key)).filter((a) => matchesVisibleWhen(a, row));
16508
+ const toca = laQueTocaAhora(visibles, row);
16509
+ if (!toca) return visibles;
16510
+ return [toca, ...visibles.filter((a) => a.key !== toca.key)];
16381
16511
  }, [config.rowActions, row, allowed, gastada]);
16512
+ const claveQueToca = useMemo(
16513
+ () => row && !gastada ? laQueTocaAhora(actions, row)?.key : void 0,
16514
+ [actions, row, gastada]
16515
+ );
16382
16516
  const title = recordLabel ?? config.title ?? "Servicio";
16383
16517
  return /* @__PURE__ */ jsxs("div", { className: "px-chat-crud-item", ref: raiz, "data-panel-record": recordId, children: [
16384
16518
  /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__header", children: title }),
@@ -16389,7 +16523,7 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16389
16523
  crud.loading && !row && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__hint", children: "Cargando\u2026" }),
16390
16524
  !crud.loading && !row && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__hint", children: "No se encontr\xF3 el servicio." }),
16391
16525
  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(
16526
+ row && !gastada && config.deadline && typeof row[config.deadline.field] === "string" && /* @__PURE__ */ jsx(
16393
16527
  DeadlineCountdown,
16394
16528
  {
16395
16529
  deadline: row[config.deadline.field],
@@ -16400,7 +16534,7 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16400
16534
  row && /* @__PURE__ */ jsxs("div", { className: "px-chat-crud-item__actions", children: [
16401
16535
  config.detail && /* @__PURE__ */ jsx("button", { type: "button", className: "px-chat-crud-item__action", onClick: () => setDetailRow(row), children: "Ver" }),
16402
16536
  actions.map((a) => {
16403
- const disabled = Boolean(chatIsForeign) || matchesDisabledWhen(a, row);
16537
+ const disabled = gastada || Boolean(chatIsForeign) || matchesDisabledWhen(a, row);
16404
16538
  const motivo = chatIsForeign ? "Esta conversaci\xF3n es de otra persona" : disabledReasonFor(a, row);
16405
16539
  const done = matchesDoneWhen(a, row);
16406
16540
  return /* @__PURE__ */ jsxs(
@@ -16410,13 +16544,18 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16410
16544
  className: [
16411
16545
  "px-chat-crud-item__action",
16412
16546
  a.intent === "danger" ? "is-danger" : "",
16413
- disabled && motivo ? "is-con-motivo" : ""
16547
+ disabled && motivo ? "is-con-motivo" : "",
16548
+ // SPEC-129 T6 — lo hecho se pinta como un acta, no como un botón apagado.
16549
+ gastada ? "is-hecho" : "",
16550
+ // SPEC-127 T4 — señalada, pero solo si de verdad se puede pulsar: destacar una
16551
+ // acción apagada sería dirigir a alguien a una puerta cerrada.
16552
+ a.key === claveQueToca && !disabled ? "is-ahora" : ""
16414
16553
  ].filter(Boolean).join(" "),
16415
16554
  disabled,
16416
16555
  onClick: () => a.form ? setActiveAction({ action: a, row }) : crud.runEndpoints(a.endpoints ?? [a.endpoint], { ...row, id: row[config.idField] }),
16417
16556
  children: [
16418
16557
  /* @__PURE__ */ jsxs("span", { className: "px-chat-crud-item__action-texts", children: [
16419
- /* @__PURE__ */ jsx("span", { children: a.label }),
16558
+ /* @__PURE__ */ jsx("span", { children: gastada && a.doneLabel ? a.doneLabel : a.label }),
16420
16559
  disabled && motivo && /* @__PURE__ */ jsx("span", { className: "px-chat-crud-item__action-reason", children: motivo })
16421
16560
  ] }),
16422
16561
  done && /* @__PURE__ */ jsx(Icon$1, { type: "check-circle", width: 18, height: 18, className: "px-chat-crud-item__done", "aria-label": "Hecho" })
@@ -16425,7 +16564,7 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16425
16564
  a.key
16426
16565
  );
16427
16566
  }),
16428
- actions.length === 0 && !config.detail && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__hint", children: "Sin acciones disponibles." })
16567
+ actions.length === 0 && !config.detail && !sinAcciones && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__hint", children: "Sin acciones disponibles." })
16429
16568
  ] }),
16430
16569
  activeAction && /* @__PURE__ */ jsx(
16431
16570
  ActionFormDrawer,
@@ -16645,7 +16784,7 @@ const ServicesMap = ({ config, eventBus, apiKey, filter, fill }) => {
16645
16784
  ),
16646
16785
  simulated > 0 && ` \xB7 ${config.live?.labels?.simulatedText ?? "incluye posiciones simuladas"}`
16647
16786
  ] }),
16648
- missing > 0 && /* @__PURE__ */ jsx("p", { className: `${CLASS$6}__note`, children: (labels.missingText ?? "{n} sin ubicaci\xF3n registrada").replace("{n}", String(missing)) }),
16787
+ 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
16788
  outside > 0 && /* @__PURE__ */ jsx("p", { className: `${CLASS$6}__note`, children: (labels.outsideText ?? "{n} fuera de la zona (aleja el mapa para verlos)").replace(
16650
16789
  "{n}",
16651
16790
  String(outside)