function fillFormFromData($form, data) {
  // ---- helpers ----
  const normalize = (v) =>
    v == null ? "" : typeof v === "string" ? v.trim() : v;

  const toArray = (v) => {
    if (Array.isArray(v)) return v;
    if (!v) return [];
    return String(v)
      .split(",")
      .map((x) => x.trim())
      .filter(Boolean);
  };

  const ensureOption = ($select, value) => {
    if (value === "" || value == null) return;
    const exists = $select.find(`option[value="${value}"]`).length > 0;
    if (!exists) $select.append(new Option(value, value));
  };

  // Convierte "22:00:00.0000000" -> "22:00" (también maneja "HH:MM:SS" y "HH:MM")
  const toTimeValue = (v) => {
    if (!v) return "";
    const s = String(v).trim();
    const m = s.match(/^(\d{1,2}):(\d{2})(?::\d{2}(?:\.\d+)?)?$/);
    if (m) return `${m[1].padStart(2, "0")}:${m[2]}`; // HH:MM
    // fallback por si llega como fecha completa
    const d = new Date(s);
    if (!isNaN(d)) return d.toTimeString().slice(0, 5); // HH:MM
    return "";
  };

  // ---- binding genérico ----
  for (const [campo, rawValor] of Object.entries(data)) {
    // Evitar índices numéricos de consultas SQL (0,1,2,...)
    if (!isNaN(campo)) continue;

    const valor = normalize(rawValor);

    // Buscar por id iptXxx y si no, por name="campo"
    const inputId = "#ipt" + campo.charAt(0).toUpperCase() + campo.slice(1);
    let $el = $form.find(inputId);
    if (!$el.length) $el = $form.find(`[name="${campo}"]`);
    if (!$el.length) continue;

    // Radios (varios inputs con el mismo name)
    if ($el.length > 1 && $el.is(":radio")) {
      $el
        .prop("checked", false)
        .filter((_, r) => $(r).val() == valor)
        .prop("checked", true)
        .trigger("change");
      continue;
    }

    const $c = $el.eq(0);

    // Checkbox
    if ($c.is(":checkbox")) {
      $c.prop("checked", valor == 1 || valor === true || valor === "1").trigger(
        "change"
      );
      continue;
    }

    // MultiSelect (Syncfusion) detectado por clase .multiselect
    if ($c.hasClass("multiselect")) {
      // obtener instancia de ej2 sin alias globales
      const host = $c.get(0);
      const inst = host?.ej2_instances?.[0]; // ej.dropdowns.MultiSelect
      if (inst) {
        inst.value = toArray(valor);
        inst.dataBind();
      } else {
        // si por algún motivo aún no está instanciado, al menos deja el valor en un data-attr
        $c.attr("data-pending-value", toArray(valor).join(","));
      }
      continue;
    }

    // Select normal (incluido selectpicker)
    if ($c.is("select")) {
      const multiple = $c.prop("multiple");
      if (multiple) {
        const arr = toArray(valor);
        arr.forEach((v) => ensureOption($c, v));
        $c.val(arr);
      } else {
        ensureOption($c, valor);
        $c.val(valor);
      }
      if ($c.hasClass("selectpicker")) $c.selectpicker("refresh");
      $c.trigger("change");
      continue;
    }

    // Input time: recorta a HH:MM
    if ($c.is('input[type="time"]')) {
      $c.val(toTimeValue(valor)).trigger("change");
      continue;
    }

    if ($c.hasClass("richtexteditor")) {
      // Buscar instancia RTE en el contenedor #editor
      const host = document.querySelector("#editor");
      const inst = host?.ej2_instances?.[0]; // instancia del RTE

      if (
        inst &&
        inst.getModuleName &&
        inst.getModuleName() === "richtexteditor"
      ) {
        inst.value = valor || "";
        inst.dataBind();

        // También reflejar el valor en el textarea oculto para que FormData lo capture
        $c.val(valor || "");
      } else {
        // Si aún no está montado el editor, guardo el valor pendiente
        $c.attr("data-pending-rte", valor || "");
      }

      continue;
    }

    // Inputs/textarea genéricos
    if ($c.is("input, textarea")) {
      $c.val(valor).trigger("change");
      continue;
    }

    // Fallback para elementos de texto (spans/divs)
    $c.text(valor);
  }
}
