<% /*
     File:          shared/resultset_table_v2.inc

     $Revision$
     $Id$

   */
%>

<!-- #include file="resultset_filter_table.inc" -->
<!-- #include file="resultset_flex.inc" -->
<!-- #include file="iface.inc" -->
<!-- #include file="../api/api.inc" -->
<!-- #include file="../Shared/rs_columns.inc" -->
<%
FCLTHeader.Requires({ plugins: ["jQuery"],
                      js: ["resultset_table_v2.js", "sorttable.js"]
                    });
var APIname = getQParam("API","");
var useHamburger = S("inline_actions_hamburger");
if (getQParam("touch","") == 1 || device.test(device.isTouch))
    useHamburger = 1; // Hamburger onClick
var hasInlineActions = false;
var book_id = null;
if (APIname == "REPORT")
{
    var API = new API_func();
    book_id = getQParam("BOOKMARK");
    var sql = "SELECT fac_bookmark_path, "
            + "       fac_bookmark_query"
            + "  FROM fac_bookmark"
            + " WHERE fac_bookmark_id = " + safe.quoted_sql(book_id);
    var oRs = Oracle.Execute(sql);
    if (oRs.EOF)
    {
        Response.Write("Bookmark niet gevonden");
        Response.End;
    }
    var path = oRs("fac_bookmark_path").Value;
    var query = oRs("fac_bookmark_query").Value;
    oRs.Close();

    if (rooturl + path != Request.ServerVariables("SCRIPT_NAME"))
    {
        shared.internal_error("Invalid bookmark path");
    }

    //
    // getQParam/getFParam onderscheppen en 'voeden' uit FAC_BOOKMARK_URL
    //
    // Simuleer een Request.Querystring compatible collection
    function bmcoll(key)
    {
        var nn = 1;
        var coll = Server.CreateObject("Scripting.Dictionary");
        var params = query.split("&");
        for (var i in params)
        {
            var xx = params[i].split("=");
            if (xx[0].toUpperCase() == key.toUpperCase() && xx.length > 1)
            {
                coll.Add(nn++, unescape(String(xx[1]).replace(/\+/g, " "))); // spaties gaan niet vanzelf
            }
        }
        return coll;
    }
    getQParam = function (pName, defVal)
    {
        return _get_Param(bmcoll, pName, defVal)
    }
    getQParamInt = function (pName, defVal)
    {
        return _get_ParamInt(bmcoll, pName, defVal)
    }
    getQParamDate = function (pName, defVal)
    {
        return _get_ParamDate(bmcoll, pName, defVal)
    }
}

var p_autosortby = getQParam('autosortby', "") || getFParam('autosortby', "");
var autosortbyrev = false;
if (p_autosortby.substr(0,1) == '-')
{
    var autosortbyrev = true;
    p_autosortby = p_autosortby.substr(1);
}

// <form> printform is nodig bij printen reserveringen i.v.m. POST formulier
// maar <form> mag niet binnen een ander <form> voorkomen en moet eigenlijk wel binnen body staan
// Formulier als tekstring samenstellen en m.b.v. $(function(){}) binnen de body zetten (resultset_table_v2.js)
var postformHTML = '';

if (Request.ServerVariables("REQUEST_METHOD") == "POST")
{
   var postformHTML = '<div id=\"postform\">'
                    + '<form id=\"printform\" name=\"printform\" target=\"_blank\" rel=\"noopener noreferrer\" method=\"post\">'
                    + '<input type=\"hidden\" id=\"outputmode\" name=\"outputmode\" value=\"0\">' // input outputmode wordt gezet in doOutput()
                    + '<input type=\"hidden\" id=\"autosortby\" name=\"autosortby\" value=\"0\">' // input autosortby wordt gezet in doOutput()
                    + '<input type=\"hidden\" id=\"showall\" name=\"showall\" value=\"1\">';
   var items = new Enumerator(Request.Form());

  while (!items.atEnd())
  {
    var itemName = items.item();
    for (var i = 1; i <= Request.Form(itemName).count; i++)
    { // De input velden outputmode en showall zijn standaard al gedefinieerd. Niet dubbel definiëren
      // Een eventuele submit ook niet doorgeven tijdens afdruk.
      if (itemName != "outputmode" && itemName != "showall" && itemName != "do_submit")
        postformHTML += '<input type=\"hidden\" name=\"' + safe.htmlattr(itemName)
                     + '\" value=\"' + safe.htmlattr(Request.Form(itemName)(i)) + '\">';
    }
    items.moveNext();
  }
  postformHTML += '</form>'
                + '</div>';
}

var isPlain = false;
var isPrinting = false;
var isExcel = false;
var isCSV = false;
var isCSV2XLSX = false;
var isZIP = false;
var isGraph = false;

function ResultsetTable(params)
{
  this.beforeEachRow = null;
  this.rowClass = null;
  this.rowData = null;
  this.newHeader = null;
  this.emptySetString = L("lcl_empty_rstable");
  this.sql = null;
  this.sqltotals = null;
  this.dataset = null;
  this.filter = "";
  this.filterParams = null;
  this.ID = null;
  this.flexModule = null;
  this.flexId = null;
  this.flexGroupId = null;
  this.flexParentGroupId = null;
  this.flexChangeNiveau = null;
  this.flexParams = null;
  this.rowNum = 0;
  this.keyColumn = null;
  this.dblClick = null;
  this.disableInlineActions = null;
  this.rowActionEnabler = null;
  this.multiActionEnabler = null;
  this.inlineDetails = null;
  this.hasInlineDetails = null;
  this.inlineDetailsChanged = null;
  this.summaryCalc = null;
  this.summaryShow = null;
  this.totalColumnText = null;
  this.totalCalc = null;
  this.totalShow = null;
  this.summaryId = null;
  this.noPrint = null;   /* if true: suppress print/excel icons */
  this.noExcel = null;   /* if true: suppress excel icon */
  this.outputmode = null; // print naar: 0 = screen, 1 = printer met flex, 2 = excel met flex, 3 = XML/XSL, 4 = CSV,
                          //             5 = printer zonder flex, 6 = excel zonder flex., 7 = zipfile met bijlagen
  this.endText = null;
  this.showAll = null;
  this.showProperties = null;
  this.propertiesColumn = null; // alleen nog maar in gebruik voor FIN flexkenmerken detailregels
  this.addurl = null;
  this.suppressKeyRepeat = null;
  this.inline = null;
  this.lastColKey = -1; // voor suppress (columns)
  this.urllink = null;
  this.subtabicon = null;
  this.rstCheckboxes = null;

  // multiple resultsets
  this.rstableCount = 1;
  this.rstableNr = 1;

  this.buildHistogram = false; // over *alle* kolommen van de sql! Originele waarden!
  this.Counter = {};

  this.noSetSummary = false;
  var param;
  // Neem alle meegegeven parameters mee.
  for (param in params)
      this[param] = params[param];

  isPlain = ((this.outputmode || 0) == 0);
  isPrinting = (this.outputmode == 1 || this.outputmode == 5);
  isExcel = ((this.outputmode == 2 || this.outputmode == 6) && S("excel_mode")==0);
  isNativeExcel = ((this.outputmode == 2 || this.outputmode == 6 || this.outputmode == 8) && S("excel_mode")==2);
  isXMLXSL = (this.outputmode == 3);
  isCSV = ((this.outputmode == 4) || (((this.outputmode == 2 || this.outputmode == 6) && S("excel_mode")==1)));
  //isCSV2XLSX = (this.outputmode == 8)
  isZIP = (this.outputmode == 7);
  isGraph = getQParam("asgraph", 0) == 1;
  this.printFlex = (this.outputmode == 1 || this.outputmode == 2 || this.showProperties) && this.flexModule != null && this.flexId != null;
  this.tableClassName = "rstable"
                      + ("nofloating" in params && params.nofloating ? " no-floating" : "")
                      + (!isExcel ? " sortable" : "");

  if (isPrinting || isExcel)
  {
    this.inlineDetails = null;
    this.hasInlineDetails = null;
    this.inlineDetailsChanged = null;
  }

  this.columns = new Array();
  this.actions = new Array();
  this.lastVal = new Array(); // voor suppress (columns)

  if (this.inline || isExcel)
  {
    this.noPrint = true; // Geen printer fuctionaliteit
    this.noSetSummary = true; // Geen summary zetten in de titelbalk
    this.noLoading = true; // Geen loading
  }
}

ResultsetTable.prototype.processResultset = __rsProcessResultset;
ResultsetTable.prototype.startTable = __rsStartTable;
ResultsetTable.prototype.makeTableHeader = __rsMakeTableHeader;
ResultsetTable.prototype.makeTableRow = __rsMakeTableRow;
ResultsetTable.prototype.makeTableFootnote = __rsMakeTableFootnote;
ResultsetTable.prototype.endTable = __rsEndTable;
ResultsetTable.prototype.addColumn = __rsAddColumn;
ResultsetTable.prototype.startPrint = __rsStartPrint;
ResultsetTable.prototype.endPrint = __rsEndPrint;
ResultsetTable.prototype.addAction = __rsAddAction;
ResultsetTable.prototype.rowFilter = function (oRs) { return true };

function __rsAddAction(params)
{
    /* Nu ook inline actions bij PDA (hamburger)

    if (device.test(device.isTouch) && !params.isDefault) {
        params.single = false; // touch heeft geen mouseover. Dan alleen de default onclick
    }

    */

    this.actions.push(params);
};

