<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Generate document</title>
<style>
  body { font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, Arial, sans-serif; margin: 0; padding: 18px; color: #242424; background: #faf9f8; }
  h1 { font-size: 16px; margin: 0 0 12px 0; }
  label { display: block; font-size: 12px; color: #605e5c; margin: 12px 0 4px 0; }
  select, input[type=text], textarea { width: 100%; box-sizing: border-box; font-size: 13px; padding: 6px 8px; border: 1px solid #8a8886; border-radius: 3px; font-family: inherit; }
  textarea { min-height: 48px; resize: vertical; }
  .checkbox-row { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
  .checkbox-row label { margin: 0; font-size: 13px; color: #242424; }
  .hint { font-size: 11px; color: #8a8886; margin-top: 3px; }
  .actions { margin-top: 18px; display: flex; gap: 8px; align-items: center; }
  button.primary { background: #0078d4; color: #fff; border: none; border-radius: 3px; padding: 8px 18px; font-size: 13px; cursor: pointer; }
  button.primary:hover { background: #106ebe; }
  button.primary:disabled { background: #c8c6c4; cursor: default; }
  button.secondary { background: #fff; color: #242424; border: 1px solid #8a8886; border-radius: 3px; padding: 8px 14px; font-size: 13px; cursor: pointer; }
  #spinner { font-size: 12px; color: #605e5c; }
  .result { margin-top: 14px; border-radius: 4px; padding: 12px 14px; font-size: 13px; }
  .result.success { background: #dff6dd; border: 1px solid #107c10; color: #0b590b; }
  .result.error { background: #fde7e9; border: 1px solid #a4262c; color: #a4262c; }
  #empty { font-size: 13px; color: #605e5c; }
  .progress-track { margin-top: 10px; height: 6px; border-radius: 3px; background: #edebe9; overflow: hidden; }
  .progress-fill { height: 100%; width: 0%; background: #0078d4; transition: width .15s ease; }
  .fail-list { margin: 10px 0 0 0; padding-left: 18px; max-height: 220px; overflow-y: auto; font-size: 12px; color: #a4262c; }

  /* The HTML `hidden` attribute only hides an element because the browser's own
     stylesheet says [hidden] { display: none }. Any author rule that sets display
     on the same element (id or class) beats it silently. Nothing above touches
     display on #bulkPanel/#bulkProgress/.fail-list, but this guard is kept as a
     hard floor regardless -- see templatestudio.htm's copy of this same rule for
     the incident (a permanently-open modal) that made it a standing requirement
     for every web resource that uses `hidden`, not just the one it happened to. */
  [hidden] { display: none !important; }
</style>
</head>
<body>
  <h1 id="pageTitle">Generate document</h1>
  <div id="loading">Loading templates&hellip;</div>
  <div id="empty" style="display:none;">No active VerseDocs template is configured for this table yet. Ask an administrator to create one (Bound Table = this table) in the VerseDocs Admin app.</div>

  <form id="genForm" style="display:none;">
    <label for="template">Template</label>
    <select id="template"></select>

    <label for="format">Output format</label>
    <select id="format">
      <option value="">Native (template's own format)</option>
      <option value="pdf">PDF</option>
    </select>

    <div class="checkbox-row">
      <input type="checkbox" id="attach" checked="checked" />
      <label for="attach">Attach to this record's timeline as a note</label>
    </div>
    <p class="hint">Unchecked = download only, nothing is written to Dataverse.</p>

    <div id="noteFields">
      <label for="subject">Note subject (optional)</label>
      <input type="text" id="subject" placeholder="Defaults to the generated file name" />
      <label for="noteText">Note text (optional)</label>
      <textarea id="noteText" placeholder="e.g. Generated for {{account.name}}"></textarea>
      <p class="hint">Supports {{field}} tokens against this record's data, e.g. {{account.name}}.</p>
    </div>

    <div class="actions">
      <button type="submit" class="primary" id="generateBtn">Generate</button>
      <span id="spinner" style="display:none;">Generating&hellip;</span>
    </div>
  </form>

  <div id="bulkPanel" hidden>
    <p id="bulkSelectionLine" class="hint"></p>

    <label for="bulkTemplate">Template</label>
    <select id="bulkTemplate"></select>
    <p class="hint">One template is used for every selected record. Each record is attached to its own timeline as a note.</p>

    <div class="actions">
      <button type="button" class="primary" id="bulkGenerateBtn">Generate</button>
    </div>

    <div id="bulkProgress" hidden>
      <p id="bulkProgressText" class="hint"></p>
      <div class="progress-track"><div id="bulkProgressFill" class="progress-fill"></div></div>
      <ul id="bulkFailList" class="fail-list" hidden></ul>
    </div>
  </div>

  <div id="resultBox"></div>

<script>
(function () {
  "use strict";

  // Bounded-concurrency worker pool (bulk mode only): at most this many calls
  // to vdocs_GenerateAndAttach in flight at once, regardless of how many
  // records were selected, so a large selection does not fire dozens of
  // simultaneous requests at the browser or the sandbox worker pool.
  var BULK_CONCURRENCY = 3;

  function findXrm() {
    if (typeof Xrm !== "undefined" && Xrm.WebApi) { return Xrm; }
    try {
      if (window.parent && window.parent.Xrm && window.parent.Xrm.WebApi) { return window.parent.Xrm; }
    } catch (e) { /* cross-frame access blocked -- fall through */ }
    try {
      if (window.top && window.top.Xrm && window.top.Xrm.WebApi) { return window.top.Xrm; }
    } catch (e) { /* ignore */ }
    return null;
  }

  function getDialogData() {
    // Xrm.Navigation.navigateTo's pageType:"webresource" `data` option is
    // passed to the resource as a "data" query-string parameter (Microsoft
    // Learn "PageInputWebResource") -- not via postMessage or a global.
    // Single-record callers (the form button) send {entity, id}; the grid/
    // subgrid bulk button sends {entity, ids: [...]} instead.
    var params = new URLSearchParams(window.location.search);
    var raw = params.get("data");
    if (!raw) { return null; }
    try { return JSON.parse(raw); } catch (e) { return null; }
  }

  /** The one result-rendering helper for this file: both single-record
   * generation and the end-of-run bulk summary go through this, so there is
   * exactly one place that builds the success/error box. */
  function showResult(cssClass, html) {
    document.getElementById("resultBox").innerHTML = '<div class="result ' + cssClass + '">' + html + "</div>";
  }

  /** The one error-message formatter for this file: template load failures,
   * the single-record generate failure, and every per-record failure in a
   * bulk run all go through this, so there is exactly one place that turns a
   * Web API error object into readable text instead of a stack or a raw
   * envelope. */
  function formatErrorMessage(error) {
    var raw = error && error.message ? error.message : String(error);

    // VerseDocs plug-ins report failures as a JSON envelope
    // ({"code":"...","message":"..."}) carried IN the exception message, so
    // error.message is the envelope itself, not prose. Printing it raw put
    // this in front of a user, braces and escapes and all:
    //
    //   Generation failed: {"code":"UNSUPPORTED_FILE_TYPE","message":"Options.
    //   outputFormat 'pdf' is only supported for docx-based templates; ..."}
    //
    // The sentence inside was exactly right; only the wrapper was wrong (QA
    // retest 2, defect 2.5). Show the sentence, and keep the code in
    // parentheses so a support conversation still has something to grep for.
    var trimmed = String(raw).trim();
    if (trimmed.charAt(0) === "{") {
      try {
        var parsed = JSON.parse(trimmed);
        if (parsed && parsed.message) {
          return parsed.code ? parsed.message + " (" + parsed.code + ")" : parsed.message;
        }
      } catch (e) {
        // Not our envelope after all -- fall through and show it as-is.
      }
    }

    return raw;
  }

  /** The one template-listing helper for this file: populates a <select>
   * with this table's active templates. Used for the single-record #template
   * select and the bulk-mode #bulkTemplate select alike. */
  function renderTemplateOptions(selectEl, templates) {
    templates.forEach(function (row) {
      var option = document.createElement("option");
      option.value = row.vdocs_templateid;
      option.textContent = row.vdocs_name + " (" + row.vdocs_templatetype + ")";
      // Carried so the Output format control can tell whether PDF is even
      // possible for the selected template -- see syncFormatToTemplate.
      option.setAttribute("data-template-type", row.vdocs_templatetype || "");
      selectEl.appendChild(option);
    });
  }

  function executeAction(xrmClient, actionName, parameters) {
    var request = parameters;
    request.getMetadata = function () {
      return {
        boundParameter: null,
        parameterTypes: {
          TemplateId: { typeName: "Edm.String", structuralProperty: 1 },
          RowId: { typeName: "Edm.String", structuralProperty: 1 },
          Options: { typeName: "Edm.String", structuralProperty: 1 },
        },
        operationType: 0, // Action
        operationName: actionName,
      };
    };
    return xrmClient.WebApi.online.execute(request);
  }

  function triggerDownload(fileName, contentType, base64Content) {
    var byteChars = atob(base64Content);
    var byteNumbers = new Array(byteChars.length);
    for (var i = 0; i < byteChars.length; i++) {
      byteNumbers[i] = byteChars.charCodeAt(i);
    }
    var blob = new Blob([new Uint8Array(byteNumbers)], { type: contentType });
    var url = URL.createObjectURL(blob);
    var link = document.createElement("a");
    link.href = url;
    link.download = fileName;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
  }

  /** Bulk mode only. Calls vdocs_GenerateAndAttach once per id, at most
   * BULK_CONCURRENCY calls in flight at once. One record's failure is
   * captured into `results` and does not stop the run -- the pool keeps
   * pulling from the queue until it is empty. onProgress fires after every
   * completion (success or failure) so the panel can show a live
   * "N of M" count. There is no server-side multi-record action (see
   * GenerateAndAttachPlugin.cs); one call per record keeps each inside the
   * 2-minute plug-in timeout on its own. */
  function runBulkGenerate(xrmClient, templateId, ids, onProgress, onDone) {
    var total = ids.length;
    var nextIndex = 0;
    var active = 0;
    var results = [];

    function launchNext() {
      if (nextIndex >= total) {
        if (active === 0) {
          onDone(results);
        }
        return;
      }
      var id = ids[nextIndex];
      nextIndex++;
      active++;
      executeAction(xrmClient, "vdocs_GenerateAndAttach", {
        TemplateId: templateId,
        RowId: id,
        Options: JSON.stringify({ attach: true }),
      }).then(
        function (response) {
          return response.json().then(
            function (body) {
              results.push({ id: id, ok: true, fileName: body.OutFileName });
            },
            function () {
              // Succeeded server-side but the response body could not be
              // read -- still count it a success; there is no file name to
              // report.
              results.push({ id: id, ok: true, fileName: null });
            }
          );
        },
        function (error) {
          results.push({ id: id, ok: false, error: formatErrorMessage(error) });
        }
      ).then(function () {
        active--;
        onProgress(results.length, total);
        launchNext();
      });
    }

    var starters = total < BULK_CONCURRENCY ? total : BULK_CONCURRENCY;
    for (var i = 0; i < starters; i++) {
      launchNext();
    }
  }

  var xrm = findXrm();
  var dialogData = getDialogData();

  if (!xrm) {
    document.getElementById("loading").style.display = "none";
    showResult("error", "This page must be opened from inside a model-driven app (it calls Dataverse via the client API).");
    return;
  }

  var recordIds = (dialogData && Array.isArray(dialogData.ids) && dialogData.ids.length > 0) ? dialogData.ids : null;
  var isBulk = recordIds !== null;

  if (!dialogData || !dialogData.entity || (!isBulk && !dialogData.id)) {
    document.getElementById("loading").style.display = "none";
    showResult("error", "No record context was passed to this dialog.");
    return;
  }

  var entityLogicalName = dialogData.entity;
  var recordId = isBulk ? null : dialogData.id;

  if (isBulk) {
    document.getElementById("pageTitle").textContent = "Generate documents";
  }

  var templateFilter = "vdocs_boundtable eq '" + entityLogicalName.replace(/'/g, "''") + "' and statecode eq 0";
  xrm.WebApi.retrieveMultipleRecords("vdocs_template", "?$select=vdocs_name,vdocs_templatetype&$filter=" + templateFilter + "&$orderby=vdocs_name asc").then(
    function (result) {
      document.getElementById("loading").style.display = "none";
      if (!result.entities || result.entities.length === 0) {
        document.getElementById("empty").style.display = "block";
        return;
      }

      if (isBulk) {
        renderTemplateOptions(document.getElementById("bulkTemplate"), result.entities);
        var count = recordIds.length;
        document.getElementById("bulkSelectionLine").textContent = count + " record" + (count === 1 ? "" : "s") + " selected.";
        document.getElementById("bulkPanel").hidden = false;
      } else {
        renderTemplateOptions(document.getElementById("template"), result.entities);
        // Templates have just landed, so the format control can now reflect
        // whichever one is selected first.
        syncFormatToTemplate();
        document.getElementById("genForm").style.display = "block";
      }
    },
    function (error) {
      document.getElementById("loading").style.display = "none";
      showResult("error", "Could not load templates: " + formatErrorMessage(error));
    }
  );

  // -----------------------------------------------------------------------
  // Single-record mode (unchanged from before bulk mode existed). Only ever
  // reached because #genForm is shown; in bulk mode it stays display:none
  // and these listeners simply never fire.
  // -----------------------------------------------------------------------
  /**
   * PDF is only produced for docx-based templates. The dropdown used to offer
   * it for every template, so choosing a pptx or xlsx and clicking Generate
   * failed AFTER the round trip, with an error the user could have been spared
   * (QA retest 2, defect 2.4). Disable the option instead, and say why on the
   * option itself rather than in a separate message nobody reads.
   */
  function syncFormatToTemplate() {
    var templateSelect = document.getElementById("template");
    var formatSelect = document.getElementById("format");
    if (!templateSelect || !formatSelect) { return; }

    var selected = templateSelect.options[templateSelect.selectedIndex];
    var type = selected ? (selected.getAttribute("data-template-type") || "") : "";
    var pdfOption = formatSelect.querySelector('option[value="pdf"]');
    if (!pdfOption) { return; }

    var pdfPossible = type.toLowerCase() === "docx";
    pdfOption.disabled = !pdfPossible;
    pdfOption.textContent = pdfPossible ? "PDF" : "PDF (not available for " + (type || "this") + " templates)";
    if (!pdfPossible && formatSelect.value === "pdf") {
      formatSelect.value = "";
    }
  }

  document.getElementById("template").addEventListener("change", syncFormatToTemplate);

  document.getElementById("attach").addEventListener("change", function (ev) {
    document.getElementById("noteFields").style.display = ev.target.checked ? "block" : "none";
  });

  document.getElementById("genForm").addEventListener("submit", function (ev) {
    ev.preventDefault();

    var templateId = document.getElementById("template").value;
    var outputFormat = document.getElementById("format").value || null;
    var attach = document.getElementById("attach").checked;
    var subject = document.getElementById("subject").value || null;
    var noteText = document.getElementById("noteText").value || null;

    var options = { attach: attach };
    if (outputFormat) { options.outputFormat = outputFormat; }
    if (attach && subject) { options.noteSubject = subject; }
    if (attach && noteText) { options.noteText = noteText; }

    var generateBtn = document.getElementById("generateBtn");
    generateBtn.disabled = true;
    document.getElementById("spinner").style.display = "inline";
    document.getElementById("resultBox").innerHTML = "";

    executeAction(xrm, "vdocs_GenerateAndAttach", {
      TemplateId: templateId,
      RowId: recordId,
      Options: JSON.stringify(options),
    }).then(
      function (response) {
        return response.json().then(function (body) {
          generateBtn.disabled = false;
          document.getElementById("spinner").style.display = "none";

          if (attach && body.AnnotationId) {
            showResult("success", "Generated <strong>" + body.OutFileName + "</strong> and attached it to this record's timeline. You can close this dialog.");
          } else {
            showResult("success", "Generated <strong>" + body.OutFileName + "</strong>. Downloading now. You can close this dialog.");
            triggerDownload(body.OutFileName, body.OutContentType, body.OutFileContent);
          }
        });
      },
      function (error) {
        generateBtn.disabled = false;
        document.getElementById("spinner").style.display = "none";
        showResult("error", "Generation failed: " + formatErrorMessage(error));
      }
    );
  });

  // -----------------------------------------------------------------------
  // Bulk mode: one template for the whole run, then vdocs_GenerateAndAttach
  // once per selected record through runBulkGenerate above. A failing
  // record is recorded and shown in the summary; it does not stop the rest
  // of the run.
  // -----------------------------------------------------------------------
  document.getElementById("bulkGenerateBtn").addEventListener("click", function () {
    var templateId = document.getElementById("bulkTemplate").value;
    if (!templateId) { return; }

    var templateSelect = document.getElementById("bulkTemplate");
    var generateBtn = document.getElementById("bulkGenerateBtn");
    var progressText = document.getElementById("bulkProgressText");
    var progressFill = document.getElementById("bulkProgressFill");
    var failList = document.getElementById("bulkFailList");

    templateSelect.disabled = true;
    generateBtn.disabled = true;
    document.getElementById("resultBox").innerHTML = "";
    failList.innerHTML = "";
    failList.hidden = true;

    progressFill.style.width = "0%";
    progressText.textContent = "Generating 0 of " + recordIds.length;
    document.getElementById("bulkProgress").hidden = false;

    runBulkGenerate(
      xrm,
      templateId,
      recordIds,
      function onProgress(done, total) {
        progressText.textContent = "Generating " + done + " of " + total;
        progressFill.style.width = Math.round((done / total) * 100) + "%";
      },
      function onDone(results) {
        var succeeded = [];
        var failed = [];
        results.forEach(function (r) {
          if (r.ok) { succeeded.push(r); } else { failed.push(r); }
        });

        progressText.textContent = "Done: " + succeeded.length + " of " + results.length + " succeeded.";

        if (failed.length > 0) {
          failed.forEach(function (f) {
            var item = document.createElement("li");
            item.textContent = "Record " + f.id + ": " + f.error;
            failList.appendChild(item);
          });
          failList.hidden = false;
        }

        var summaryHtml = "Generated <strong>" + succeeded.length + "</strong> of " + results.length + " document" + (results.length === 1 ? "" : "s") + ".";
        if (failed.length > 0) {
          summaryHtml += " " + failed.length + " failed; see the list above for details.";
        }
        summaryHtml += " You can close this dialog.";
        showResult(failed.length > 0 ? "error" : "success", summaryHtml);
      }
    );
  });
})();
</script>
</body>
</html>
