pixelon 0.3.27 → 0.3.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1832,6 +1832,114 @@ function useComponentInteraction({
1832
1832
  return { emitInteraction, isConfirmed };
1833
1833
  }
1834
1834
 
1835
+ const FETCH_TIMEOUT_MS = 12e4;
1836
+ function fetchViaEventBus(eventBus, service, url, method, body) {
1837
+ const requestId = globalThis.crypto?.randomUUID?.() ?? "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1838
+ const r = Math.random() * 16 | 0;
1839
+ return (c === "x" ? r : r & 3 | 8).toString(16);
1840
+ });
1841
+ return new Promise((resolve, reject) => {
1842
+ const timeoutId = setTimeout(() => {
1843
+ unsubscribe();
1844
+ reject(new Error(`Timeout: no response for ${url} after ${FETCH_TIMEOUT_MS}ms`));
1845
+ }, FETCH_TIMEOUT_MS);
1846
+ const unsubscribe = eventBus.on(CHAT_EVENT.FETCH_RESPONSE, (response) => {
1847
+ if (response.requestId !== requestId) return;
1848
+ clearTimeout(timeoutId);
1849
+ unsubscribe();
1850
+ if (response.httpError) {
1851
+ reject(new Error(response.httpError));
1852
+ } else {
1853
+ resolve(response.data);
1854
+ }
1855
+ });
1856
+ eventBus.emit(CHAT_EVENT.FETCH_REQUEST, {
1857
+ requestId,
1858
+ service,
1859
+ url,
1860
+ method,
1861
+ ...body ? { body } : {}
1862
+ });
1863
+ });
1864
+ }
1865
+
1866
+ function mapResponseToOptions(data, endpoint) {
1867
+ let items = [];
1868
+ if (endpoint.response_path) {
1869
+ const parts = endpoint.response_path.split(".");
1870
+ let current = data;
1871
+ for (const part of parts) {
1872
+ if (current != null && typeof current === "object") {
1873
+ current = current[part];
1874
+ } else {
1875
+ return [];
1876
+ }
1877
+ }
1878
+ items = Array.isArray(current) ? current : [];
1879
+ } else if (Array.isArray(data)) {
1880
+ items = data;
1881
+ } else {
1882
+ return [];
1883
+ }
1884
+ return items.filter((item) => item != null && typeof item === "object").map((item) => ({
1885
+ value: String(item[endpoint.value_field] ?? ""),
1886
+ label: String(item[endpoint.label_field] ?? "")
1887
+ }));
1888
+ }
1889
+
1890
+ function normalizeBody(body) {
1891
+ if (!body) return void 0;
1892
+ const normalized = { ...body };
1893
+ for (const [key, val] of Object.entries(normalized)) {
1894
+ if (/s_id$/.test(key) && typeof val === "string") {
1895
+ normalized[key] = [val];
1896
+ }
1897
+ }
1898
+ return normalized;
1899
+ }
1900
+ function useSelectEndpoint(endpoint, eventBus) {
1901
+ const [options, setOptions] = React.useState([]);
1902
+ const [status, setStatus] = React.useState(endpoint ? "loading" : "idle");
1903
+ const [errorMessage, setErrorMessage] = React.useState("");
1904
+ const fetchedRef = React.useRef(false);
1905
+ const endpointRef = React.useRef(endpoint);
1906
+ endpointRef.current = endpoint;
1907
+ const doFetch = React.useCallback(async () => {
1908
+ const ep = endpointRef.current;
1909
+ if (!ep || fetchedRef.current) return;
1910
+ fetchedRef.current = true;
1911
+ if (!eventBus) {
1912
+ setOptions([
1913
+ { value: "mock-1", label: "Mock Option 1" },
1914
+ { value: "mock-2", label: "Mock Option 2" }
1915
+ ]);
1916
+ setStatus("idle");
1917
+ return;
1918
+ }
1919
+ try {
1920
+ const body = normalizeBody(ep.body);
1921
+ if (ep.method.toUpperCase() === "POST" && !body) {
1922
+ setErrorMessage("Faltan par\xE1metros para cargar opciones");
1923
+ setStatus("error");
1924
+ return;
1925
+ }
1926
+ const data = await fetchViaEventBus(eventBus, ep.service, ep.url, ep.method, body);
1927
+ const mapped = mapResponseToOptions(data, ep);
1928
+ setOptions(mapped);
1929
+ setStatus("idle");
1930
+ } catch (err) {
1931
+ setErrorMessage(err instanceof Error ? err.message : "Error al cargar opciones");
1932
+ setStatus("error");
1933
+ }
1934
+ }, [eventBus]);
1935
+ React.useEffect(() => {
1936
+ if (endpoint && !fetchedRef.current) {
1937
+ doFetch();
1938
+ }
1939
+ }, [endpoint, doFetch]);
1940
+ return { options, status, errorMessage };
1941
+ }
1942
+
1835
1943
  const MOBILE_BREAKPOINT$4 = 640;