// JGL: O.a. UWVA smoelenboek
ResultsetTable.prototype.ProcessAsXMLXSL = function()
{
    var xmlDoc = new ActiveXObject("MSXML2.DOMDocument.6.0");

    xmlDoc.appendChild(xmlDoc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"windows-1252\""))
    // MGE: De encoding komt niet in de xml te staan, maar alleen de version: < ?xml version="1.0"? >. MSXML print nooit de encoding.
    //      Kan dit problemen geven?
    var FCLTElement = xmlDoc.createElement("facilitor");

    var FCLTHeader = xmlDoc.createElement("header");

    var param;
    var info = { //ID: this.ID,
                 file: String(Request.ServerVariables("SCRIPT_NAME")).toLowerCase(),
                 dateTime: toDateTimeString(new Date(), true),
                 dateDay: (new Date).getDate(),
                 dateMonth: (new Date).getMonth()+1,
                 dateYear: (new Date).getFullYear(),
                 user: customerId,
                 rapportmode: getQParam("rapportmode", null), // internal abuse only
                 custId: customerId,
                 language: user.lang()
                 // ....
               }
    for (param in info)
    {
        if (info[param] != null)
        {
            var FCLTdata = xmlDoc.createElement(param);
            FCLTdata.appendChild(xmlDoc.createTextNode(info[param]));
            FCLTHeader.appendChild(FCLTdata);
        }
    }
    FCLTElement.appendChild(FCLTHeader);

    var rootElement = xmlDoc.createElement("resultset");
    rootElement.setAttribute("id", this.ID);

  with(this)
  {
    var oRs = Oracle.Execute(sql);
    var theSqlFlex = "";
    if (this.printFlex)
        theSqlFlex = getSqlFlex(flexModule.toUpperCase(), flexId, this.myRs(flexParams, oRs));
    // nooit meer dan qp_maxrows2
    for (var cnt = 0; !oRs.EOF && cnt < S("qp_maxrows2"); cnt++)
    {
        // Vooralsnog geen flex
        var elementRecord = xmlDoc.createElement("data_row");

        for (var i = 0; i < columns.length; i++)
        {
            var fieldname;
            if (columns[i].orgContent instanceof Function)
            {
                fieldname = columns[i].orgContent.toString().match(/function\s*(\w*)/)[1];
                if (!fieldname)
                    fieldname = columns[i].xmltag; // Bij anonieme contentfunctie moet je xmltag toevoegen
                if (!fieldname)
                    INTERNAL_ERROR_MISSING_XMLTAG;
            }
            else
            {
                fieldname = columns[i].orgContent;
            }
            var elementField = xmlDoc.createElement(fieldname.toLowerCase() /*"field"*/);
            try
            {
              var val = (columns[i].content != null ? columns[i].content(oRs, outputmode) : "");
            }
            catch (e)
            {
              __Log("resultset_table_v2.inc: Probleem met het ophalen van columns[" + i + "]: " + columns[i].caption);
              throw(e);
            }
            var elementFieldText = xmlDoc.createTextNode(val);
            elementField.appendChild(elementFieldText);
            elementRecord.appendChild(elementField);
        }
        if (this.printFlex)
        {
            var flexKey = (!oRs.Fields(flexId).Value ? oRs.Fields("id").Value : oRs.Fields(flexId).Value); // primary key van tabel uit recordset of id uit dataset van api2-model
            completeSqlFlex = "SELECT * FROM (" + theSqlFlex + ")"
                            + " WHERE flexparentkey = " + flexKey // De flexKey invullen in de sqlFlex query
                            + " ORDER BY volgnummer, omschrijving";

            var oRsFlex = Oracle.Execute(completeSqlFlex);
            while (!oRsFlex.EOF)
            {
                if (oRsFlex("waarde").Value)
                {
                    var elementField = xmlDoc.createElement("kenmerk");
                    //completeSqlFlex bevat (helaas nog) geen "kenmerk_key"
                    var info = ["omschrijving", "kenmerktype", "nmin", "nmax", "volgnummer", "dimensie", "decimalen"];
                    for (var param in info)
                    {
                        if (oRsFlex(info[param]).Value)
                            elementField.setAttribute(info[param], oRsFlex(info[param]).Value);
                    }

                    var elementFieldText = xmlDoc.createTextNode(flexValue(oRsFlex));
                    elementField.appendChild(elementFieldText);
                    elementRecord.appendChild(elementField);
                }
                oRsFlex.MoveNext();
            }
            oRsFlex.Close();
        }

        rootElement.appendChild(elementRecord);
        oRs.MoveNext();
    }
    oRs.close();
    FCLTElement.appendChild(rootElement);
    xmlDoc.appendChild(FCLTElement);

    Response.ContentType = "text/xml";
    Response.Clear();

    if (__Logging & 1)
    {
          var in_file = shared.tempFolder() + "/";
          in_file = in_file + customerId + this.ID + ".xml";
          var fs  = Server.CreateObject("Scripting.FileSystemObject");
          var ts  = fs.CreateTextFile(in_file, true);
          ts.WriteLine (xmlDoc.xml);
          ts.Close();
    }

    Response.ContentType = "text/html";
    var xslfile = Server.MapPath(getCustXsl()); // FSN#39809

    var xslt = new ActiveXObject("Msxml2.XSLTemplate.6.0");
    var xsldoc = new ActiveXObject("Msxml2.FreeThreadedDOMDocument.6.0");
    var xslproc;
    xsldoc.async = false;
    xsldoc.setProperty("AllowXsltScript", true);
    xsldoc.load(xslfile);
    xsldoc.resolveExternals = true; // XSL kan includes hebben
    xsldoc.validateOnParse = true; // en moet correct zijn
    xslt.stylesheet = xsldoc;
    xslproc = xslt.createProcessor();
    xslproc.input = xmlDoc;
    //xslproc.addParameter("mode", mode);
    //xslproc.addParameter("srtnotificatiecode", srtnotificatie);
    xslproc.output = Response;
    xslproc.transform();
    Response.End;
  }
}

ResultsetTable.prototype.ProcessAsCSV = function _ProcessAsCSV()
{
    var oRs;
    if (this.pRs)
        oRs = this.pRs;
    else if (this.dataset)
        oRs = new data_recordset( this.dataset );
    else
        oRs = Oracle.Execute(this.sql);
    // Header row
    var safeheader = [];
    for (var i = 0; i < this.columns.length; i++)
        safeheader.push(this.columns[i].caption);

    if (isCSV2XLSX) // dan maken een CSV bestand in de TEMP folder en leveren een JSON op
    {
        var fso = new ActiveXObject("Scripting.FileSystemObject")
        var tmpcsv = "CSV_" + shared.random(16) +".csv"
        var uitvoerstream  = fso.CreateTextFile(shared.tempFolder() + "/" + tmpcsv, true);
        Session.LCID = 1033; // English(United States) omdat csv2xlsx_386.exe dat verwacht voor floating points
    }
    else
    {
        var uitvoerstream = Response;
    }
    Response.Clear(); // ook voor csv

    uitvoerstream.Write(safeheader.join(";") + "\r\n");

    while (!oRs.EOF)
    {
        var saferow = [];
        for (var i = 0; i < this.columns.length; i++)
        {
            try
            {
              var val = this.columns[i].content(oRs, this.outputmode);
            }
            catch (e)
            {
              __Log("resultset_table_v2.inc: Probleem met het ophalen van columns[" + i + "]: " + this.columns[i].caption);
              throw(e);
            }
            saferow.push(safe.csv(val));
        }

        uitvoerstream.Write(saferow.join(";") + "\r\n");

        oRs.MoveNext();
    }
    oRs.close();

    if (isCSV2XLSX)
    {
        uitvoerstream.Close();
        Response.ContentType = "application/json";
        var result = {
                        "tmpcsv" : tmpcsv,
                        "columns" : this.columns,
                        "colspec" : [] // voor csv2xlsx
                     };
        for (var i = 0; i < this.columns.length; i++)
        {
            // Type is one of: text|number|integer|currency|date|standard|percent|formula|format
            var csvtype = "standard";
            switch (this.columns[i].datatype)
            {
                // let op: format truc werkt alleen voor numbers (waar parseFloat op toegepast kan worden)
                case "date"    : csvtype = "date";            break; // kan dit beter? format:"d-m-jjjj" of zo?
                case "datetime": csvtype = "date";            break; // kan dit beter? format:"d-m-jjjj uu:mm" of zo?
                case "time"    : csvtype = "standard";        break; // kan dit beter? format:"uu:mm" of zo?
                case "currency": csvtype = "format:###0.00";  break;
                case "float"   : csvtype = "number";          break;
                case "number"  : csvtype = "integer";         break;
                case "html"    : return val;
            }
            result.colspec.push(String(i) + ":" + csvtype);
        }
        Response.Write(JSON.stringify(result));
    }
    else
    {
        Response.ContentType = "text/csv";
    }
    ASPPAGE_END();
    Response.End;
}

ResultsetTable.prototype.excelTableHeader = function _excelTableHeader(oWs, row, oRs, oRsFlexData)
{
    var maxlengths = [];
    for (var i = 0; i < this.columns.length; i++)
    {
        var caption = this.columns[i].caption;
        caption = caption.replace(/(<([^>]+)>)/ig,""); // strip html
        caption = caption.replace(/\&nbsp\;/ig, " ");// &nbsp; naar spatie
        var len = oWs.write_string(row, i, caption, 1); // 1 = bold
        maxlengths.push(len);

        oWs.set_column_width(i, 8.43 /* LXW_DEF_COL_WIDTH */, 1); // Forceer alvast 'wrap' omdat met constant memory het niet achteraf kan
    }
    if (this.printFlex)
    {
        for (var i_f=0; i_f < oRsFlexData.length; i_f++)
        {
            var caption = oRsFlexData[i_f].header;
            var len = oWs.write_string(row, i + i_f, caption, 1); // 1 = bold
            maxlengths.push(len);
        }
    }

    return maxlengths;
}

