pixelon 0.3.80 → 0.3.82

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.
@@ -42,6 +42,7 @@ export declare const LUMI_ICON_TYPES: {
42
42
  readonly MAP_PIN: "map-pin";
43
43
  readonly USER_X: "user-x";
44
44
  readonly REFRESH: "refresh";
45
+ readonly TRUCK: "truck";
45
46
  readonly MESSAGE_SQUARE: "message-square";
46
47
  readonly MESSAGE_CIRCLE: "message-circle";
47
48
  readonly MAP: "map";
@@ -5,6 +5,12 @@ export interface SelectOption {
5
5
  label: string;
6
6
  disabled?: boolean;
7
7
  icon?: ReactNode;
8
+ /** SPEC-104: segunda línea, en gris y más pequeña. Para el PORQUÉ de la opción —quién la lleva,
9
+ * a qué distancia, si hay algún aviso— sin que compita con su nombre. */
10
+ sublabel?: string;
11
+ /** SPEC-104: icono por nombre del set de lumi. Existe además de `icon` porque las opciones que
12
+ * llegan de un endpoint son JSON y no pueden traer un nodo de React. */
13
+ iconName?: string;
8
14
  }
9
15
  /** Tipo de valor según el modo */
10
16
  export type SelectValue = string | number | (string | number)[];
@@ -1,6 +1,7 @@
1
1
  import type { SelectOption } from '../../../../../organisms/lumi/DynamicForm/types';
2
2
  import type { ChatEventBus } from '../../../../../../core/eventBus/chatEventBus';
3
3
  import type { CrudFieldOptionsEndpoint } from './types';
4
+ export declare const OPTIONS_LOAD_ERROR = "No se pudieron cargar las opciones. Int\u00E9ntalo de nuevo.";
4
5
  export declare function unwrap(raw: unknown): unknown;
5
6
  export declare function resolveBody(body: Record<string, unknown> | undefined, ctx?: Record<string, unknown>): Record<string, unknown>;
6
7
  export declare function loadOptions(eventBus: ChatEventBus, ep: CrudFieldOptionsEndpoint, ctx?: Record<string, unknown>): Promise<SelectOption[]>;
@@ -89,7 +89,13 @@ export interface RowActionDef {
89
89
  label: string;
90
90
  icon?: string;
91
91
  intent?: 'primary' | 'secondary' | 'danger';
92
- visibleWhen?: RowActionVisibleWhen;
92
+ /** SPEC-103: una condición, o VARIAS unidas por «y». */
93
+ visibleWhen?: RowActionVisibleWhen | RowActionVisibleWhen[];
94
+ /** SPEC-103: la excepción. Si se cumplen TODAS, la acción no se muestra aunque `visibleWhen` dé
95
+ * por buena la fila. Es para las reglas con forma de «se ve en estos estados, salvo este caso»
96
+ * —p.ej. asignar grúa en PENDING_APPROVAL, salvo si quien pide es un invitado que todavía no ha
97
+ * aceptado el precio—, que con condiciones unidas por «y» no se pueden escribir. */
98
+ hiddenWhen?: RowActionVisibleWhen | RowActionVisibleWhen[];
93
99
  /** SPEC-007: la acción se muestra pero queda DESHABILITADA cuando `String(row[field])` está en
94
100
  * `in` (ej. "Iniciar" deshabilitado hasta que `has_inspection` sea true). */
95
101
  disabledWhen?: RowActionVisibleWhen;
@@ -10,6 +10,10 @@ export interface SelectOption {
10
10
  value: string | number;
11
11
  label: string;
12
12
  disabled?: boolean;
13
+ /** SPEC-104: segunda línea en gris — el porqué de la opción, sin competir con su nombre. */
14
+ sublabel?: string;
15
+ /** SPEC-104: nombre de icono del set de lumi (no un nodo: esto viaja como JSON desde el backend). */
16
+ iconName?: string;
13
17
  }
14
18
  /**
15
19
  * Configuración de un campo del formulario
package/dist/index.esm.js CHANGED
@@ -5739,6 +5739,9 @@ const LUMI_ICON_TYPES = {
5739
5739
  // Iconos para Agenda
5740
5740
  USER_X: "user-x",
5741
5741
  REFRESH: "refresh",
5742
+ // SPEC-104 — grúas: el selector de despacho necesitaba distinguir de un vistazo qué es cada
5743
+ // opción, y no había ningún icono de vehículo en el set.
5744
+ TRUCK: "truck",
5742
5745
  // Iconos para Chat
5743
5746
  MESSAGE_SQUARE: "message-square",
5744
5747
  MESSAGE_CIRCLE: "message-circle",
@@ -6449,6 +6452,27 @@ const BuildingIcon = (props) => /* @__PURE__ */ jsxs(
6449
6452
  ]
6450
6453
  }
6451
6454
  );