1836
1944
  const useIsMobile$5 = () => {
1837
1945
  const [isMobile, setIsMobile] = React.useState(
@@ -1855,10 +1963,15 @@ const SelectWithConfirm = ({
1855
1963
  const [searchValue, setSearchValue] = React.useState("");
1856
1964
  const searchInputRef = React.useRef(null);
1857
1965
  const isMobile = useIsMobile$5();
1858
- const { field_name, value, ...componentProps } = component.props;
1859
- const options = componentProps.options || [];
1966
+ const { field_name, value, endpoint: endpointProp, ...componentProps } = component.props;
1967
+ const endpoint = endpointProp;
1968
+ const { options: endpointOptions, status: endpointStatus, errorMessage } = useSelectEndpoint(endpoint, eventBus);
1969
+ const staticOptions = componentProps.options || [];
1970
+ const options = endpoint ? endpointOptions : staticOptions;
1860
1971
  const placeholder = componentProps.placeholder || "Seleccionar...";
1861
1972
  const label = componentProps.label || "";
1973
+ const isLoading = endpointStatus === "loading";
1974
+ const isError = endpointStatus === "error";
1862
1975
  const { emitInteraction, isConfirmed } = useComponentInteraction({
1863
1976
  eventBus,
1864
1977
  fieldName: component.props.field_name,
@@ -1878,6 +1991,18 @@ const SelectWithConfirm = ({
1878
1991
  }
1879
1992
  }, [isDrawerOpen]);
1880
1993
  if (isConfirmed) return null;
1994
+ if (isLoading) {
1995
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-select-with-confirm", children: [
1996
+ label && /* @__PURE__ */ jsxRuntime.jsx("label", { className: "px-chat-select-with-confirm__label", children: label }),
1997
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-select-with-confirm__loading", children: "Cargando opciones..." })
1998
+ ] });
1999
+ }
2000
+ if (isError) {
2001
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-select-with-confirm", children: [
2002
+ label && /* @__PURE__ */ jsxRuntime.jsx("label", { className: "px-chat-select-with-confirm__label", children: label }),
2003
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-select-with-confirm__error", children: errorMessage })
2004
+ ] });
2005
+ }
1881
2006
  if (isMobile) {
1882
2007
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-select-with-confirm", children: [
1883
2008
  label && /* @__PURE__ */ jsxRuntime.jsx("label", { className: "px-chat-select-with-confirm__label", children: label }),
@@ -1997,8 +2122,8 @@ const SelectWithConfirm = ({
1997
2122
  /* @__PURE__ */ jsxRuntime.jsx(
1998
2123
  Select$2,
1999
2124
  {
2000
- options: [],
2001
2125
  ...componentProps,
2126
+ options,
2002
2127
  variant: "chat",
2003
2128
  mode: "single",
2004
2129
  showSearch: true,
@@ -3158,7 +3283,7 @@ const SelectField$1 = ({ field, value, disabled, loading, options, onChange, dra
3158
3283
  ),
3159
3284
  searchValue && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "px-chat-select-drawer__search-clear", onClick: () => setSearchValue(""), children: /* @__PURE__ */ jsxRuntime.jsx(Icon$1, { type: ICON_TYPES.CLOSE, size: "small" }) })
3160
3285
  ] }),
3161
- /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "px-chat-select-drawer__options", role: "listbox", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("li", { className: "px-chat-select-drawer__empty", children: "No hay opciones disponibles" }) : filteredOptions.map((opt) => {
3286
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "px-chat-select-drawer__options", role: "listbox", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("li", { className: "px-chat-select-drawer__empty", children: field.empty_message || "No hay opciones disponibles" }) : filteredOptions.map((opt) => {
3162
3287
  const isSelected = opt.value === value;
3163
3288
  return /* @__PURE__ */ jsxRuntime.jsxs(
3164
3289
  "li",
@@ -3279,7 +3404,7 @@ const SelectMultipleField = ({ field, value, disabled, loading, options, onChang
3279
3404
  ),
3280
3405
  searchValue && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "px-chat-select-drawer__search-clear", onClick: () => setSearchValue(""), children: /* @__PURE__ */ jsxRuntime.jsx(Icon$1, { type: ICON_TYPES.CLOSE, size: "small" }) })
3281
3406
  ] }),
3282
- /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "px-chat-select-drawer__options", role: "listbox", "aria-multiselectable": "true", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("li", { className: "px-chat-select-drawer__empty", children: "No hay opciones disponibles" }) : filteredOptions.map((opt) => {
3407
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "px-chat-select-drawer__options", role: "listbox", "aria-multiselectable": "true", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("li", { className: "px-chat-select-drawer__empty", children: field.empty_message || "No hay opciones disponibles" }) : filteredOptions.map((opt) => {
3283
3408
  const isChecked = selected.includes(opt.value);
3284
3409
  return /* @__PURE__ */ jsxRuntime.jsxs(
3285
3410
  "li",
@@ -3377,6 +3502,200 @@ const DateField = ({ field, value, disabled, onChange, drawerPlacement }) => {
3377
3502
  ] });
3378
3503
  };
3379
3504
 
3505
+ const PREFIX$3 = "px-chat-currency";
3506
+ const ChatCurrencyDisplay = ({
3507
+ value,
3508
+ currencyCode,
3509
+ currencySymbol,
3510
+ showCode = true,
3511
+ size = "md",
3512
+ className
3513
+ }) => {
3514
+ const { locale, currency: configCurrency } = useChatConfig();
3515
+ const code = currencyCode || configCurrency?.code || "COP";
3516
+ const symbol = currencySymbol || configCurrency?.symbol || "$";
3517
+ const formatted = React.useMemo(() => {
3518
+ if (value === null || value === void 0 || value === "") return null;
3519
+ const num = typeof value === "string" ? parseFloat(value) : value;
3520
+ if (isNaN(num)) return null;
3521
+ try {
3522
+ return new Intl.NumberFormat(locale || "es-CO", {
3523
+ minimumFractionDigits: 2,
3524
+ maximumFractionDigits: 2
3525
+ }).format(num);
3526
+ } catch {
3527
+ return num.toFixed(2);
3528
+ }
3529
+ }, [value, locale]);
3530
+ if (!formatted) return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${PREFIX$3} ${PREFIX$3}--empty`, children: "-" });
3531
+ const classes = [PREFIX$3, `${PREFIX$3}--${size}`, className].filter(Boolean).join(" ");
3532
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: classes, children: [
3533
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${PREFIX$3}__symbol`, children: symbol }),
3534
+ " ",
3535
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${PREFIX$3}__value`, children: formatted }),
3536
+ showCode && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3537
+ " ",
3538
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${PREFIX$3}__code`, children: code })
3539
+ ] })
3540
+ ] });
3541
+ };
3542
+ ChatCurrencyDisplay.displayName = "ChatCurrencyDisplay";
3543
+
3544
+ function resolveVariable(template, formValues, fieldStates) {
3545
+ if (!template.startsWith("$")) return template;
3546
+ const withoutDollar = template.slice(1);
3547
+ const dotIndex = withoutDollar.indexOf(".");
3548
+ const fieldName = dotIndex >= 0 ? withoutDollar.slice(0, dotIndex) : withoutDollar;
3549
+ const prop = dotIndex >= 0 ? withoutDollar.slice(dotIndex + 1) : null;
3550
+ const value = formValues[fieldName];
3551
+ const state = fieldStates[fieldName];
3552
+ if (!prop) return value;
3553
+ if (prop === "label" && state) {
3554
+ return state.label || "";
3555
+ }
3556
+ if (prop === "labels" && state && Array.isArray(value)) {
3557
+ return value.map((v) => {
3558
+ const opt = state.options.find((o) => o.value === v);
3559
+ return opt?.label || v;
3560
+ }).join(", ");
3561
+ }
3562
+ if (state) {
3563
+ const selectedOpt = state.options.find((o) => o.value === value);
3564
+ if (selectedOpt && prop in selectedOpt) {
3565
+ return selectedOpt[prop];
3566
+ }
3567
+ }
3568
+ return "";
3569
+ }
3570
+ function resolveBody(obj, formValues, fieldStates) {
3571
+ if (typeof obj === "string") return resolveVariable(obj, formValues, fieldStates);
3572
+ if (Array.isArray(obj)) return obj.map((item) => resolveBody(item, formValues, fieldStates));
3573
+ if (obj !== null && typeof obj === "object") {
3574
+ const resolved = {};
3575
+ for (const [key, value] of Object.entries(obj)) {
3576
+ resolved[key] = resolveBody(value, formValues, fieldStates);
3577
+ }
3578
+ return resolved;
3579
+ }
3580
+ return obj;
3581
+ }
3582
+ function resolveParams(params, formValues, fieldStates) {
3583
+ const resolved = {};
3584
+ for (const [key, template] of Object.entries(params)) {
3585
+ resolved[key] = resolveVariable(template, formValues, fieldStates);
3586
+ }
3587
+ return resolved;
3588
+ }
3589
+ function resolveSummaryRows(rows, formValues, fieldStates) {
3590
+ return rows.map((row) => ({
3591
+ label: row.label,
3592
+ value: String(resolveVariable(row.value, formValues, fieldStates) ?? "")
3593
+ }));
3594
+ }
3595
+
3596
+ const CostSummaryField = ({
3597
+ field,
3598
+ values,
3599
+ fieldStates,
3600
+ onCostResolved,
3601
+ eventBus,
3602
+ onCancel
3603
+ }) => {
3604
+ const [totalCost, setTotalCost] = React.useState(null);
3605
+ const [loading, setLoading] = React.useState(true);
3606
+ const [error, setError] = React.useState("");
3607
+ const selectedItems = React.useMemo(() => {
3608
+ if (!field.items_from) return [];
3609
+ const { source_field, label_key, value_key } = field.items_from;
3610
+ const sourceState = fieldStates[source_field];
3611
+ const selectedValues = values[source_field];
3612
+ if (!sourceState?.options || !selectedValues) return [];
3613
+ const selectedArr = Array.isArray(selectedValues) ? selectedValues : [selectedValues];
3614
+ return sourceState.options.filter((opt) => selectedArr.includes(opt.value)).map((opt) => ({
3615
+ label: String(opt[label_key] || opt.label || ""),
3616
+ value: Number(opt[value_key] || 0)
3617
+ }));
3618
+ }, [field.items_from, fieldStates, values]);
3619
+ const fetchedRef = React.useRef(false);
3620
+ React.useEffect(() => {
3621
+ if (!field.endpoint || !eventBus || fetchedRef.current) {
3622
+ if (!field.endpoint || !eventBus) setLoading(false);
3623
+ return;
3624
+ }
3625
+ fetchedRef.current = true;
3626
+ const body = resolveBody(field.endpoint.body || {}, values, fieldStates);
3627
+ setLoading(true);
3628
+ setError("");
3629
+ fetchViaEventBus(eventBus, field.endpoint.service, field.endpoint.url, field.endpoint.method, body).then((data) => {
3630
+ const resp = data;
3631
+ if (resp?.notification_type && String(resp.notification_type).toUpperCase() !== "SUCCESS") {
3632
+ setError(String(resp.message || "Error al calcular costo"));
3633
+ return;
3634
+ }
3635
+ const responseData = resp?.response;
3636
+ const totalField = field.endpoint.total_field || "total_cost";
3637
+ const cost = Number(responseData?.[totalField] ?? resp?.[totalField] ?? 0);
3638
+ setTotalCost(cost);
3639
+ if (onCostResolved) {
3640
+ try {
3641
+ const formatted = new Intl.NumberFormat("es-CO", { style: "currency", currency: "COP" }).format(cost);
3642
+ onCostResolved(field.name, formatted);
3643
+ } catch {
3644
+ onCostResolved(field.name, `$${cost}`);
3645
+ }
3646
+ }
3647
+ }).catch((err) => {
3648
+ setError(err instanceof Error ? err.message : "Error al calcular costo");
3649
+ }).finally(() => setLoading(false));
3650
+ }, [field.endpoint, eventBus]);
3651
+ const handleCancel = () => {
3652
+ if (field.on_cancel && eventBus) {
3653
+ if (field.on_cancel.type === "system_message") {
3654
+ eventBus.emit(CHAT_EVENT.FORM_TRACE, {
3655
+ chatId: "",
3656
+ formTitle: "",
3657
+ source: "reactive_form",
3658
+ phase: "cancel",
3659
+ data: { message: field.on_cancel.message },
3660
+ service: "",
3661
+ url: "",
3662
+ method: ""
3663
+ });
3664
+ }
3665
+ }
3666
+ onCancel();
3667
+ };
3668
+ if (loading) {
3669
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__cost-summary px-chat-rf__cost-summary--loading", children: [
3670
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__title", children: field.label || "Resumen de costos" }),
3671
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__cost-summary__loader", children: [
3672
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__spinner" }),
3673
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Calculando costo..." })
3674
+ ] })
3675
+ ] });
3676
+ }
3677
+ if (error) {
3678
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__cost-summary px-chat-rf__cost-summary--error", children: [
3679
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__title", children: field.label || "Resumen de costos" }),
3680
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__error", children: error }),
3681
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "px-chat-rf__btn px-chat-rf__btn--secondary", onClick: handleCancel, children: "Cancelar" })
3682
+ ] });
3683
+ }
3684
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__cost-summary", children: [
3685
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__title", children: field.label || "Resumen de costos" }),
3686
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__items", children: selectedItems.map((item, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__cost-summary__item", children: [
3687
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__cost-summary__item-label", children: item.label }),
3688
+ /* @__PURE__ */ jsxRuntime.jsx(ChatCurrencyDisplay, { value: item.value, size: "sm" })
3689
+ ] }, i)) }),
3690
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__divider" }),
3691
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__cost-summary__total", children: [
3692
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__cost-summary__total-label", children: "Total" }),
3693
+ /* @__PURE__ */ jsxRuntime.jsx(ChatCurrencyDisplay, { value: totalCost, size: "lg" })
3694
+ ] }),
3695
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__actions", children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "px-chat-rf__btn px-chat-rf__btn--secondary", onClick: handleCancel, children: "Cancelar" }) })
3696
+ ] });
3697
+ };
3698
+
3380
3699
  const checkIcon = /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2.5 6L5 8.5L9.5 3.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) });