ResultsetTable.prototype.ProcessAsNativeExcel = function _ProcessAsNativeExcel(formail)
{

    Response.Clear(); // voor de zekerheid

    var oRs;
    if (this.pRs)
        oRs = this.pRs;
    else if (this.dataset)
        oRs = new data_recordset( this.dataset );
    else
        oRs = Oracle.Execute(this.sql);

    var oExcel = new ActiveXObject("SLNKXLSX.Excel");
    var tmpxlsx = "XLSX_" + shared.random(16) +".xlsx"
    var oWb = oExcel.workbook_new(shared.tempFolder() + "/" + tmpxlsx, 1); // 1==constant memory
    oWb.set_property("author", user.naam());
    oWb.set_property("title", this.title);
    oWb.set_property("comments", "Created with Facilitor v{0}".format(FCLTVersion));

    var oWs = oWb.add_worksheet("Sheet1");


    var printedFirstHeader = false; // Is de eerste header afgedrukt?. Bij isExcel en isPrinting worden namelijk meerdere headers afgedrukt
    var printMoreHeaders = false; // Wordt gebruikt bij isExcel en isPrinting om meerdere headers af te drukken, maar dan moet de flexModule en flexId wel gedefineerd zijn
    // Alleen    headers weergeven bij printen naar printer of excel
    // en indien de flexkenmerkquery, flexid en flexgroupid is meegegeven
    if (this.printFlex && this.flexGroupId)
      printMoreHeaders = true;
    var lastFlexGroupKey = -1; // Wordt gebruikt bij isExcel. Start without a previous flex group, the first one is new.
    var lastFlexParentGroupKey = -1 // Wordt gebruikt bij isExcel. Start without a previous flex group, the first one is new.
    var flexGroupChanged = false;

    var theSqlFlex = "";
    if (this.printFlex)
        theSqlFlex = getSqlFlex(this.flexModule.toUpperCase(), this.flexId, (this.flexParams instanceof Function ? this.flexParams(oRs) : this.flexParams));
    var lastHadFlex = false;

    oWs.freeze_panes(1, 0);
    // 1e Header row respecteren we maxlength's ook van.
    var maxlengths = [];

    // Ik wil altijd de originele waardes hebben. Binnen Excel is toch alles 'safe'
    safe.html = safe.htmlattr = function (waarde, params)
                {
                    return waarde || "";
                }
    safe.curr = function (waarde, params)
                {
                    return waarde;
                }
    var rownum = 0;
    while (!oRs.EOF)
    {
        if (!this.rowFilter(oRs)) // Vanuit MJOB en fin
        {
             oRs.MoveNext();
             continue;
        }

        var oRsFlex = null;
        if (this.printFlex)
        {
          var flexKey = (!oRs.Fields(this.flexId).Value ? oRs.Fields("id").Value : oRs.Fields(this.flexId).Value); // primary key van tabel uit recordset of id uit dataset van api2-model
          var oRsFlexData = getFlexData(theSqlFlex, flexKey, { showProperties: this.showProperties });  // is een JSON

          if (!oRsFlexData.length)
            var noFlexResult = true; // Er zijn geen flexkenmerken
          else
            var noFlexResult = false; // Er zijn flexkenmerken

          if (this.flexGroupId) // Is er een flexGroupId meegegeven
          {
            var flexGroupKey = this.myRs(this.flexGroupId, oRs);
            flexGroupChanged = (lastFlexGroupKey != flexGroupKey); // Alleen bij printen naar excel moet er wellicht een nieuwe header boven i.v.m. andere flexkenmerken
            var flexChanged = flexGroupChanged;
            lastFlexGroupKey = flexGroupKey;

            if (this.flexParentGroupId) // Is er een flexParentGroupId meegegeven
            {
              var flexParentGroupKey = this.myRs(this.flexParentGroupId, oRs);
              flexParentGroupChanged = (lastFlexParentGroupKey != flexParentGroupKey); // Alleen bij printen naar excel moet er wellicht een nieuwe header boven i.v.m. andere flexkenmerken
              if (!flexParentGroupChanged && flexGroupChanged && this.flexChangeNiveau && !noFlexResult && !flexChanged)
              { // Flexgroep is veranderd maar parentflexgroep niet. Alleen als er op flexgroep niveau flexkenmerken zijn gedefinieerd is er een nieuwe header nodig.
                flexChanged = false;
                for (var datai=0; i < oRsFlexData.length; i++)
                {
                  var rec = oRsFlexData[datai];
                  var kenmerk_niveau = rec.niveau;
                  if (kenmerk_niveau == this.flexChangeNiveau)
                  {
                    flexChanged = true;
                    break;
                  }
                }
              }
              flexChanged = flexParentGroupChanged || flexChanged; // Als parent group veranderd is zijn er waarchijnlijk ook andere kenmerken. Kan voor meldingen nog scherper i.v.m. nog een niveau (parent) hoger. Maar is in praktijk nog niet echt nodig.
              lastFlexParentGroupKey = flexParentGroupKey;
            }
          }
        }

        if (!printedFirstHeader || (printMoreHeaders && flexChanged && (!noFlexResult || (lastHadFlex && noFlexResult))))
        {
          // Header van ResultsetTable
          var ml = this.excelTableHeader(oWs, rownum++, oRs, oRsFlexData);
          if (rownum == 1)
            maxlengths = ml;
          printedFirstHeader = true;
        }
        lastHadFlex = !noFlexResult;

        var saferow = [];
        for (var i = 0; i < this.columns.length; i++)
        {
            try
            {
              if (this.columns[i].orgContent instanceof Function)
              {
                var val = this.columns[i].orgContent(oRs, this.outputmode)
              }
              else
              {
                var val = oRs.Fields(this.columns[i].orgContent).Value;
              }
            }
            catch (e)
            {
              __Log("resultset_table_v2.inc: Probleem met het ophalen van columns[" + i + "]: " + this.columns[i].caption);
              throw(e);
            }

            // Zie ook api2_rest.inc function _json2xlsx
            if (val != null && typeof val != "undefined")
            {
                if (typeof val == "object" && "name" in val)
                {
                    val = val["name"];
                    this.columns[i].datatype = null; // default string
                }

                if  (typeof val == "string" && (this.columns[i].orgContent instanceof Function || this.columns[i].datatype == 'html'))
                {
                    val = val.replace(/(<([^>]+)>)/ig,"");  // strip html
                    val = val.replace(/\&nbsp\;/ig, " ");// &nbsp; naar spatie;
                    val = val.replace(/\&amp;\;/ig, "&");// er zijn er nog meer maar dit vind ik genoeg
                    val = val.replace(/\&lt\;/ig, "<");
                    val = val.replace(/\&gt\;/ig, ">");
                    val = val.replace(/\&apos\;/ig, "'>'");
                    val = val.replace(/\&quot\;/ig, "\"");
                    // TODO: andere html-entities ook vertalen?
                }

                var len = 0;
                if  (typeof val == "string" && inArray(this.columns[i].datatype, ["date", "datetime", "time", "currency", "float", "number"]))
                {
                   if (!(this.columns[i].orgContent instanceof Function))
                       __Log("Vreemd: kolom {0} is string en niet van het type {1}?".format(this.columns[i].caption, this.columns[i].datatype), "#ff0000");
                   len = oWs.write_string(rownum, i, val);
                }
                else
                    switch (this.columns[i].datatype)
                    {
                        case "date"    : oWs.write_vardate(rownum, i, new Date(val).getVarDate(), 0); //TODO .midnight() toepassen eindigt in verkeerde tijdzone?
                                         break;
                        case "datetime": oWs.write_vardate(rownum, i, new Date(val).getVarDate(), 1);
                                         break;
                        case "time"    : oWs.write_vardate(rownum, i, new Date(val).getVarDate(), 2);
                                         break;
                        case "currency": oWs.write_number(rownum, i, val, 1);
                                         break;
                        case "float"   : oWs.write_number(rownum, i, val, 1);
                                         break;
                        case "number":   oWs.write_number(rownum, i, val, 0);
                                         break;
                        case "processingtime":
                                        if (val["duration"] != null)
                                            val = val["duration"] + " " + (val["unit"]=='U'?L("lcl_mld_hours"):L("lcl_mld_days"));
                                        else
                                            val = "";
                                        len = oWs.write_string(rownum, i, val);
                                        break;
                        case "html"    : // hij is hierboven al decoded
                                        len = oWs.write_string(rownum, i, val);
                                        break;
                        default:
                              // Autotypering "datum"
                              if (typeof val == "date" || val instanceof Date)
                              {
                                oWs.write_vardate(rownum, i, new Date(val).getVarDate(), 0); // params.prettydate toepassen?
                              }
                              else
                              {
                                len = oWs.write_string(rownum, i, String(val));
                              }
                    }
                if (len > maxlengths[i])
                    maxlengths[i] = len;

            }
        }

        if (this.printFlex)
        {
           // equivalent van html += addFlexKenmerkKolommen(oRsFlexData, this,
          for (var i_f=0; i_f< oRsFlexData.length; i_f++)
          {
            var rec = oRsFlexData[i_f];
            var val = rec.value; // altijd string!
            if (val == null || val === "") // Leeg flexkenmerk is vreemd?
                continue;
            // Type  'C','N','D','T','R','X','L','Q','F','M','E','S','l','B','V'
            // Sommige kunnen we slimmer
            // Q/L en negatieve volgnummers heeft function getFlexData er al uit gestript

            switch(rec.type)
            {
                case "E": // Encrypted
                case "F": // Komen nog niet voor/gaan niet goed met kenmerk_niveau vanwege RWSN#78535
                    var flex_bijlagen = flexProps(this.flexModule, rec.key, rec.kenmerk_key, rec.kenmerk_niveau);
                    if (flex_bijlagen.files.length)
                        oWs.write_string(rownum, i + i_f, flex_bijlagen.files[0].name);
                    break;
                case "N": // numeriek
                    var fval = parseFloat(val);
                    if (isNaN(fval)) // geeft ongeldige xlsx
                    {
                        __DoLog(rec);
                        __DoLog("NaN, waarde is geen float: " + val);
                    }
                    if (rec.decimalen)
                      oWs.write_number(rownum, i + i_f, fval, 1); // Altijd 2 decimalen
                    else
                      oWs.write_number(rownum, i + i_f, fval, 0); // geen decimalen
                    break;
                case "D": // datum
                    var a = /^(\d{2})-(\d{2})-(\d{4})$/.exec(val); // 27-02-2024
                    if (a) {
                        var dtval = new Date(+a[3], +a[2] - 1, +a[1]);
                        oWs.write_vardate(rownum, i + i_f, dtval.getVarDate(), 0);
                    }
                break;
                case "T": // tijd
                    var a = /^(\d{2}):(\d{2})$/.exec(val); // 13:35
                    if (a) {
                        var dtval = new Date(1970, 0, 1, +a[1], +a[2]);
                        oWs.write_vardate(rownum, i + i_f, dtval.getVarDate(), 2);
                    }
                    break;
                case "C": // Character
                case "R": // Referentie
                case "S": // Suggest
                case "X": // Bestandsnaam
                case "V": // checkbox doorvallen naar default?
                default:
                    oWs.write_string(rownum, i + i_f, String(val)); // Expliciete String
            }
          }
        }

        rownum++;
        oRs.MoveNext();
    }
    oRs.close();
    for (var i = 0; i < maxlengths.length; i++)
    {
        if (maxlengths[i] > 8.43 * 10) // LXW_DEF_COL_WIDTH, 8.43 is de default Excel cell width
        {
            oWs.set_column_width(i, 1 + Math.min(maxlengths[i] / 10, 50), 1); // als je (net) te krap instelt wordt de lineheight soms hoger
                                                                              // omdat Excel toch bang is dat er iets wrapt?
        }
    }

    oWb.workbook_close();

    if (formail) // Putorders pakt de temp-file verder op
    {
        Response.ContentType = "application/json";
        var result = {
                        "tmpxlsx" : tmpxlsx,
                        deletefile: __Logging == 0
                     };
        Response.Write(JSON.stringify(result));
    }
    else
    {
        var mime = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        var filename = (this.filename ? this.filename : "Facilitor") + " {0}.xlsx".format(toISODateTimeString(new Date()));
        var pcontentdp = "attachment";
        Response.AddHeader("content-disposition", pcontentdp + "; filename= \"" + safe.ContentName(filename) + "\"")
        StreamFile(shared.tempFolder(), tmpxlsx, { mime: mime ,
                                                   pcontentdp: "none", // geen pcontentdp, dat hebben we zelf gedaan
                                                   deletefile: __Logging == 0});
    }
    ASPPAGE_END();
    Response.End;
}

// SQL Query moet een veld fac_bijlagen_key bevatten en optioneel fac_bijlagen_zippath
ResultsetTable.prototype.ProcessAsZIP = function _ProcessAsZIP()
{
    var oRs;
    if (this.pRs)
        oRs = this.pRs;
    else if (this.dataset)
        oRs = new data_recordset( this.dataset );
    else
        oRs = Oracle.Execute(this.sql);

    var has_path = false;
    for (var i = 0; i < this.columns.length; i++)
    {
        if (this.columns[i].name == "fac_bijlagen_zippath")
            has_path = true;;
    }
    var bijlagen_list = [];
    while (!oRs.EOF)
    {
        if (oRs.Fields("fac_bijlagen_key").Value) {
            var onefile = { fac_bijlagen_key: oRs.Fields("fac_bijlagen_key").Value.id };
            if (has_path)
                onefile.zippath = oRs.Fields("fac_bijlagen_zippath").Value;
            bijlagen_list.push(onefile);
        }
        oRs.MoveNext();
    }
    oRs.close();

    zipfacbijlagen("facilitor.zip", bijlagen_list);
    ASPPAGE_END();
    Response.End;
}

// We bouwen een api2-model van onze recordset
// en geven die door aan fac_usrrap_list_graph (zoals onze rapporten ook doen)
// Proof of concept, niet in gebruik
ResultsetTable.prototype.ProcessAsGraph = function _ProcessAsGraph()
{
    var model = { fields: {} };
    var scf_params = { list: { columns: [] }};

    var oRs;
    if (this.pRs)
        oRs = this.pRs;
    else if (this.dataset)
        oRs = new data_recordset( this.dataset );
    else
        oRs = Oracle.Execute(this.sql);
    // Header row
    var safeheader = [];
    for (var i = 0; i < this.columns.length; i++)
    {
        var typ = this.columns[i].datatype|| "varchar";
        if (typ == "float" || typ == "currency")
        {
            this.columns[i].datatype = "number";
            typ = "number"; // anders gaat het zo maar naar string
        }
        model.fields["col" + i] = { "label": this.columns[i].caption, "typ": typ };
        scf_params.list.columns.push("col" + i); // TODO: welke kolommen?
    }
    var xxx_array = []; // met data
    while (!oRs.EOF)
    {
        var saferow = {};
        for (var i = 0; i < this.columns.length; i++)
        {
            try
            {
              var val = this.columns[i].content(oRs, this.outputmode);
            }
            catch (e)
            {
              __Log("resultset_table_v2.inc: Probleem met het ophalen van columns[" + i + "]: " + this.columns[i].caption);
              throw(e);
            }
            saferow["col" + i] = val;
        }

        xxx_array.push(saferow);

        oRs.MoveNext();
    }
    oRs.close();

scf_params.list.columns = getQParamArray("graphcols")// "col3,col4" --> ["col3", "col4"];
//__DoLog(model);
//__DoLog(scf_params);
//__DoLog(xxx_array);
    fac_usrrap_list_graph (model, scf_params, xxx_array)
    Response.End;
}

ResultsetTable.prototype.countHistogram = function(oRs)
{
    var i, kolomnaam, kolomval, cnt;
    for (i= 0; i < oRs.Fields.Count; i++)
    {
        kolomnaam = oRs.Fields(i).Name;
        if (kolomnaam.toUpperCase() == this.keyColumn.toUpperCase())
            continue;
        kolomval = oRs.Fields(i).Value;
        if (!this.Counter[kolomnaam])
            this.Counter[kolomnaam] = {};
        if (!this.Counter[kolomnaam][kolomval])
            this.Counter[kolomnaam][kolomval] = 1;
        else
            this.Counter[kolomnaam][kolomval] += 1
    }
}