6455
+ const TruckIcon = (props) => /* @__PURE__ */ jsxs(
6456
+ "svg",
6457
+ {
6458
+ width: "16",
6459
+ height: "16",
6460
+ viewBox: "0 0 24 24",
6461
+ fill: "none",
6462
+ stroke: "currentColor",
6463
+ strokeWidth: "2",
6464
+ strokeLinecap: "round",
6465
+ strokeLinejoin: "round",
6466
+ ...props,
6467
+ children: [
6468
+ /* @__PURE__ */ jsx("path", { d: "M2 17V8h10v9" }),
6469
+ /* @__PURE__ */ jsx("path", { d: "M12 11h4l3 3v3" }),
6470
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "18", r: "2" }),
6471
+ /* @__PURE__ */ jsx("circle", { cx: "17", cy: "18", r: "2" }),
6472
+ /* @__PURE__ */ jsx("path", { d: "M9 18h6" })
6473
+ ]
6474
+ }
6475
+ );
6452
6476
  const MapPinIcon = (props) => /* @__PURE__ */ jsxs(
6453
6477
  "svg",
6454
6478
  {
@@ -6567,6 +6591,7 @@ const lumiIconMap = {
6567
6591
  // Iconos para Servicios
6568
6592
  [LUMI_ICON_TYPES.BUILDING]: BuildingIcon,
6569
6593
  [LUMI_ICON_TYPES.MAP_PIN]: MapPinIcon,
6594
+ [LUMI_ICON_TYPES.TRUCK]: TruckIcon,
6570
6595
  // Iconos para Agenda
6571
6596
  [LUMI_ICON_TYPES.USER_X]: UserXIcon,
6572
6597
  [LUMI_ICON_TYPES.REFRESH]: RefreshIcon,
@@ -7738,8 +7763,11 @@ const Select$1 = forwardRef(
7738
7763
  `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__checkbox`,
7739
7764
  isSelected && `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__checkbox--checked`
7740
7765
  ].filter(Boolean).join(" "), children: isSelected && /* @__PURE__ */ jsx(Icon, { type: LUMI_ICON_TYPES.CHECK, size: "small" }) }) }),