3381
3700
  const calendarIcon = /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
3382
3701
  /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "3", width: "12", height: "11", rx: "1.5", stroke: "currentColor", strokeWidth: "1.2" }),
@@ -3392,7 +3711,7 @@ const formIcon = /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "
3392
3711
  ] });
3393
3712
  const chevronDown = /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 4.5L6 7.5L9 4.5", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round", strokeLinejoin: "round" }) });
3394
3713
  const chevronRight = /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4.5 3L7.5 6L4.5 9", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round", strokeLinejoin: "round" }) });
3395
- const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form, drawerPlacement = "bottom" }) => {
3714
+ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form, drawerPlacement = "bottom", eventBus }) => {
3396
3715
  const {
3397
3716
  formStatus,
3398
3717
  setFormStatus,
@@ -3408,6 +3727,7 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3408
3727
  handleNext,
3409
3728
  handleBack,
3410
3729
  handleSubmit,
3730
+ isSubmitting,
3411
3731
  getDependencies,
3412
3732
  allDepsResolved,
3413
3733
  resolveSummaryRows
@@ -3467,11 +3787,25 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3467
3787
  const renderSummaryActions = () => {
3468
3788
  const actions = summary.actions;
3469
3789
  if (!actions || actions.length === 0) {
3470
- return /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "px-chat-rf__btn px-chat-rf__btn--primary px-chat-rf__btn--full", onClick: handleSubmit, children: submit.label });
3790
+ return /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "px-chat-rf__btn px-chat-rf__btn--primary px-chat-rf__btn--full", onClick: handleSubmit, disabled: isSubmitting, children: isSubmitting ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__spinner" }) : submit.label });
3471
3791
  }
3472
3792
  const actionHandlers = {
3473
3793
  submit: handleSubmit,
3474
- close: () => setFormStatus("closed"),
3794
+ close: () => {
3795
+ if (eventBus) {
3796
+ eventBus.emit(CHAT_EVENT.FORM_TRACE, {
3797
+ chatId: "",
3798
+ formTitle: "",
3799
+ source: "reactive_form",
3800
+ phase: "cancel",
3801
+ data: { message: "El usuario cancel\xF3 el agendamiento de la cita." },
3802
+ service: "",
3803
+ url: "",
3804
+ method: ""
3805
+ });
3806
+ }
3807
+ setFormStatus("closed");
3808
+ },
3475
3809
  back: handleBack
3476
3810
  };
3477
3811
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__nav", children: actions.map((act, i) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -3481,7 +3815,8 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3481
3815
  className: `px-chat-rf__btn px-chat-rf__btn--${act.variant || "primary"}`,
3482
3816
  onClick: actionHandlers[act.action] || (() => {
3483
3817
  }),
3484
- children: act.label
3818
+ disabled: act.action === "submit" && isSubmitting,
3819
+ children: act.action === "submit" && isSubmitting ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__spinner" }) : act.label
3485
3820
  },