function __rsProcessResultset(processParams) // processParams wordt blind aan alle callbackfuncties gegevens als tweede parameter
{
  var result = { modified: false };
  if (isPlain || isPrinting) {
    if (this.tabs_code) {
      result = reorderPerslidCols(this.tabs_code, this.columns);
      this.columns = result.columns;
%>    <!-- COLUMNS --> <%
      if (!this.inline && isPlain) {
        rs_columns(this.tabs_code, this.columns);
      }
    }
    if (!this.tabs_code || !result.modified) { // Default kolommen
      for (var i in this.columns) {
        if (this.columns[i].showDefault === false) { // Onderdruk deze kolommen
          this.columns[i].hidden = true;
        }
      }
    }
  }

  if (this.sql == null && !this.dataset)
    return;

  this.processParams = processParams;

  this.noPrint |= !device.test(device.canPrint);
  this.noExcel |= !device.test(device.canExcel);

  if (isXMLXSL)
  {
    this.ProcessAsXMLXSL();
    return; // die is zo compleet anders....
  }

  if (isCSV || isCSV2XLSX)
  {
    this.ProcessAsCSV();
    return; // die is zo compleet anders....
  }
  if (isNativeExcel)
  {
    this.ProcessAsNativeExcel(this.outputmode == 8);
    return; // die is zo compleet anders....
  }

  if (isZIP)
  {
    this.ProcessAsZIP();
    return; // die is zo compleet anders....
  }

  if (isGraph)
  {
    this.ProcessAsGraph();
    return; // die is zo compleet anders....
  }

  if (this.canBookmark)
  {
      if (Request.ServerVariables("REQUEST_METHOD") == "POST") // POST is nog te lastig
          INTERNAL_ERROR_CANNOT_BOOKMARK_POST;

      if (book_id || getQParam("bybookmark",0)==1)
          this.canBookmark = false;
  }

  with(this)
  {
    if (isPrinting)
      startPrint();

    // Print filter tabel met filterwaarden van het zoekscherm
    var html_filter = "";
    if (isPrinting == true)
    {
      if (filterParams)
        html_filter = getRSFilterTable(filterParams);
    }
    Response.Write(html_filter);

    // class moet hetzelfde zijn zodat summary text overal (incl inline overzichten) hetzelfde uitelijk krijgen
    // id moet verschillend zijn omdat je meerdere rsSummary divs onafhankelijk van elkaar moet benaderen (vullen)

    if (isPrinting)
    {
%>
      <div id="rsSummary<%=summaryId%>" class="rsSummary" style="display:none">
        <span id="rsSummaryInfo<%=summaryId%>" class="rsSummaryInfo"></span>
      </div>
      <script>
        function setSummary(html)
        {
           if (html != "")
           {
             var elm = document.getElementById("rsSummaryInfo<%=summaryId%>");
             elm.innerHTML = html;
             var elm = document.getElementById("rsSummary<%=summaryId%>");
             elm.style.display='block';
           }
        }
      </script>
<%
    }
    else if (isPlain)
    {
      if (this.rstableNr == 1)
      {
%>
      <script>
        var postformHTML = '<%=safe.jsstring(postformHTML)%>'; <% /* postformHTML naar javascript voor gebruik in resultset_table_v2.js */ %>
        var /*function*/ setSummary = FcltMgr.setHeaderExtraTitle;
        var result_refresher_url = "<%= safe.jsstring(this.refresher_url) %>";
        var result_refresher_url = <%= (this.refresher_timer||0) %>;
      </script>
<%
      }
      if (this.canBookmark)
      {
%>
        <script>
          function saveBookmark($dialog)
          {
            var bmtitel = $dialog.find("#bmtitel").val();
            var data = { path: "<%=safe.jsstring(String(Request.ServerVariables("SCRIPT_NAME")))%>",
                         query: "outputmode=" + (<%=this.advancedprint?3:outputmode%>) + "<%=safe.jsstring(transitQS())%>",
                         naam: bmtitel
                       };
            <% protectRequest.dataToken("data"); %>
            $.post("../shared/create_bookmark.asp",
                   data,
                   FcltCallback, "json");
          }

          function createBookmark(evt, deze)
          {
            params = {oktekst: L("lcl_fac_bookmark"),
                      title: L("lcl_fac_bookmark"),
                      ishtmlsafe: true,
                      fncallback: saveBookmark,
                      buttons: [{ text: L("lcl_cancel"), icon: "fal fa-fw fa-undo", class: "alertCancel" }]
            };
            var page_content = "<input type='text' id='bmtitel' class='fld' value='<%=safe.htmlattr(this.title)%>'>";
            FcltMgr.mydialog(page_content, params);
          }
        </script>
<%
      }
      if (!noPrint)
      {
          if (this.rstableNr == 1)
          {
%>
        <script>
          function doPrint(poutputmode)
          {
              var autosortby = "";
              var xx = $("th.sorttable_sorted,th.sorttable_sorted_reverse");
              if (xx.length)
              {
                  if (xx.html().split("<br>").length > 1)
                  {
                      autosortby = xx.html().split("<br>")[0];// Bij combined alleen de eerste
                  }
                  else // moeten we complexer strippen
                  {
                      autosortby = xx.text();
                      autosortby = autosortby.substring(0, autosortby.length - 1); // Spatie er af halen. Font Awsome teken is een html <i> tag/element en geen karakter.
                  }
              }
              if (xx.hasClass("sorttable_sorted_reverse"))
              {
                  autosortby = "-" + autosortby;
              }

<%            if (Request.ServerVariables("REQUEST_METHOD") == "POST")
              { %>
                document.printform.autosortby.value = autosortby;
                document.printform.outputmode.value = poutputmode;
                document.printform.submit();
<%            }
              else
              { %>
                FcltMgr.windowopen("<%=Request.ServerVariables("SCRIPT_NAME")%>?outputmode=" + poutputmode + "&autosortby=" + escape(autosortby) + "<%=safe.jsstring(transitQS())%>");
<%            } %>
          }

          function doOutput(poutputmode)
          {

<%          if (Request.ServerVariables("REQUEST_METHOD") == "POST")
            { %>
              // Alle parameters zijn in een formulier gestopt als request methode POST is.
              if (poutputmode == 0)
                document.printform.target = "_self";
<%          }
            var hasFlex = this.flexModule && this.flexId;
%>
            if (!<%=hasFlex?"true":"false"%> || !(poutputmode == 1 || (poutputmode == 2 && ("<%=S("excel_mode")%>" == "0" || "<%=S("excel_mode")%>" == "2"))))
            {
                doPrint(poutputmode);
                return;
            }
            params = { oktekst: L("lcl_Yes"), title: L(poutputmode==1?"lcl_print":"lcl_export_to_excel") };
            params.fncallback = function ()
                                {
                                    doPrint(poutputmode); // poutputmode wordt 5 (printer zonder flex) of 6 (excel zonder flex);
                                    // close gaat vanzelf;
                                }

            params.buttons = [
                {
                    text: L("lcl_No"),
                    icon: "fal fa-fw fa-times",
                    class: 'resultprintno',
                    click: function()
                    {
                        doPrint(poutputmode + 4);
                    }
                },
                {
                    text: L("lcl_cancel"),
                    icon: "fal fa-fw fa-undo",
                    class: 'resultprintcancel',
                }];
            FcltMgr.mydialog(L("lcl_print_properties"), params);
          }
        </script>
<%
          }
      }
      if (this.dataset && this.urllink)
      {
%>
        <script>
          function clickAction(urlkey, titel)
          {
            FcltMgr.openDetail("<%=safe.htmlattr(urllink)%>" + urlkey, "");
          }
        </script>
<%
      }

      var asSavedWidget = getQParamInt("asSavedWidget", 0) == 1;
      if (asSavedWidget)
      { %>
        <script>
          function _toggleExpandIcon()
          {
              $("#searchBlockToggler > i").toggleClass("fa-search-plus fa-search-minus");
          }

          function toggleSearchBlock()
          {
              _toggleExpandIcon();
              parent.toggleSearchBlock();
          }

           $(function() { // Ik zoek opnieuw (of ververs) en het zoekblok is opengeklapt, corrigeer dan het expand-icoontje
              if (!$(parent.document.body).find(".container-fluid.hidden").length)
                  _toggleExpandIcon();
           });
        </script>
<%    }

      if (book_id)
      {
        this.actions = [];
        this.buttons = [];
      }

      buttons = this.buttons || [];
      if (this.roundtripCode) { %>
        <script>
          function doRoundtrip_<%=this.roundtripCode%>_exp()
          {
              var url = "../fac/fac_roundtrip_exp.asp?app_code=<%=this.roundtripCode%>";
              FcltMgr.windowopen(url);
          }

          function doRoundtrip_<%=this.roundtripCode%>_imp()
          {
              var url = "appl/fac/fac_roundtrip_imp.asp?app_code=<%=this.roundtripCode%>";
              FcltMgr.openDetail(url, L("lcl_import"));
          }
        </script>
<%      buttons.push({ icon: "fa-file-upload", title: L("lcl_upload"), action: "doRoundtrip_" + this.roundtripCode + "_imp()", order: 2 });
        buttons.push({ icon: "fa-file-download", title: L("lcl_download"), action: "doRoundtrip_" + this.roundtripCode + "_exp()", order: 3 });
        buttons.push({ horizontalRule: true, order: 1 }); // Vanwege vieze CSS truukjes moet deze als laatste gepushed worden
      }

      if (!noPrint && Request.ServerVariables("REQUEST_METHOD") == "GET") // Alleen dan auto-refreshknop
      {
        var found = false;
        for (var i=0; i < buttons.length; i++)
        {
            if (buttons[i].title == L("lcl_refresh") || buttons[i].action == "FcltMgr.reload()")
                found = true;
        }
        if (!found)
            buttons.push({ icon: "fa-fclt-refresh", title: L("lcl_refresh"), action: "FcltMgr.reload()", id: "btn_scf_refresh" });
      }
      if (!noPrint)
      {
        buttons = buttons.concat([ { icon: "fa-print", title: L("lcl_print_table"),     action: "doOutput(1)" } ]);
        if (!noExcel)
        {
            buttons = buttons.concat([ { icon: "fa-table", title: L("lcl_export_to_excel"), action: "doOutput(2)" } ]);
        }
      }
      if (this.advancedprint)
      {
          buttons.push({ icon: "fa-address-card",  title: advancedprint_title, action: "doOutput(3)" }); // advanced
      }

      if (this.canCSV && !noExcel)
      {
          buttons.push({ icon: "fa-file", title: L("lcl_export_to_csv"), action: "doOutput(4)" }); // CSV
      }

      if (this.canBookmark)
        buttons.push({ icon: "fa-bookmark", title: L("lcl_fac_bookmark"), action: "createBookmark(window.event, this)" });

      if (asSavedWidget)
        this.buttons.push({ icon: "fa-search-plus", id: "searchBlockToggler", action: "toggleSearchBlock();", tooltip: L("lcl_search") });

      if (this.title || buttons.length > 0 || (this.rstCheckBoxes && this.rstCheckBoxes.length > 0) )
         IFRAMER_HEADER(
          this.title || "",
          buttons, { id: this.rstableNr,
          inlineId: (this.inline && this.filterParams && this.filterParams.mld_key ? this.filterParams.mld_key : ""), collapsible: this.collapsible },
          this.rstCheckBoxes
        );


      if (this.rstableNr == 1)
      { // collapseblock sluiten we nooit omdat we niet weten of er nog een volgende tabel komen.
        // Een browser sluit dan vanzelf aan het einde en dan klopt het weer. Niet perfect maar ach..
%>
        <div id="collapseblock">
<%
      }
    } // isPlain

    var html = "";

    if (isPlain)
    {
        // ========= ACTIONS =========
        // == zijn er multi ACTIONS?
        var anyMultiActions = false;
        for (var i=0; i<this.actions.length; i++)
        {
              anyMultiActions |= this.actions[i].multi;
        }

        // Eerst: maak één div met alle acties.
        var hasActionsCol = 0;
        var skip = 0;
        for (var i=0; i < columns.length; i++)
        {
            if (columns[i].combine)
                skip ++;
            if (columns[i].hasActions)
            {
                hasActionsCol = i - skip;
                break;
            }
        }
        if (anyMultiActions)
            hasActionsCol++;
        if (this.inlineDetails)
            hasActionsCol++;

        // Nu de inline buttons renderen. Ze zijn er één keer en worden voor alle regels hergebruikt
        var actionsDiv = "";
        var aantalInline = 0;
        for (var i = 0; i < this.actions.length; i++)
        {
            if (disableInlineActions && !this.actions[i].isDefault)
                continue;
            if (this.actions[i].single === false || this.actions[i].onlyMulti) // triple '=' omdat undefined niet false mag zijn
                continue; // niet ook nog inline
            if (!this.actions[i].isDefault)
                aantalInline++;
            var def = (this.actions[i].isDefault? " isdefault='1'" : "")
                    + (this.actions[i].onlyMulti? " onlymulti='1'" : "")
                    + (this.actions[i].isDefault || this.actions[i].onlyMulti? " style='display:none'" : "");
            var tooltip_actions = this.actions[i].tooltip? " title='" + safe.htmlattr(this.actions[i].tooltip) + "'" : "";
            var disp = useHamburger != 0 ? " style='display: block;'" : "";
            if (this.actions[i].multiOnce)
                val = "<span class='ia2' " + def + " onclick='doAm(event, this, \"" + this.actions[i].action + "\")'" + disp + tooltip_actions + ">";
            else
                val = "<span class='ia2' " + def + " onclick='doA(event, this, \"" + this.actions[i].action + "\")'" + disp + tooltip_actions + ">";
            actionsDiv += val + this.actions[i].caption + "</span>";
        }
        hasInlineActions = aantalInline > 0;
        html += "\n<div id='allactions_" + ID + "' class='allactions' style='display:none;z-index:13' hasActionsCol='"+hasActionsCol+"' >" + actionsDiv + "</div>"
              + "\n<script type='text/javascript'>"
              + "\n    initActions('" + this.ID + "', "+useHamburger+")"
              + "</script>";
    }
    html += startTable();

    if (!Response.IsClientConnected)
    {
        __Log("Response.IsClientConnected false, Skipping sql query.", "#00FF00");
        Response.End;
    }
    // read general totals and general averages, if any
    if (this.sqltotals)
    {
      oRs_tot = Oracle.Execute(sqltotals);
      if (!oRs_tot.EOF)
      {
        for (fld in columns)
        {
          var field = columns[fld];
          if (field.generaltotal)
          {
            field.generaltotalsum = oRs_tot(field.name).Value;
          }
          else if (field.generalaverage)
          {
            field.generaltotalavg = oRs_tot(field.name).Value;
          }
        }
      }
      oRs_tot.Close();
    }

    // vanuit fac_usrrap_list krijgen we een open recordsset pRs door
    // hij moest hem zelf al openen om de kolommen te bepalen
    var oRs;
    if (this.pRs)
        oRs = this.pRs;
    else if (this.dataset)
        oRs = new data_recordset( this.dataset );
    else
        oRs = Oracle.Execute(sql);

    if (!Response.IsClientConnected)
    {   // Gebruiker was te ongeduldig. Hoeven we de rest ook niet meer te doen
        __Log("Response.IsClientConnected false, discarding result.", "#FFFF00");
        Response.End;
    }

    if (this.subtabicon)
    {
        var sti = (subtabicon instanceof Function ? subtabicon(oRs) : subtabicon);
        if (sti && sti.icon)
        {
            var subtabicon = sti.icon;
            var color = sti.color;
%>      <script>
            FcltMgr.setSubtabExtra("<span class='subtab-badge-icon' style='color:<%=color%>'>" + I("<%=subtabicon%> fa-lg", { "fastyle": "fas"}) + "</span>");
        </script>
<%      }

    }

    if (oRs == null || oRs.EOF)
    {
        Response.Write("<div id='rstable'><table><tr><td class='emptyset'>" + emptySetString + "</td></tr>");
        endTable();
        if (isPrinting)
          endPrint();
        if (oRs)
            oRs.close();

        var badgehtml = "";
        if (this.required)
        {
          badgehtml = "<span class='subtab-badge-icon subtab-badge-missing'>"
                    +     I("fa-exclamation-triangle fa-lg", { "fastyle": "fas" })
                    + "</span>"
        }
        if (!this.subtabicon) {
%>    <script>
          var prevent_badge_reset = false;
<%        if (!badgehtml && this.rstableNr && this.rstableNr > 1)
          { /* Itereer over alle voorgaande resultsets, als er 1 bestaat hoeven we de spinner niet meer uit de badge te halen,
               want dat is dan al gebeurd, en mogelijk staat er ook al een counter in. */
%>          for (var i = <%=this.rstableNr-1%>; i && !prevent_badge_reset; i--) {
              prevent_badge_reset = $("[id=rstable].rstable" + i).length > 0;
            }
<%        } %>
          if (!prevent_badge_reset) {
            FcltMgr.setSubtabExtra("<%=safe.jsstring(badgehtml)%>");
          }
      </script>
<%      }
        return -1;
    }
    else
    {
%>    <script>
            FcltMgr.setSubtabExtra("");
      </script>
<%  }


    var printedFirstHeader = false; // Is de eerste header afgedrukt?. Bij isExcel en isPrinting worden namelijk meerdere headers afgedrukt
    var printMoreHeaders = false; // Wordt gebruikt bij isExcel en isPrinting om meerdere headers af te drukken, maar dan moet de flexModule en flexId wel gedefineerd zijn
    // Alleen    headers weergeven bij printen naar printer of excel
    // en indien de flexkenmerkquery, flexid en flexgroupid is meegegeven
    if ((isPrinting || isExcel || this.showProperties) && this.printFlex && this.flexGroupId)
      printMoreHeaders = true;
    var lastFlexGroupKey = -1; // Wordt gebruikt bij isExcel. Start without a previous flex group, the first one is new.
    var lastFlexParentGroupKey = -1 // Wordt gebruikt bij isExcel. Start without a previous flex group, the first one is new.
    var flexGroupChanged = false;

    var theSqlFlex = "";
    if (this.printFlex)
        theSqlFlex = getSqlFlex(flexModule.toUpperCase(), flexId, (flexParams instanceof Function ? flexParams(oRs) : flexParams));

    var lastHadFlex = false;
    var nrOutOfFilter = 0;
    for (var cnt = 0; (cnt < S("qp_maxrows") + nrOutOfFilter || isPrinting || isExcel || showAll || showProperties) &&
                      (cnt < S("qp_maxrows2") + nrOutOfFilter || isExcel) && // Naar excel altijd alle regels (meer dan S("qp_maxrows2")) weergeven.
                      !oRs.EOF; cnt++)
    {
      if (!Response.IsClientConnected)
      {
          __Log("Response.IsClientConnected false, Resultset aborted.", "#00FF00");
          Response.End;
      }
      if (cnt && cnt % 1000 == 0)
        __Log("Records: " + cnt, "#E0E0FF");

      var oRsFlex = null;
      if ((isPrinting || isExcel || showProperties) && this.printFlex)
      {
        var flexKey = (!oRs.Fields(flexId).Value ? oRs.Fields("id").Value : oRs.Fields(flexId).Value); // primary key van tabel uit recordset of id uit dataset van api2-model
        var oRsFlexData = getFlexData(theSqlFlex, flexKey, { showProperties: showProperties });  // is een JSON

        if (!oRsFlexData.length)
          var noFlexResult = true; // Er zijn geen flexkenmerken
        else
          var noFlexResult = false; // Er zijn flexkenmerken

        if (flexGroupId) // Is er een flexGroupId meegegeven
        {
          var flexGroupKey = this.myRs(flexGroupId, oRs);
          flexGroupChanged = isExcel && (lastFlexGroupKey != flexGroupKey); // Alleen bij printen naar excel moet er wellicht een nieuwe header boven i.v.m. andere flexkenmerken
          var flexChanged = flexGroupChanged;
          lastFlexGroupKey = flexGroupKey;

          if (flexParentGroupId) // Is er een flexParentGroupId meegegeven
          {
            var flexParentGroupKey = this.myRs(flexParentGroupId, oRs);
            flexParentGroupChanged = isExcel && (lastFlexParentGroupKey != flexParentGroupKey); // Alleen bij printen naar excel moet er wellicht een nieuwe header boven i.v.m. andere flexkenmerken
            if (!flexParentGroupChanged && flexGroupChanged && flexChangeNiveau && !noFlexResult && !flexChanged)
            { // Flexgroep is veranderd maar parentflexgroep niet. Alleen als er op flexgroep niveau flexkenmerken zijn gedefinieerd is er een nieuwe header nodig.
              flexChanged = false;
              for (var datai=0; i < oRsFlexData.length; i++)
              {
                var rec = oRsFlexData[datai];
                var kenmerk_niveau = rec.niveau;
                if (kenmerk_niveau == flexChangeNiveau)
                {
                  flexChanged = true;
                  break;
                }
              }
            }
            flexChanged = flexParentGroupChanged || flexChanged; // Als parent group veranderd is zijn er waarchijnlijk ook andere kenmerken. Kan voor meldingen nog scherper i.v.m. nog een niveau (parent) hoger. Maar is in praktijk nog niet echt nodig.
            lastFlexParentGroupKey = flexParentGroupKey;
          }
        }
      }

      if (!printedFirstHeader || (printMoreHeaders && flexChanged && (!noFlexResult || (lastHadFlex && noFlexResult))))
      {
        // Header van ResultsetTable
        html += makeTableHeader(oRs, oRsFlexData, anyMultiActions);
        html += "\n<tbody>";
        printedFirstHeader = true;
      }
      lastHadFlex = !noFlexResult;

      // Een row van ResultsetTable
      if (rowFilter(oRs)) {

          html += makeTableRow(oRs, oRsFlexData, rowNum, anyMultiActions, noFlexResult);
          rowNum = rowNum + 1;

          if (this.buildHistogram)
            this.countHistogram(oRs);

          // Voer berekeningen uit i.v.m. extra info die moet komen in de regel waar ook de print en excel iconen staan
          if (summaryCalc)
            summaryCalc(oRs, this.processParams);

          if (totalCalc)
            totalCalc(oRs, this.processParams);
      }
      else
        nrOutOfFilter++;

      if (isExcel) // Excel heeft geen S("qp_maxrows2") bescherming. Extreem grote rapportages leiden
      {            // tot een out-of-memory. Omdat bij Excel verderop toch niets wordt gedaan met html
                   // gooien we die hier alvast naar buiten zodat Response Buffer Exceeded wel ingrijpt.
        Response.Write(html);
        html = "";
      }
      oRs.MoveNext();
    }

    html += "</tbody>";

    if ((!oRs.EOF || this.hasMore) && !isPrinting && !isExcel)
    {
      var a_element = "";
      if (Request.ServerVariables("REQUEST_METHOD") == "POST")
      {
        // Inline details met POST methode en meer dan max regels tonen werkt niet. Komt ook niet voor.
        a_element = "<a href='javascript:doOutput(0)'>";
      }
      else
      {
        a_element = "<a href='"
                      + (inline // Voor inline details moet niet gehele window vervangen worden omdat je dan ook de hoofd regels kwijt bent (dan alleen de <td> cell vervangen)
                        ? "javascript:loadInlineDetails(\""
                          + safe.jsstring(Request.ServerVariables("SCRIPT_NAME") + "?showall=1" + transitQS())
                          + "\");"
                        : safe.jsstring(Request.ServerVariables("SCRIPT_NAME") + "?showall=1" + transitQS()) + "' target='_self")
                      + "'>";
      }
      a_element += L("lcl_qp_maxrows1").format(S("qp_maxrows")) + "</a>";

      // Op regel klikken om de overige regels op te halen, boven de tabel
      html = "<div id='rsMaxHistoryB'><span id='rsMaxHistoryInfoB'>"
           + (cnt <= S("qp_maxrows") + nrOutOfFilter && S("qp_maxrows") != S("qp_maxrows2")
             ? a_element.replace(/&collapsed=1/gi, "") // Als er op deze regel wordt geklikt moet IFRAMER niet meer dichtgeklapt worden, dus &collapsed=1 verwijderen uit de link.
             : L("lcl_qp_maxrows2").format(S("qp_maxrows2"))
             )
           + "</span></div>" + html;
    }

    // Footnote van ResultsetTable
    // Footnote hoort eigenlijk voor de tbody (HTML4.0) maar dan weten we het totaal nog niet.
    // MGE: TODO: Footnote kan hier nog voor de body gezet worden want Response.Write(html) heeft nog niet plaatsgevonden!
    //      Echter bij (printMoreHeaders && flexGroupChanged) worden er meerdere headers geprint op verschillende plekken.
    //      Dit hoort eigenlijk niet zo.
    var htmlFoot = "";
    if (oRs.EOF)
      htmlFoot += makeTableFootnote(anyMultiActions, oRsFlexData);

    if (anyMultiActions && !inline)
    {
        appender = "\n<div id='multiactiondiv'><span id='multilabel'>" + safe.html(L("lcl_with_selected").format(0)) + "</span>";
        var multiActions = 0;

        // Geen inline acties, dus ook geen rowEnabler, dus is okformulti ook nog niet bepaald, dan doen we dat hier
        if (disableInlineActions && multiActionEnabler)
        {
            var validateAction = multiActionEnabler();
            for (var i=0; i < actions.length; i++)
            {
                if (!actions[i].okformulti &&                                                       // No double check
                    (actions[i].multi && !(actions[i].onlyMulti || actions[i].single === false)) && // No unnecessary check
                    actions[i].enabler && validateAction && validateAction[actions[i].enabler])     // Check
                {
                    actions[i].okformulti = true;
                }
            }
        }

        for (var i=0; i < actions.length; i++)
        {
            /////
            //
            //   Een bulk-actie wordt weergegeven indien:
            //
            //   - multi = true, EN:
            //
            //   - okformulti = true (oftewel, de enabler is succesvol)
            //   - OF: het is uitsluitend een multi actie, dan altijd weergeven. Oftewel: (onlyMulti || single === false)
            //
            /////

            if (actions[i].multi && (actions[i].okformulti || actions[i].onlyMulti || actions[i].single === false))
            {
                multiActions++;
                appender += "<span onClick='javascript:doMulti(\"" + this.ID + "\", \"" +
                                                                    actions[i].action + "\", \"" +
                                                                    safe.jsstring(actions[i].caption) + "\", " +
                                                                    actions[i].multiOnce + ");" +
                                                                   "FcltMgr.stopPropagation(event);' class='button footerbutton'>"+(actions[i].icon?I(actions[i].icon):"")+safe.html(actions[i].caption)+"</span>";
            }
        }
        if (multiActions == 0)
            appender += "<i style=\"padding-left: 9px\">" + L("lcl_no_multi_actions") + "</i>";
        appender += "</div>";
        var flexColumns = 0; /* Afgekeken van addFlexKenmerkFooter; bepaal het aantal columns dat nodig is voor de kenmerken */
        if ((isPrinting || showProperties) && this.printFlex) {
          flexColumns = 1;
        } else if (isExcel && this.printFlex) {
          for (var i=0; i < oRsFlexData.length; i++) {
            flexColumns++;
          }
        }
        htmlFoot += "<tr><td class='rsfooter' colspan=" + (columns.length + flexColumns + ((useHamburger && hasInlineActions) ? 1 : 0) + (!isPrinting && !isExcel && anyMultiActions && !inline? 1 : 0) + (inlineDetails? 1 : 0)) + ">" + appender + "</td></tr>";
    }

    if (htmlFoot != "")
      htmlFoot = "<tfoot>" + htmlFoot + "</tfoot>";

    html += htmlFoot;

    Response.Write(html);

    endTable();

    // 'Klik voor meer' onderaan de tabel
    if (!oRs.EOF || this.hasMore)
    {
      var a_element = "";
      if (Request.ServerVariables("REQUEST_METHOD") == "POST")
      {
        // Inline details met POST methode en meer dan max regels tonen werkt niet. Komt ook niet voor.
        a_element = "<a href='javascript:doOutput(0)'>";
      }
      else
      {
        a_element = "<a href='"
                      + (inline // Voor inline details moet niet gehele window vervangen worden omdat je dan ook de hoofd regels kwijt bent (dan alleen de <td> cell vervangen)
                        ? "javascript:loadInlineDetails(\""
                          + safe.jsstring(Request.ServerVariables("SCRIPT_NAME") + "?showall=1" + transitQS())
                          + "\");"
                        : safe.jsstring(Request.ServerVariables("SCRIPT_NAME") + "?showall=1" + transitQS()) + "' target='_self")
                      + "'>";
      }
      a_element += L("lcl_qp_maxrows1").format(S("qp_maxrows")) + "</a>";

      // Op regel klikken om de overige regels op te halen, onder de tabel
      html = "\n<div id='rsMaxHistoryE'><span id='rsMaxHistoryInfoE'>"
           + (cnt <= S("qp_maxrows") + nrOutOfFilter && S("qp_maxrows") != S("qp_maxrows2")
             ? a_element
             : L("lcl_qp_maxrows2").format(S("qp_maxrows2"))
             )
           + "</span></div>";

      Response.Write(html);
    }

    if ("auditcount" in this && rowNum >= this.auditcount)
    {
        shared.registeraction("audit", {info: L("fac_audit_listrequested").format(this.title, rowNum), daily: 0 });
    }

    // default summary text
    if (rowNum == 1)
        summaryInfo = L("lcl_nr_lines_resultone");
    else
        summaryInfo = L("lcl_nr_lines_result").format(rowNum);

    if (summaryShow)
    {
      var summaryInfo = ((!oRs.EOF || this.hasMore)? "<span class='summaryincomplete'>" + L("lcl_suminfo_incomplete") + "</span>&nbsp;" : "") + (summaryShow(rowNum) || "");
    }
    if (!this.noSetSummary)
    {
%>    <script>
        setSummary("<%=safe.jsstring(summaryInfo)%>");
        var do_sum_lines = false;
<%      if (this.rstableNr && this.rstableNr > 1)
        { %>
        for (var i = <%=this.rstableNr-1%>; i && !do_sum_lines; i--) {
          do_sum_lines = $("[id=rstable].rstable" + i).length;
        }
<%      } %>
        FcltMgr.setSubtabLines(<%=rowNum%>, { add: do_sum_lines, hasMore: <%=(!oRs.EOF || this.hasMore) ? "true" : "false"%> });
      </script>
<%  }

    // Eventueel doortellen tot het einde. Dat doen we maximaal 1 seconde! (zo'n 3200 records)
    var tmStart = (new Date).getTime();
    var teller = 0;
    while (!oRs.EOF && this.buildHistogram && ((new Date).getTime() - tmStart) < 1000)
    {
      teller ++;
      this.countHistogram(oRs);
      oRs.MoveNext();
    }
    if (!oRs.EOF && this.buildHistogram)
    { // Niet gered binnen een seconde
      __Log("Histogram niet binnen 1000ms klaar. Reset histogram");
      this.Counter = {};
    }
    else if (teller > 0)
    {
      __Log("Histogram afgerond ("+teller+" extra items) in " + ((new Date).getTime() - tmStart) + "ms");
    }
    oRs.close();

    if (endText)
    {
      var endTextInfo = (endText instanceof Function ? endText(oRs, this.processParams) : endText);
      if (endTextInfo != "")
      {
%>
        <div id="rsEndText">
          <span id="rsEndTextInfo"><%=endTextInfo%></span>
        </div>
  </div>
<%
      }
    }

    if (isPrinting)
      endPrint();

    if (this.buildHistogram)
        return this.Counter;
    else
        return rowNum; // simpel return aantal rows
  } // with(this)
}