7741
- option.icon && /* @__PURE__ */ jsx("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-icon`, children: option.icon }),
7742
- /* @__PURE__ */ jsx("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-label`, children: option.label }),
7766
+ (option.icon || option.iconName) && /* @__PURE__ */ jsx("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-icon`, children: option.icon ?? /* @__PURE__ */ jsx(Icon, { type: option.iconName }) }),
7767
+ /* @__PURE__ */ jsxs("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-texts`, children: [
7768
+ /* @__PURE__ */ jsx("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-label`, children: option.label }),
7769
+ option.sublabel && /* @__PURE__ */ jsx("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-sublabel`, children: option.sublabel })
7770
+ ] }),
7743
7771
  mode === "single" && isSelected && /* @__PURE__ */ jsx("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-check`, children: /* @__PURE__ */ jsx(Icon, { type: LUMI_ICON_TYPES.CHECK, size: "small" }) })
7744
7772
  ]
7745
7773
  },
@@ -14083,6 +14111,7 @@ function getFormatter(key) {
14083
14111
  return key ? CRUD_FORMATTERS[key] : void 0;
14084
14112
  }
14085
14113
 
14114
+ const OPTIONS_LOAD_ERROR = "No se pudieron cargar las opciones. Int\xE9ntalo de nuevo.";
14086
14115
  function unwrap$3(raw) {
14087
14116
  if (raw && typeof raw === "object" && "response" in raw) {
14088
14117
  return raw.response;
@@ -14111,7 +14140,9 @@ async function loadOptions(eventBus, ep, ctx) {
14111
14140
  const arr = Array.isArray(data) ? data : [];
14112
14141
  return arr.map((item) => ({
14113
14142
  value: String(item[ep.value_field] ?? ""),
14114
- label: String(item[ep.label_field] ?? "")
14143
+ label: String(item[ep.label_field] ?? ""),
14144
+ ...item.sublabel ? { sublabel: String(item.sublabel) } : {},
14145
+ ...item.icon ? { iconName: String(item.icon) } : {}
14115
14146
  }));
14116
14147
  }
14117
14148
 
@@ -14418,6 +14449,7 @@ function ActionFormDrawer({ action, row, idField, eventBus, onExecute, onClose }
14418
14449
  const fileFields = useMemo(() => formFields.filter((f) => f.moleculeType === "FILE"), [formFields]);
14419
14450
  const normalFields = useMemo(() => formFields.filter((f) => f.moleculeType !== "FILE"), [formFields]);
14420
14451
  const [optionsByField, setOptionsByField] = useState({});
14452
+ const [optionsErrorByField, setOptionsErrorByField] = useState({});
14421
14453
  const [loadingOptions, setLoadingOptions] = useState(false);
14422
14454
  const [submitting, setSubmitting] = useState(false);
14423
14455
  const [fileEntries, setFileEntries] = useState({});
@@ -14431,14 +14463,17 @@ function ActionFormDrawer({ action, row, idField, eventBus, onExecute, onClose }
14431
14463
  toLoad.map(async (f) => {
14432
14464
  try {
14433
14465
  const opts = await loadOptions(eventBus, f.optionsEndpoint, { ...row, id: row[idField] });
14434
- return [f.name, opts];
14466
+ return [f.name, opts, null];
14435
14467
  } catch {
14436
- return [f.name, []];
14468
+ return [f.name, [], OPTIONS_LOAD_ERROR];
14437
14469
  }
14438
14470
  })
14439
14471
  ).then((entries) => {
14440
14472
  if (!alive) return;
14441
- setOptionsByField(Object.fromEntries(entries));
14473
+ setOptionsByField(Object.fromEntries(entries.map(([n, o]) => [n, o])));
14474
+ setOptionsErrorByField(
14475
+ Object.fromEntries(entries.filter(([, , e]) => e).map(([n, , e]) => [n, e]))
14476
+ );
14442
14477
  setLoadingOptions(false);
14443
14478
  });
14444
14479
  return () => {
@@ -14452,9 +14487,12 @@ function ActionFormDrawer({ action, row, idField, eventBus, onExecute, onClose }
14452
14487
  moleculeType: CRUD_TO_FORM_MOLECULE[f.moleculeType] ?? "INPUT",
14453
14488
  required: f.required,
14454
14489
  placeholder: f.placeholder,
14455
- options: f.options ?? optionsByField[f.name]
14490
+ options: f.options ?? optionsByField[f.name],
14491
+ // SPEC-106 T8 — `uiProps` llega tal cual a la molécula, así que el desplegable dice lo
14492
+ // 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] } } : {}
14456
14494
  })),
14457
- [normalFields, optionsByField]
14495
+ [normalFields, optionsByField, optionsErrorByField]
14458
14496
  );
14459
14497
  const validationSchema = useMemo(() => buildCrudYupSchema(normalFields), [normalFields]);
14460
14498
  const uploadOne = async (f, file, idx) => {
@@ -15889,7 +15927,7 @@ function chatLinkFormatter(field, eventBus) {
15889
15927
  return createElement(ChatLinkCell, { targets, eventBus });
15890
15928
  };
15891
15929
  }
15892
- function toRenderField(f, loaded, eventBus, mapsApiKey) {
15930
+ function toRenderField(f, loaded, eventBus, mapsApiKey, loadError) {
15893
15931
  const fmt = getFormatter(f.format);
15894
15932
  const options = f.options ?? loaded[f.name];
15895
15933
  if (f.moleculeType === "LOCATION") {
@@ -15934,13 +15972,18 @@ function toRenderField(f, loaded, eventBus, mapsApiKey) {
15934
15972
  columnWidth: f.columnWidth,
15935
15973
  fixed: f.fixed,
15936
15974
  align: f.align,
15937
- uiProps: options ? { options } : void 0,
15975
+ // SPEC-106 T8 — con un fallo de carga, el desplegable dice que no se pudo preguntar en vez
15976
+ // de «no hay opciones disponibles», que es una respuesta y no la tenemos.
15977
+ uiProps: loadError ? { options: options ?? [], notFoundContent: loadError } : options ? { options } : void 0,
15938
15978
  formatValue
15939
15979
  };
15940
15980
  }
15981
+ const cumpleCondicion = (c, row) => c.in.includes(String(row[c.field]));
15982
+ const cumplenTodas = (c, row) => (Array.isArray(c) ? c : [c]).every((x) => cumpleCondicion(x, row));
15941
15983
  function matchesVisibleWhen(a, row) {
15984
+ if (a.hiddenWhen && cumplenTodas(a.hiddenWhen, row)) return false;
15942
15985
  if (!a.visibleWhen) return true;
15943
- return a.visibleWhen.in.includes(String(row[a.visibleWhen.field]));
15986
+ return cumplenTodas(a.visibleWhen, row);
15944
15987
  }
15945
15988
  function matchesDisabledWhen(a, row) {
15946
15989
  if (!a.disabledWhen) return false;
@@ -15981,6 +16024,7 @@ function ReactiveCrud({ config, eventBus, autoOpenCreate, prefill, onCreated, on
15981
16024
  const [activeAction, setActiveAction] = useState(null);
15982
16025
  const [detailRow, setDetailRow] = useState(null);
15983
16026
  const [optionsByField, setOptionsByField] = useState({});
16027
+ const [optionsErrorByField, setOptionsErrorByField] = useState({});
15984
16028
  useEffect(() => {
15985
16029
  let alive = true;
15986
16030
  const toLoad = config.fields.filter((f) => f.optionsEndpoint);
@@ -15988,21 +16032,25 @@ function ReactiveCrud({ config, eventBus, autoOpenCreate, prefill, onCreated, on
15988
16032
  void Promise.all(
15989
16033
  toLoad.map(async (f) => {
15990
16034
  try {
15991
- return [f.name, await loadOptions(eventBus, f.optionsEndpoint)];
16035
+ return [f.name, await loadOptions(eventBus, f.optionsEndpoint), null];
15992
16036
  } catch {
15993
- return [f.name, []];
16037
+ return [f.name, [], OPTIONS_LOAD_ERROR];
15994
16038
  }
15995
16039
  })
15996
16040
  ).then((entries) => {
15997
- if (alive) setOptionsByField(Object.fromEntries(entries));
16041
+ if (!alive) return;
16042
+ setOptionsByField(Object.fromEntries(entries.map(([n, o]) => [n, o])));
16043
+ setOptionsErrorByField(
16044
+ Object.fromEntries(entries.filter(([, , e]) => e).map(([n, , e]) => [n, e]))
16045
+ );
15998
16046
  });
15999
16047
  return () => {
16000
16048
  alive = false;
16001
16049
  };
16002
16050
  }, [eventBus, config.fields]);
16003
16051
  const renderByFields = useMemo(
16004
- () => config.fields.map((f) => toRenderField(f, optionsByField, eventBus, googleMapsApiKey)),
16005
- [config.fields, optionsByField, eventBus]
16052
+ () => config.fields.map((f) => toRenderField(f, optionsByField, eventBus, googleMapsApiKey, optionsErrorByField[f.name])),
16053
+ [config.fields, optionsByField, optionsErrorByField, eventBus]
16006
16054
  );
16007
16055
  const validationSchema = useMemo(() => buildCrudYupSchema(config.fields), [config.fields]);
16008
16056
  const rowActions = useMemo(() => {
@@ -16297,12 +16345,27 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16297
16345
  () => crud.rows.find((r) => String(r[config.idField]) === recordId),
16298
16346
  [crud.rows, config.idField, recordId]
16299
16347
  );
16348
+ const raiz = useRef(null);
16349
+ const [esUltima, setEsUltima] = useState(true);
16350
+ useEffect(() => {
16351
+ const calcular = () => {
16352
+ const el = raiz.current;
16353
+ if (!el) return;
16354
+ const hermanas = document.querySelectorAll(`[data-panel-record="${recordId}"]`);
16355
+ setEsUltima(hermanas.length === 0 || hermanas[hermanas.length - 1] === el);
16356
+ };
16357
+ calcular();
16358
+ const observador = new MutationObserver(calcular);
16359
+ observador.observe(document.body, { childList: true, subtree: true });
16360
+ return () => observador.disconnect();
16361
+ }, [recordId]);
16362
+ const gastada = !esUltima;
16300
16363
  const actions = useMemo(() => {
16301
- if (!row) return [];
16364
+ if (!row || gastada) return [];
16302
16365
  return (config.rowActions ?? []).filter((a) => !allowed || allowed.includes(a.key)).filter((a) => matchesVisibleWhen(a, row));
16303
- }, [config.rowActions, row, allowed]);
16366
+ }, [config.rowActions, row, allowed, gastada]);
16304
16367
  const title = recordLabel ?? config.title ?? "Servicio";
16305
- return /* @__PURE__ */ jsxs("div", { className: "px-chat-crud-item", children: [
16368
+ return /* @__PURE__ */ jsxs("div", { className: "px-chat-crud-item", ref: raiz, "data-panel-record": recordId, children: [
16306
16369
  /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__header", children: title }),
16307
16370
  crud.actionError && /* @__PURE__ */ jsxs("div", { className: "px-chat-crud-item__error", role: "alert", children: [
16308
16371
  /* @__PURE__ */ jsx("span", { children: crud.actionError }),
@@ -16310,6 +16373,7 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16310
16373
  ] }),
16311
16374
  crud.loading && !row && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__hint", children: "Cargando\u2026" }),
16312
16375
  !crud.loading && !row && /* @__PURE__ */ jsx("div", { className: "px-chat-crud-item__hint", children: "No se encontr\xF3 el servicio." }),
16376
+ 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." }),
16313
16377
  row && config.deadline && typeof row[config.deadline.field] === "string" && /* @__PURE__ */ jsx(
16314
16378
  DeadlineCountdown,
16315
16379
  {
package/dist/index.js CHANGED
@@ -5760,6 +5760,9 @@ const LUMI_ICON_TYPES = {
5760
5760
  // Iconos para Agenda
5761
5761
  USER_X: "user-x",
5762
5762
  REFRESH: "refresh",
5763
+ // SPEC-104 — grúas: el selector de despacho necesitaba distinguir de un vistazo qué es cada
5764
+ // opción, y no había ningún icono de vehículo en el set.
5765
+ TRUCK: "truck",
5763
5766
  // Iconos para Chat
5764
5767
  MESSAGE_SQUARE: "message-square",
5765
5768
  MESSAGE_CIRCLE: "message-circle",
@@ -6470,6 +6473,27 @@ const BuildingIcon = (props) => /* @__PURE__ */ jsxRuntime.jsxs(
6470
6473
  ]
6471
6474
  }
6472
6475
  );
6476
+ const TruckIcon = (props) => /* @__PURE__ */ jsxRuntime.jsxs(
6477
+ "svg",
6478
+ {
6479
+ width: "16",
6480
+ height: "16",
6481
+ viewBox: "0 0 24 24",
6482
+ fill: "none",
6483
+ stroke: "currentColor",
6484
+ strokeWidth: "2",
6485
+ strokeLinecap: "round",
6486
+ strokeLinejoin: "round",
6487
+ ...props,
6488
+ children: [
6489
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 17V8h10v9" }),
6490
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 11h4l3 3v3" }),
6491
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "7", cy: "18", r: "2" }),
6492
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "17", cy: "18", r: "2" }),
6493
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 18h6" })
6494
+ ]
6495
+ }
6496
+ );
6473
6497
  const MapPinIcon = (props) => /* @__PURE__ */ jsxRuntime.jsxs(
6474
6498
  "svg",
6475
6499
  {
@@ -6588,6 +6612,7 @@ const lumiIconMap = {
6588
6612
  // Iconos para Servicios
6589
6613
  [LUMI_ICON_TYPES.BUILDING]: BuildingIcon,
6590
6614
  [LUMI_ICON_TYPES.MAP_PIN]: MapPinIcon,
6615
+ [LUMI_ICON_TYPES.TRUCK]: TruckIcon,
6591
6616
  // Iconos para Agenda
6592
6617
  [LUMI_ICON_TYPES.USER_X]: UserXIcon,
6593
6618
  [LUMI_ICON_TYPES.REFRESH]: RefreshIcon,
@@ -7759,8 +7784,11 @@ const Select$1 = React.forwardRef(
7759
7784
  `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__checkbox`,
7760
7785
  isSelected && `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__checkbox--checked`
7761
7786
  ].filter(Boolean).join(" "), children: isSelected && /* @__PURE__ */ jsxRuntime.jsx(Icon, { type: LUMI_ICON_TYPES.CHECK, size: "small" }) }) }),
7762
- option.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-icon`, children: option.icon }),
7763
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-label`, children: option.label }),
7787
+ (option.icon || option.iconName) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-icon`, children: option.icon ?? /* @__PURE__ */ jsxRuntime.jsx(Icon, { type: option.iconName }) }),
7788
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-texts`, children: [
7789
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-label`, children: option.label }),
7790
+ option.sublabel && /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-sublabel`, children: option.sublabel })
7791
+ ] }),
7764
7792
  mode === "single" && isSelected && /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${COMPONENT_PREFIX}${COMPONENT_TYPES.SELECT}__option-check`, children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { type: LUMI_ICON_TYPES.CHECK, size: "small" }) })
7765
7793
  ]
7766
7794
  },
@@ -14104,6 +14132,7 @@ function getFormatter(key) {
14104
14132
  return key ? CRUD_FORMATTERS[key] : void 0;
14105
14133
  }
14106
14134
 
14135
+ const OPTIONS_LOAD_ERROR = "No se pudieron cargar las opciones. Int\xE9ntalo de nuevo.";
14107
14136
  function unwrap$3(raw) {
14108
14137
  if (raw && typeof raw === "object" && "response" in raw) {
14109
14138
  return raw.response;
@@ -14132,7 +14161,9 @@ async function loadOptions(eventBus, ep, ctx) {
14132
14161
  const arr = Array.isArray(data) ? data : [];
14133
14162
  return arr.map((item) => ({
14134
14163
  value: String(item[ep.value_field] ?? ""),
14135
- label: String(item[ep.label_field] ?? "")
14164
+ label: String(item[ep.label_field] ?? ""),
14165
+ ...item.sublabel ? { sublabel: String(item.sublabel) } : {},
14166
+ ...item.icon ? { iconName: String(item.icon) } : {}
14136
14167
  }));
14137
14168
  }
14138
14169
 
@@ -14439,6 +14470,7 @@ function ActionFormDrawer({ action, row, idField, eventBus, onExecute, onClose }
14439
14470
  const fileFields = React.useMemo(() => formFields.filter((f) => f.moleculeType === "FILE"), [formFields]);
14440
14471
  const normalFields = React.useMemo(() => formFields.filter((f) => f.moleculeType !== "FILE"), [formFields]);
14441
14472
  const [optionsByField, setOptionsByField] = React.useState({});
14473
+ const [optionsErrorByField, setOptionsErrorByField] = React.useState({});
14442
14474
  const [loadingOptions, setLoadingOptions] = React.useState(false);
14443
14475
  const [submitting, setSubmitting] = React.useState(false);
14444
14476
  const [fileEntries, setFileEntries] = React.useState({});
@@ -14452,14 +14484,17 @@ function ActionFormDrawer({ action, row, idField, eventBus, onExecute, onClose }
14452
14484
  toLoad.map(async (f) => {
14453
14485
  try {
14454
14486
  const opts = await loadOptions(eventBus, f.optionsEndpoint, { ...row, id: row[idField] });
14455
- return [f.name, opts];
14487
+ return [f.name, opts, null];
14456
14488
  } catch {
14457
- return [f.name, []];
14489
+ return [f.name, [], OPTIONS_LOAD_ERROR];
14458
14490
  }
14459
14491
  })
14460
14492
  ).then((entries) => {
14461
14493
  if (!alive) return;
14462
- setOptionsByField(Object.fromEntries(entries));
14494
+ setOptionsByField(Object.fromEntries(entries.map(([n, o]) => [n, o])));
14495
+ setOptionsErrorByField(
14496
+ Object.fromEntries(entries.filter(([, , e]) => e).map(([n, , e]) => [n, e]))
14497
+ );
14463
14498
  setLoadingOptions(false);
14464
14499
  });
14465
14500
  return () => {
@@ -14473,9 +14508,12 @@ function ActionFormDrawer({ action, row, idField, eventBus, onExecute, onClose }
14473
14508
  moleculeType: CRUD_TO_FORM_MOLECULE[f.moleculeType] ?? "INPUT",
14474
14509
  required: f.required,
14475
14510
  placeholder: f.placeholder,
14476
- options: f.options ?? optionsByField[f.name]
14511
+ options: f.options ?? optionsByField[f.name],
14512
+ // SPEC-106 T8 — `uiProps` llega tal cual a la molécula, así que el desplegable dice lo
14513
+ // 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] } } : {}
14477
14515
  })),
14478
- [normalFields, optionsByField]
14516
+ [normalFields, optionsByField, optionsErrorByField]
14479
14517
  );
14480
14518
  const validationSchema = React.useMemo(() => buildCrudYupSchema(normalFields), [normalFields]);
14481
14519
  const uploadOne = async (f, file, idx) => {
@@ -15910,7 +15948,7 @@ function chatLinkFormatter(field, eventBus) {
15910
15948
  return React.createElement(ChatLinkCell, { targets, eventBus });
15911
15949
  };
15912
15950
  }
15913
- function toRenderField(f, loaded, eventBus, mapsApiKey) {
15951
+ function toRenderField(f, loaded, eventBus, mapsApiKey, loadError) {
15914
15952
  const fmt = getFormatter(f.format);
15915
15953
  const options = f.options ?? loaded[f.name];
15916
15954
  if (f.moleculeType === "LOCATION") {
@@ -15955,13 +15993,18 @@ function toRenderField(f, loaded, eventBus, mapsApiKey) {
15955
15993
  columnWidth: f.columnWidth,
15956
15994
  fixed: f.fixed,
15957
15995
  align: f.align,
15958
- uiProps: options ? { options } : void 0,
15996
+ // SPEC-106 T8 — con un fallo de carga, el desplegable dice que no se pudo preguntar en vez
15997
+ // de «no hay opciones disponibles», que es una respuesta y no la tenemos.
15998
+ uiProps: loadError ? { options: options ?? [], notFoundContent: loadError } : options ? { options } : void 0,
15959
15999
  formatValue
15960
16000
  };
15961
16001
  }
16002
+ const cumpleCondicion = (c, row) => c.in.includes(String(row[c.field]));
16003
+ const cumplenTodas = (c, row) => (Array.isArray(c) ? c : [c]).every((x) => cumpleCondicion(x, row));
15962
16004
  function matchesVisibleWhen(a, row) {
16005
+ if (a.hiddenWhen && cumplenTodas(a.hiddenWhen, row)) return false;
15963
16006
  if (!a.visibleWhen) return true;
15964
- return a.visibleWhen.in.includes(String(row[a.visibleWhen.field]));
16007
+ return cumplenTodas(a.visibleWhen, row);
15965
16008
  }
15966
16009
  function matchesDisabledWhen(a, row) {
15967
16010
  if (!a.disabledWhen) return false;
@@ -16002,6 +16045,7 @@ function ReactiveCrud({ config, eventBus, autoOpenCreate, prefill, onCreated, on
16002
16045
  const [activeAction, setActiveAction] = React.useState(null);
16003
16046
  const [detailRow, setDetailRow] = React.useState(null);
16004
16047
  const [optionsByField, setOptionsByField] = React.useState({});
16048
+ const [optionsErrorByField, setOptionsErrorByField] = React.useState({});
16005
16049
  React.useEffect(() => {
16006
16050
  let alive = true;
16007
16051
  const toLoad = config.fields.filter((f) => f.optionsEndpoint);
@@ -16009,21 +16053,25 @@ function ReactiveCrud({ config, eventBus, autoOpenCreate, prefill, onCreated, on
16009
16053
  void Promise.all(
16010
16054
  toLoad.map(async (f) => {
16011
16055
  try {
16012
- return [f.name, await loadOptions(eventBus, f.optionsEndpoint)];
16056
+ return [f.name, await loadOptions(eventBus, f.optionsEndpoint), null];
16013
16057
  } catch {
16014
- return [f.name, []];
16058
+ return [f.name, [], OPTIONS_LOAD_ERROR];
16015
16059
  }
16016
16060
  })
16017
16061
  ).then((entries) => {
16018
- if (alive) setOptionsByField(Object.fromEntries(entries));
16062
+ if (!alive) return;
16063
+ setOptionsByField(Object.fromEntries(entries.map(([n, o]) => [n, o])));
16064
+ setOptionsErrorByField(
16065
+ Object.fromEntries(entries.filter(([, , e]) => e).map(([n, , e]) => [n, e]))
16066
+ );
16019
16067
  });
16020
16068
  return () => {
16021
16069
  alive = false;
16022
16070
  };
16023
16071
  }, [eventBus, config.fields]);
16024
16072
  const renderByFields = React.useMemo(
16025
- () => config.fields.map((f) => toRenderField(f, optionsByField, eventBus, googleMapsApiKey)),
16026
- [config.fields, optionsByField, eventBus]
16073
+ () => config.fields.map((f) => toRenderField(f, optionsByField, eventBus, googleMapsApiKey, optionsErrorByField[f.name])),
16074
+ [config.fields, optionsByField, optionsErrorByField, eventBus]
16027
16075
  );
16028
16076
  const validationSchema = React.useMemo(() => buildCrudYupSchema(config.fields), [config.fields]);
16029
16077
  const rowActions = React.useMemo(() => {
@@ -16318,12 +16366,27 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16318
16366
  () => crud.rows.find((r) => String(r[config.idField]) === recordId),
16319
16367
  [crud.rows, config.idField, recordId]
16320
16368
  );
16369
+ const raiz = React.useRef(null);
16370
+ const [esUltima, setEsUltima] = React.useState(true);
16371
+ React.useEffect(() => {
16372
+ const calcular = () => {
16373
+ const el = raiz.current;
16374
+ if (!el) return;
16375
+ const hermanas = document.querySelectorAll(`[data-panel-record="${recordId}"]`);
16376
+ setEsUltima(hermanas.length === 0 || hermanas[hermanas.length - 1] === el);
16377
+ };
16378
+ calcular();
16379
+ const observador = new MutationObserver(calcular);
16380
+ observador.observe(document.body, { childList: true, subtree: true });
16381
+ return () => observador.disconnect();
16382
+ }, [recordId]);
16383
+ const gastada = !esUltima;
16321
16384
  const actions = React.useMemo(() => {
16322
- if (!row) return [];
16385
+ if (!row || gastada) return [];
16323
16386
  return (config.rowActions ?? []).filter((a) => !allowed || allowed.includes(a.key)).filter((a) => matchesVisibleWhen(a, row));
16324
- }, [config.rowActions, row, allowed]);
16387
+ }, [config.rowActions, row, allowed, gastada]);
16325
16388
  const title = recordLabel ?? config.title ?? "Servicio";
16326
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-crud-item", children: [
16389
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-crud-item", ref: raiz, "data-panel-record": recordId, children: [
16327
16390
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-crud-item__header", children: title }),
16328
16391
  crud.actionError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-crud-item__error", role: "alert", children: [
16329
16392
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: crud.actionError }),
@@ -16331,6 +16394,7 @@ function ServiceActionsPanel({ config, recordId, recordLabel, allowed, eventBus
16331
16394
  ] }),
16332
16395
  crud.loading && !row && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-crud-item__hint", children: "Cargando\u2026" }),
16333
16396
  !crud.loading && !row && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-crud-item__hint", children: "No se encontr\xF3 el servicio." }),
16397
+ 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." }),
16334
16398
  row && config.deadline && typeof row[config.deadline.field] === "string" && /* @__PURE__ */ jsxRuntime.jsx(
16335
16399
  DeadlineCountdown,
16336
16400
  {