3486
3821
  i
3487
3822
  )) });
@@ -3514,8 +3849,15 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3514
3849
  )
3515
3850
  ] }),
3516
3851
  prevStep.fields.map((fname) => {
3517
- const fs = fieldStates[fname];
3518
3852
  const fld = fields.find((f) => f.name === fname);
3853
+ if (fld?.type === "cost_summary") {
3854
+ const costLabel = fieldStates[fname]?.label;
3855
+ return costLabel ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__completed-field", children: [
3856
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__completed-field-label", children: "Total:" }),
3857
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__completed-field-value", children: costLabel })
3858
+ ] }, fname) : null;
3859
+ }
3860
+ const fs = fieldStates[fname];
3519
3861
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__completed-field", children: [
3520
3862
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "px-chat-rf__completed-field-label", children: [
3521
3863
  fld?.label,
@@ -3528,6 +3870,20 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3528
3870
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__active-step", children: [
3529
3871
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__step-title", children: step.title }),
3530
3872
  stepFields.map((field) => {
3873
+ if (field.type === "cost_summary") {
3874
+ return /* @__PURE__ */ jsxRuntime.jsx(
3875
+ CostSummaryField,
3876
+ {
3877
+ field,
3878
+ values,
3879
+ fieldStates,
3880
+ eventBus,
3881
+ onCancel: () => setFormStatus("closed"),
3882
+ onCostResolved: (name, label) => handleFieldChange(name, label, label)
3883
+ },
3884
+ field.name
3885
+ );
3886
+ }
3531
3887
  const deps = getDependencies(field);
3532
3888
  const isDisabled = !allDepsResolved(deps, values);
3533
3889
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -3623,89 +3979,6 @@ function buildInitialValues(fields) {
3623
3979
  return values;
3624
3980
  }
3625
3981
 
3626
- function resolveVariable(template, formValues, fieldStates) {
3627
- if (!template.startsWith("$")) return template;
3628
- const withoutDollar = template.slice(1);
3629
- const dotIndex = withoutDollar.indexOf(".");
3630
- const fieldName = dotIndex >= 0 ? withoutDollar.slice(0, dotIndex) : withoutDollar;
3631
- const prop = dotIndex >= 0 ? withoutDollar.slice(dotIndex + 1) : null;
3632
- const value = formValues[fieldName];
3633
- const state = fieldStates[fieldName];
3634
- if (!prop) return value;
3635
- if (prop === "label" && state) {
3636
- return state.label || "";
3637
- }
3638
- if (prop === "labels" && state && Array.isArray(value)) {
3639
- return value.map((v) => {
3640
- const opt = state.options.find((o) => o.value === v);
3641
- return opt?.label || v;
3642
- }).join(", ");
3643
- }
3644
- if (state) {
3645
- const selectedOpt = state.options.find((o) => o.value === value);
3646
- if (selectedOpt && prop in selectedOpt) {
3647
- return selectedOpt[prop];
3648
- }
3649
- }
3650
- return "";
3651
- }
3652
- function resolveBody(obj, formValues, fieldStates) {
3653
- if (typeof obj === "string") return resolveVariable(obj, formValues, fieldStates);
3654
- if (Array.isArray(obj)) return obj.map((item) => resolveBody(item, formValues, fieldStates));
3655
- if (obj !== null && typeof obj === "object") {
3656
- const resolved = {};
3657
- for (const [key, value] of Object.entries(obj)) {
3658
- resolved[key] = resolveBody(value, formValues, fieldStates);
3659
- }
3660
- return resolved;
3661
- }
3662
- return obj;
3663
- }
3664
- function resolveParams(params, formValues, fieldStates) {
3665
- const resolved = {};
3666
- for (const [key, template] of Object.entries(params)) {
3667
- resolved[key] = resolveVariable(template, formValues, fieldStates);
3668
- }
3669
- return resolved;
3670
- }
3671
- function resolveSummaryRows(rows, formValues, fieldStates) {
3672
- return rows.map((row) => ({
3673
- label: row.label,
3674
- value: String(resolveVariable(row.value, formValues, fieldStates) ?? "")
3675
- }));
3676
- }
3677
-
3678
- const FETCH_TIMEOUT_MS = 12e4;
3679
- function fetchViaEventBus(eventBus, service, url, method, body) {
3680
- const requestId = globalThis.crypto?.randomUUID?.() ?? "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
3681
- const r = Math.random() * 16 | 0;
3682
- return (c === "x" ? r : r & 3 | 8).toString(16);
3683
- });
3684
- return new Promise((resolve, reject) => {
3685
- const timeoutId = setTimeout(() => {
3686
- unsubscribe();
3687
- reject(new Error(`Timeout: no response for ${url} after ${FETCH_TIMEOUT_MS}ms`));
3688
- }, FETCH_TIMEOUT_MS);
3689
- const unsubscribe = eventBus.on(CHAT_EVENT.FETCH_RESPONSE, (response) => {
3690
- if (response.requestId !== requestId) return;
3691
- clearTimeout(timeoutId);
3692
- unsubscribe();
3693
- if (response.httpError) {
3694
- reject(new Error(response.httpError));
3695
- } else {
3696
- resolve(response.data);
3697
- }
3698
- });
3699
- eventBus.emit(CHAT_EVENT.FETCH_REQUEST, {
3700
- requestId,
3701
- service,
3702
- url,
3703
- method,
3704
- ...body ? { body } : {}
3705
- });
3706
- });
3707
- }
3708
-
3709
3982
  function formatTime$1(value, timezone) {
3710
3983
  try {
3711
3984
  const d = new Date(value);
@@ -3713,7 +3986,7 @@ function formatTime$1(value, timezone) {
3713
3986
  return d.toLocaleTimeString([], {
3714
3987
  hour: "2-digit",
3715
3988
  minute: "2-digit",
3716
- hour12: false,
3989
+ hour12: true,
3717
3990
  ...timezone ? { timeZone: timezone } : {}
3718
3991
  });
3719
3992
  } catch {
@@ -4075,7 +4348,10 @@ function useReactiveForm(props) {
4075
4348
  };
4076
4349
  const handleBack = () => setCurrentStep((s) => s - 1);
4077
4350
  const [submitMessage, setSubmitMessage] = React.useState("");
4351
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
4078
4352
  const handleSubmit = async () => {
4353
+ if (isSubmitting) return;
4354
+ setIsSubmitting(true);
4079
4355
  const resolvedPayload = resolveBody(submit.payload_map, values, fieldStates);
4080
4356
  if (eventBus) {
4081
4357
  try {
@@ -4098,7 +4374,8 @@ function useReactiveForm(props) {
4098
4374
  submitUrl: submit.url,
4099
4375
  result: data,
4100
4376
  formValues: values,
4101
- formLabels
4377
+ formLabels,
4378
+ onSuccess: submit.on_success
4102
4379
  });
4103
4380
  if (notificationType === "SUCCESS") {
4104
4381
  setSubmitMessage(message || "Completado");
@@ -4114,6 +4391,7 @@ function useReactiveForm(props) {
4114
4391
  const message = err instanceof Error ? err.message : "Error al enviar formulario";
4115
4392
  setSubmitMessage(message);
4116
4393
  setFormStatus("error");
4394
+ setIsSubmitting(false);
4117
4395
  }
4118
4396
  } else {
4119
4397
  console.log("[ReactiveForm] Submit:", values);
@@ -4142,6 +4420,7 @@ function useReactiveForm(props) {
4142
4420
  handleNext,
4143
4421
  handleBack,
4144
4422
  handleSubmit,
4423
+ isSubmitting,
4145
4424
  getDependencies,
4146
4425
  allDepsResolved,
4147
4426
  resolveSummaryRows: resolveRows
@@ -4173,7 +4452,8 @@ const ReactiveForm = ({ component, eventBus }) => {
4173
4452
  {
4174
4453
  ...props,
4175
4454
  form,
4176
- drawerPlacement: isMobile ? "bottom" : "right"
4455
+ drawerPlacement: isMobile ? "bottom" : "right",
4456
+ eventBus
4177
4457
  }
4178
4458
  );
4179
4459
  };