function __rsStartTable()
{
  var html = "";
  with (this)
  {
    html = "<div id='rstable' class='rstable" + this.rstableNr + "'>";
    html += "<table cellspacing='0' cellpadding='0' ";

    if (getQParamInt("noqfilter", 0) == 1)
        html += " noqfilter='1'";

    if (isPrinting == true) {
      html += " isPrinting='TRUE' ";
    }

    if (ID != null)
      html += " ID='" + ID + "' ";
    if (tableClassName != null)
      html += " class='" + tableClassName + "' ";
    html += ">";

    rowNum = 0;
  }
  return html;
}

function __rsMakeTableHeader(oRs, oRsFlexData, anyMultiActions)
{
  var html = "";
  with(this)
  {
    html += "\n<thead><tr>";
    if (!isPrinting && !isExcel)
    {
        // ========= ACTIONS =========
        // == zijn er multi ACTIONS?
        if (anyMultiActions && !inline)
        {
            html += "<th class='multiselect sorttable_nosort'><input type=checkbox class='master' onclick='checkAll(\"" + ID + "\", this)'></th>";
        }
        if (useHamburger && hasInlineActions)
        {
            html += "<th class='sorttable_nosort'></th>";
        }
        if (inlineDetails)
        {
            html += "<th>"+I("fa-plus-square")+"</th>";
        }
    }
    // ========= COLUMNS/THEADER =========
    for (var i=0; i < columns.length; i++)
    {
        if (propertiesColumn == i)
          // Nog kolommen toevoegen voor de flexkenmerken
          html += addFlexKenmerkHeader(oRsFlexData, this);

        if ((isPlain && (columns[i].purpose & VIEW_ONLY)) ||
             ((!isPlain || showProperties) && (columns[i].purpose & PRINTING_ONLY)))
        {
          if ((isPrinting || showProperties || isExcel || !columns[i].combine) && !(isExcel && columns[i].hidden))
          {
              if (isExcel)
              {
                html += "<th>";
              }
              else
              {
                  if (columns[i].hidden)
                  {
                    html += "<th style='display:none'>";
                  }
                  else
                  {
                    html += "<th";
                    if (columns[i].caption == p_autosortby)
                    {
                      html += " id='autosortbyme'";
                    }
                    var sort_class = "";
                    if (columns[i].datatype == "currency" ||
                        columns[i].datatype == "float" ||
                        columns[i].datatype == "number")
                    {
                        html += " style='text-align:right;'";
                        if (safe.curr(1.5).indexOf(",") == -1)
                          sort_class += " sortable_numeric";
                        else
                          sort_class += " sorttable_numericcomma";
                    }
                    else if (columns[i].datatype == "varchar" ||
                             columns[i].datatype == "html")
                    {
                        sort_class += " sorttable_alpha";
                    }
                    if (columns[i].align)
                      html += " style='text-align:" + columns[i].align + "'";

                    var theClass = sort_class
                                 + (columns[i].thClass   ? " " + columns[i].thClass   : "")
                                 + (columns[i].classList ? " " + columns[i].classList : "");

                    if (theClass != null && theClass != "")
                      html += " class='" + theClass + "' ";
                    if (inlineDetails)
                      html += " preSorttable='hideAllInlineDetails(\""+ID+"\")'";
                    var ctitle = columns[i].title == null ? "" : (" title='" + safe.htmlattr(columns[i].title) + "'");
                    html += ctitle + ">"
                  }
              }
          }
          if (!(isExcel && columns[i].hidden))
          {
            var combine_caption = columns[i].caption.replace(/^\s+|\s+$/g,""); // trim spaces: functie trim() werkt niet server-side!
            if (columns[i].combine && combine_caption && !isPrinting)
              html += "<br>";
            html += columns[i].caption;
          }

          // Is de volgende kolom een combine. LET OP!!!: PRINTING_ONLY kolommen niet meerekenen (overslaan) in niet print uitvoer.
          var combine = false;
          for (var c = i + 1; c < columns.length; c++)
          {
            if (columns[c].purpose != PRINTING_ONLY)
            {
              combine = columns[c].combine;
              break;
            }
          }

          if ((!isPrinting && !isExcel && (i < columns.length - 1) && combine) || (isExcel && columns[i].hidden))
          {
              // html += "<br>";
            }
          else
              html += "</th>";
        }
    }

    if (propertiesColumn == null)
      // Nog header toevoegen voor de flexkenmerken.
      html += addFlexKenmerkHeader(oRsFlexData, this);

    html += "</tr></thead>";
  }
  return html;
}

var lastRowKey = -1; // voor suppress
function __rsMakeTableRow(oRs, oRsFlexData, cnt, anyMultiActions, noFlexResult)
{
  var html = "";
  // Pas op: dit komt voor elke regel terug, dus hou het kort!
  with(this)
  {
        if (beforeEachRow)
            beforeEachRow(oRs, this.processParams);

        var thisKey = -1;
        if (suppressKeyRepeat)
            thisKey = oRs(suppressKeyRepeat).Value;

        // HIER START EEN NIEUWE REGEL
        var appender = ("\n<tr");

        var OddRowClass = ""; // was:(cnt & 1)? "O " : "E "; maar dat kan inmiddels met css clientside beter

        if (suppressKeyRepeat)
          if (lastRowKey!=thisKey)
              OddRowClass += "firstRow ";
          else
              OddRowClass += "suppressRow ";

        if (!isExcel)
        {
            if (rowClass)
              appender += " class='" + OddRowClass + rowClass(oRs, this.processParams) + "' ";
            else
              appender += " class='" + OddRowClass + "' ";
        }

        if (keyColumn != null && !isPrinting && !isExcel)
        {
          appender += " ROWKEY='" + safe.htmlattr(this.myRs(keyColumn, oRs)) + "'";
        }
        if (!isPrinting && !isExcel && rowData != null)
          appender += " ROWDATA='" + safe.htmlattr(rowData(oRs, this.processParams)) + "'";

        if (!isPrinting && dblClick != null)
          appender += " onDblClick='" + safe.htmlattr(dblClick) + "'";

        if (!isPrinting && !isExcel && this.actions && this.actions.length)
        {
          if (!keyColumn)
            INTERNAL_ERROR_ACTION_ZONDER_KEYCOLUMN; // dat kan niet echt, we hebben een ROWKEY nodig

          if (rowActionEnabler)
            var validator = rowActionEnabler(oRs, this.processParams);

          var actionbits = "";
          var noActionBit = "";

          for (var i = 0; i < actions.length; i++)
          {
              if (actions[i].single === false || actions[i].onlyMulti) // triple '=' omdat undefined niet false mag zijn
              {
                  // die negeren we altijd actionbits += "0";
                  continue; // niet ook nog inline
              }
              if (actions[i].enabler && validator && !validator[actions[i].enabler])
              {
                  actionbits += "0";
                  noActionBit += "0";
                  continue; // niet enabled
              }
              actionbits += "1"; // Enabled
              noActionBit += actions[i].isDefault ? "1" : "0";
              actions[i].okformulti = true;
          }
          appender += " ACTIONBITS='" + safe.htmlattr(actionbits) + "'";
      }

      appender += (">");

        // Het <TR> start element is nu klaar

        // ========= COLUMNS =========
        if (!isPrinting && !isExcel && anyMultiActions && !inline) // Checkboxje erbij voor 'select all'
        {
            var isChecked = this.rowChecked && this.rowChecked(oRs, this.processParams);
            appender += "<td class='multiselect'><input type=checkbox class='multiselect'" + (isChecked?' checked=1':'') + "></td>";
        }

        if (useHamburger && hasInlineActions)
        {

            if (actionbits == noActionBit) // No inline actions
            {
                appender += "<td></td>";
            }
            else
            {
                appender += "<td class=\"hambCont\"><div class=\"hamburger\">" + I("fa-bars fa-lg") + "</div></td>";
            }
        }

        if (!isPrinting && !isExcel && inlineDetails)
        {
            var ttl = null;
            if (hasInlineDetails) ttl = hasInlineDetails(oRs, this.processParams);

            if (!hasInlineDetails || ttl)
            {
                var opdr_gewijzigd = (inlineDetailsChanged ? oRs("isGewijzigd").Value == 1 : false);
                if (typeof ttl == 'string')
                    ttl = " title ='" + safe.htmlattr(ttl) + "'"
                else
                    ttl = "";

                appender += "<td class='inlinedetails closed' onclick='return showInlineDetails(this, \"" + inlineDetails+"\")'" + ttl + ">"
                          + (opdr_gewijzigd ? I("fa-square-exclamation") : I("fa-plus-square")) + "</td>";
            }
            else
                appender += "<td>"+L("lcl_inlinecolumnnone")+"</td>";
        }

        // ========= De echte waarde =========
        var colHasVal;
        for (var i = 0; i < columns.length; i++)
        {
            if (propertiesColumn == i)
              // Nog kolommen toevoegen voor de flexkenmerken
              appender += addFlexKenmerkKolommen(oRsFlexData, this,
                                                 { lastRowKey: lastRowKey,
                                                   thisKey: thisKey
                                                 });

            if ((isPlain && (columns[i].purpose & VIEW_ONLY)) ||
                 ((!isPlain || showProperties) && (columns[i].purpose & PRINTING_ONLY)))
            {
              if (!columns[i].combine)
                colHasVal = false;
              try
              {
                  var val = (columns[i].content != null ? columns[i].content(oRs, this.processParams) : "");
              }
              catch (e)
              {
                __Log("resultset_table_v2.inc: Probleem met het ophalen van columns[" + i + "]: " + columns[i].caption);
                throw(e);
              }

              if (columns[i].image)
              {
                    var imgval = "<img src='" + columns[i].image + "'";
                    if (val) imgval += " alt='" + safe.htmlattr(val) + "'";
                    val = imgval + ">";
              }

              if (val == null) val = "&nbsp;";
              if (suppressKeyRepeat && lastRowKey == thisKey && !columns[i].keepKeyRepeat)
                val = "&nbsp;";
              // just in case?
              val = String(val);

              // Klik actie op de waarde.
              var urlkey = -1;
              // urlkey (columns[i].urlkey) is altijd een functie of -1 als er geen functie is gedefinieerd.
              var urlkey = columns[i].urlkey instanceof Function? columns[i].urlkey(oRs, this.processParams, this) : -1;
              var clickaction = urlkey == -1? "" : " class='clickaction' OnClick=' clickAction(" + urlkey + ")'";

              // Tooltip voor de kolom
              var tooltip = columns[i].tooltip == null ? null : this.myRs(columns[i].tooltip, oRs);
              if (!tooltip && columns[i].prettydate)
              {
                tooltip = toDateString(oRs(columns[i].orgContent).Value, isExcel, false);
              }
              tooltip = (tooltip == null? "" : " title='" + safe.htmlattr(tooltip) + "'");

              // Class voor de kolom
              var columnClass = columns[i].columnClass == null ?  (columns[i].tdClass instanceof Function ? columns[i].tdClass(oRs) : columns[i].tdClass) : this.myRs(columns[i].columnClass, oRs);

              if (i <= lastColKey)
              {
                if (lastVal[i] == null || lastVal[i] != val)
                  lastVal[i] = val;
                else if (val != "&nbsp;" && val !== "")
                  columnClass = (columnClass == null ? "dupe" : columnClass + " dupe");
              }

              columnClass = (columnClass == null ? "" : " class='" + columnClass + "'");

              // custom sort key voor de kolom
              var customSort = columns[i].customSort == null ? null : this.myRs(columns[i].customSort, oRs);
              if (!customSort && columns[i].prettydate)
              {
                customSort = toDateString(oRs(columns[i].orgContent).Value, isExcel, false);
              }
              customSort = (customSort == null? "" : " sorttable_customkey='" + customSort + "'"); // pikt sorttable.js vanzelf op

              // Uitlijning voor de kolom
              var align = "";
              var style = "";
              if (columns[i].align)
                    align = " align='" + columns[i].align + "'";
              else
              {
                 if (columns[i].datatype == "currency" ||
                     columns[i].datatype == "float" ||
                     columns[i].datatype == "number")
                     style += "text-align:right;";
              }

              // Niet zichtbare kolom
              style += (columns[i].hidden ? " display:none;" : "");

              // Prevent wrapping if required
              var nowrap = columns[i].nowrap ? " class='nowrap'" : "";

              // Opbouw cell
              style = (style ? " style='" + style + "'" : "");
              if ((isPrinting || showProperties || isExcel || !columns[i].combine) && !(isExcel && columns[i].hidden))
              {
                if (isExcel)
                  appender += "<td";
                else
                  appender += "<td" + align + style + columnClass + customSort + nowrap;
                if (isExcel && val != null && val.length > 0 && val.match(/[0-9]/)
                        && !inArray(columns[i].datatype, ["date", "currency", "datetime", "float", "number"]))
                {
                    appender += " style='mso-number-format:\"\@\"'";
                }
                appender += (columns[i].classList ? " CLASS='" + columns[i].classList + "'" : "") + ">";
              }
              else
              {  // Combine maar geen <br> voor lege val
                 if (val != "&nbsp;" && val !== "" && colHasVal)
                 {
                  appender += "<br>"
                 }
                else
                    appender = appender.replace(/\&nbsp\;$/,"") // "Vorige" &nbsp; er af strippen
              }

              if (!(isExcel && columns[i].hidden))
                if (tooltip || clickaction) // show tooltip on field, not on <td>
                  appender += "<span " + tooltip + clickaction + ">" + val + "</span>";
                else
                  appender += val;

              // Is de volgende kolom een combine. LET OP!!!: PRINTING_ONLY kolommen niet meerekenen (overslaan) in niet print uitvoer.
              var combine = false;
              for (var c = i + 1; c < columns.length; c++)
              {
                if (columns[c].purpose != PRINTING_ONLY)
                {
                  combine = columns[c].combine;
                  break;
                }
              }

              if ((!isPrinting  && !isExcel && (i < columns.length - 1) && combine) || (isExcel && columns[i].hidden))
                ;/* appender += "<br>"; */
              else
                appender += "</td>";

              // Hebben we al een echt waarde gehad?
              colHasVal = colHasVal || (val != "&nbsp;");

              // Als totaal van de kolom weergegeven moet worden dan waarden optellen.
              // amount is altijd een functie
              if (columns[i].total || columns[i].average)
              {
                var amount = (columns[i].amount != null ? columns[i].amount(oRs, this.processParams) : null)
                if (amount != null && !isNaN(amount))
                    columns[i].totalsum += amount;
              }

              if (columns[i].average) // only on the last iteration is this needed
              {
                if (!columns[i].totalRows)
                    columns[i].totalRows = 0;
                columns[i].totalRows++;
              }
            }
        }

        html += appender;

        if (propertiesColumn == null)
          // Nog kolommen toevoegen voor de flexkenmerken
          html += addFlexKenmerkKolommen(oRsFlexData, this,
                                         { lastRowKey: lastRowKey,
                                           thisKey: thisKey
                                         });

        html += "</tr>";
    // rowNum = cnt + 1;
  }

  lastRowKey = thisKey;
  return html;
}

function __rsMakeTableFootnote(anyMultiActions, oRsFlexData)
{
  var html = "";
  with(this)
  {
    var total = false;
    var average = false;
    // Kijk eerst of er een totaalregel toegevoegd moet worden
    for (var i=0; i < columns.length; i++)
    {
        if (columns[i].total || columns[i].generaltotal) total = true;
        if (columns[i].average || columns[i].generalaverage) average = true;
    }
    if ((total || average) && !isExcel)
    {
      html += "<tr>";
      if (!isPrinting  && anyMultiActions && !inline) // Kolom i.v.m. checkboxje erbij voor 'select all'
        html += "<td></td>";
      if (hasInlineDetails && !isPrinting && !isExcel && inlineDetails)
        html += "<td></td>"; // Kolom i.v.m. inlinedetails
      if (useHamburger && hasInlineActions)
          html += "<td></td>"; // Ook nog kolom i.v.m. hamburger die er sowieso mogelijk alleen is als er acties zijn

      var html_f = "";
      var once = true; // Indien totalColumnText is gedfinieerd, eenmalig de text van totalColumnText tonen in de kolom voor de eerste totaal kolom.
      for (var i = 0; i < columns.length; i++)
      {
          if ((isPlain && (columns[i].purpose & VIEW_ONLY)) ||
               ((!isPlain || showProperties) && (columns[i].purpose & PRINTING_ONLY)))
          {
            if (!columns[i].combine || isPrinting || showProperties || isExcel) // Bij isPrinting, isExcel of showProperties niet combineren.
            {
              // Nog kolommen toevoegen voor flexkenmerken
              if (propertiesColumn == i)
                html_f += addFlexKenmerkFooter(oRsFlexData, this);

              html_f += "<td";
              var sort_class = columns[i].thClass ? " " + columns[i].thClass : "";
              if (sort_class != null)
                html_f += " CLASS='" + sort_class + "' ";
              // Uitlijning voor de kolom
              var style = "";
              var align = "";
              if (totalColumnText && once && (i + 1) < columns.length && columns[i + 1].total)
                style = "text-align: right;";
              else if (columns[i].align)
                  align = " align='" + columns[i].align + "'";
              else
              {
                 if (columns[i].datatype == "currency" ||
                     columns[i].datatype == "float" ||
                     columns[i].datatype == "number")
                     style = "text-align: right;";
              }
              if (columns[i].hidden) style += "display: none;";
              html_f += align + (style ? " style='" + style + "'" : "") + ">";
            }
            if (totalColumnText && once && (i + 1) < columns.length && columns[i + 1].total)
            {
              once = false;
              html_f += totalColumnText();
            }

            // Weergeven als integer of als getal met twee decimalen.
            if (columns[i].total)
            {
              if (totalShow)
              {
                html_f += totalShow(columns[i], rowNum);
              }
              else
              {
                html_f += ResultsetTable.formatValue(columns[i].totalsum, columns[i]);
              }
            }
            else if (columns[i].generaltotal)
            {
              html_f += ResultsetTable.formatValue(columns[i].generaltotalsum, columns[i]);
            }
            else if (columns[i].generalaverage)
            {
              html_f += ResultsetTable.formatValue(columns[i].generaltotalavg, {datatype: "float", decimals: 2});
            }
            else if (columns[i].average && columns[i].totalRows > 1) // geen gemiddelde tonen van 1 resultaat
                html_f += ResultsetTable.formatValue((columns[i].totalsum / columns[i].totalRows), {datatype: "float", decimals: 2});
            else
              html_f += "";
            if ((i < columns.length - 1) && !columns[i + 1].combine)
              html_f += "</td>";
          }
      }

      // Nog extra kolom headers voor de flexkenmerken
      // Flexkenmerken tonen bij isPrinting, isExcel of showProperties
      if (this.printFlex && this.propertiesColumn == null)
      {
        html_f += "<td></td>";
      }

      html_f = balanceCloseTag("td", html_f);

      html += html_f + "</tr>";
    }
  }
  return html;
}

function balanceCloseTag(tagname, chtml)
{   // Als het aantal tag-open groter is dan het aantal tag-close dan een tag-close toevoegen.
    var tag_open  = chtml.toLowerCase().split("<"+tagname.toLowerCase());
    var tag_close = chtml.toLowerCase().split("/"+tagname.toLowerCase()+">");
    if (tag_open.length > tag_close.length)
    {
        chtml += "</"+tagname+">";
    }
    return chtml;
}

function __rsEndTable()
{
  Response.Write("\n</table>");
  Response.Write("\n</div>  <!-- for DIV id='rstable' -->");
}

function __rsAddColumn(column)
{
    this.columns.push(column);
}

function __rsStartPrint()
{
  if (!isPrinting) return;

  if (getQParamInt("formail", 0) == 1)
  {
    var logo_fileName = "cid:" + S("fac_logo_file");
  }
  else
  {
    var logo_fileName = custpath + "/" + S("fac_logo_file");
  }
  Response.Write('\n<div id="printheader">');
  Response.Write('\n<table id="head"><tr>');
  Response.Write('\n<td align="left">');
  Response.Write('<img alt="' + L("lcl_photos") + '" src="' + logo_fileName+ '"></TD>');
  Response.Write('\n<td align="right">');
  Response.Write('\n<table id="right">');
  Response.Write('\n <tr><td colspan="2" id="titel">' + (this.title||"") + '</td></tr>');
  Response.Write('\n <tr><td>' + L("lcl_name") + ' :</td><td>' + safe.html(user.naam()) + ' </td></tr>');
  Response.Write('\n <tr><td>' + L("lcl_date") + ' :</td><td>' + toDateString(new Date()) + ' </td></tr>');
  Response.Write('\n</table>');
  Response.Write('\n</td>');
  Response.Write('\n</tr></table>');
  Response.Write('\n</div>');
}

function __rsEndPrint()
{
  if (isPrinting)
  {
    var prtDate = toDateTimeString(new Date());
    Response.Write('\n<div id="printfooter">');
    Response.Write('\n<table id="foot"><tr>');
    Response.Write( '\n<td>' + (this.title||"") + '</td>');  // deze this.title lijkt intussen verloren gegaan
    Response.Write( '\n<td align="right">' + toDateTimeString(new Date()) + '</td>');
    Response.Write( '\n</tr></table>');
    Response.Write('\n</div>');
    if (p_autosortby)
    {
        var sortfn = autosortbyrev?"sorttable.doSortRev":"sorttable.doSort";
        Response.Write('<script>$(function () { if ($("#autosortbyme").length) { '+sortfn+'.apply($("#autosortbyme")[0]) };window.print() });</script>');
    }
    else
        Response.Write('<script>window.print();</script>'); // Zonder jQuery
  }
}


///////////
// Column
///////////
var PRINTING_ONLY = 1;
var VIEW_ONLY = 2;
var PRINT_AND_VIEW = PRINTING_ONLY + VIEW_ONLY;

function Column(params)
{
  if (!params) params = {};
  this.caption = params.caption;
  this.title = params.title;
  this.filter = params.filter;
  this.content = params.content;
  this.image = params.image;
  this.action = params.action;
  this.tooltip = params.tooltip;
  this.thClass = params.thClass;
  this.columnClass = params.columnClass;
  this.classList = params.classList;
  this.customSort = params.customSort;
  this.align = params.align;
  this.purpose = params.purpose ? params.purpose : PRINT_AND_VIEW;
  this.hidden = params.hidden;
  this.showDefault = "showDefault" in params ? params.showDefault : true;
  this.datatype = params.datatype;
  this.decimals = params.decimals;
  this.trimZeros = params.trimZeros;
  this.total = params.total;
  this.average = params.average;
  this.amount = null; // Voor INTERN gebruik: is de integerwaarde van content, indien de kolomwaarden opgeteld moeten worden.
  this.urlkey = null; // Voor INTERN gebruik: als er een urlkey is meegegeven.
  this.totalsum = 0;
  this.average = 0.00;
  this.generaltotalsum = 0;
  this.generaltotalavg = 0.00;
  this.name = params.name;
  this.nowrap = params.nowrap;
  this.orgContent = params.content;
  this.nomidnight = params.nomidnight;
  this.filename = params.filename; // voor csv/xlsx

  // Neem alle meegegeven parameters mee.
  for (param in params)
  {
    if (param == "content" && params.content)
    {
       if (params.content instanceof Function)
       {
           this.content = params.content;
           this.amount  = params.fnAmount || params.content;
           this.urlkey  = params.fnUrlkey || -1;
       }
       else
       {
           this.content = function (oRs) { return ResultsetTable.formatValue(oRs.Fields(params.content).Value, this) } ;
           this.amount  = function (oRs) { return oRs.Fields(params.content).Value };
           this.urlkey  = function (oRs, processParams, params) { return params.dataset? ResultsetTable.getUrlKey(oRs.Fields("scf_urlkey").Value, params.content) : -1; };
       }
    }
    else
      this[param] = params[param];
  }
  if (!isPlain && this.prettydate)
     this.prettydate = false;
}

// Levert een html-safe waarde op
ResultsetTable.formatValue = function (val, params) //datatype, decimals)
{
  if (val == null)
  {
    if (isXMLXSL || isCSV || isCSV2XLSX)
      return "";
    else
      return "&nbsp;";
  }
  // Vanuit API2 model
  if (typeof val == "object" && "name" in val)
  {
    val = val["name"];
    params.datatype = null; // default string
  }

  // Eerst de geforceerde datatypes
  switch (params.datatype)
  {
    case "date"    : return toDateString(val, isExcel||isCSV || isCSV2XLSX, params.prettydate); // isExcel-->noDay
    case "datetime": return toDateTimeString(val, params.withSeconds, isExcel||isCSV || isCSV2XLSX, params.prettydate, params.nomidnight);
    case "time"    : { val = toTimeString(val, params.withSeconds);
                       if (params.datatype == "time" && params.nomidnight && val == "00:00")
                         val = "";
                       return val;
                     }
    case "processingtime": { if (val["duration"] == null)
                               val = "";
                             else
                               val = val["duration"] + " " + (val["unit"]=="U"?L("lcl_mld_hours"):L("lcl_mld_days"));
                             return val;
                           }
    case "currency": return safe.curr(val);
    case "float"   : return safe.displayfloat(val, params.decimals, params.trimZeros);
    case "number"  : return val;
    case "html"    : return val;
  }

  // Autotypering "datum"
  if (typeof val == "date" || val instanceof Date)
    return toDateString(val, isExcel, params.prettydate);

  if (!val)
    return "";

  if (typeof val != "string")
  { // Je raakt bijvoorbeeld de rechtse uitlijning van number-velden kwijt
    __Log("Ongespecificeerd datatype van veld '{0}'{1} is {2}, fallback naar string".format(params.caption, typeof params.orgContent=='string'?' (' + params.orgContent + ')':"", typeof val), "#FF0");
    val = String(val);
  }
  if (params.secret)
    return "<span title='{0}'>".format(safe.htmlattr(val)) + I("fa-eye fa-lg") + "</span>";

  val = shared.stripbbcodes(val);

  if (isXMLXSL || isCSV || isCSV2XLSX)
    return val; // DOM lost encoding wel op of we doen safe.csv.

  // default datatype is "string"
  val = String(val).replace(/[\n\r]+/g,"\n"); // Compact, geen lege regels tonen

  var safeval = __rsTruncMore(val, {isPrinting: isPrinting, isExcel: isExcel}); // Zorgt voor het afkappen van de tekst en het toevoegen van ... meer.

  return safeval;
}

ResultsetTable.getUrlKey = function (urlval, contenttext)
{
  var urlkey = urlval? urlval[contenttext] || -1 : -1;
  return urlkey;
}

ResultsetTable.prototype.truncMore = function __rsTruncMore(pval, pparams)
{
  if (pparams.isExcel)
  { // Hier geen safe.html omdat we voor Excel de <br> naar \n vertaling niet kunnen hebben.
    // Met <br> krijg je een nieuwe rij in excel.
    return Server.HTMLEncode(pval);
  }

  if (pparams.isPrinting || S("rs_maxchar") < 0 || pval.length < S("rs_maxchar") * 1.20) // 'Meer' moet minstens 20% extra tekst opleveren.
    return safe.html(pval); // simpelweg hele tekst

  // Oke, op scherm afkappen na S("rs_maxchar") karakters.
  // Probeer op de eerste spatie maar die moet wel binnen 20% karakters komen
  var fullval = pval;
  var spatie = String(pval).substr(S("rs_maxchar")).indexOf(" ");
  if (spatie < 0 || spatie > S("rs_maxchar")/5)
      spatie = 0;
  pval = fullval.substr(0, S("rs_maxchar") + spatie + 1);
  // Pas op: pval is al "html-safe" terwijl we dat voor afkappen niet kunnen hebben?
  // In ieder geval geen safe.htmlattr.
  var safeval = safe.html(pval)
              + "<span class='rsTruncmore' " // Dubbel encoderen hieronder, anders html-injections was achter de Meer... mogelijk
              + " resttxt='" + safe.htmlattr(safe.html(fullval.substr(S("rs_maxchar") + spatie + 1))) + "'"
              + " title='" + L("lcl_rs_truncmore") + "'"
              + " onclick='return expandTruncate(event, this)'"
              + ">" + L("lcl_rs_truncated") + "</span>";
  return safeval;
}

function ResultsetTable.prototype.myRs(fn, oRs)
{
  if (fn instanceof Function)
  {
    return fn(oRs, this.processParams);
  }
  else
  {
    return oRs.Fields(fn).Value;
  }
}
%>
