Login   Register  
PHP Classes
elePHPant
Icontem

File: as_reportool.php

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Alexander Selifonov  >  Report generating and drawing  >  as_reportool.php  >  Download  
File: as_reportool.php
Role: Class source
Content type: text/plain
Description: main class module
Class: Report generating and drawing
Generate reports from database query results
Author: By
Last change: new features added and documented
Date: 11 months ago
Size: 40,340 bytes
 

Contents

Class file image Download
<?php
/**
* @package as_reportool - class for Html Report composing & printing
* @name as_reportool.php
* @author Alexander Selifonov <as-works@narod.ru>
* @link http://www.selifan.ru
* @license http://www.gnu.org/copyleft/gpl.html
* Updated 2013-09-16
* @version 1.23.023
**/
define('ASREPT_ERR_WRONGXMLFILE', -101);
define('ASREPT_ERR_WRONGFORMAT', -102);
define('ASREPT_ERR_WRONGPHP', -201);
/**
* @desc CReportField - one reporting field definition
*/
$_lbpath = defined('LIBPATH')? LIBPATH: '';
@require_once($_lbpath.'as_dbutils.php'); # db access wrapper class
/**
* report parameter definition class
*/
class CReportParam {
  var $id;
  var $inputtype;
  var $type;
  var $prompt;
  var $fillfunc;
  var $mandatory;
  var $expression = '';
  private $linkedCells = array();
  private $groupLinks  = array();
  function CReportParam($id,$itype='text', $type='char', $prompt='',$ffunc='',$expr='',$mand=0) {
    $this->id=$id;
    $this->inputtype=$itype;
    $this->type=$type;
    $this->prompt = empty($prompt) ? $id : $prompt;
    $this->fillfunc=$ffunc;
    $this->expression = $expr;
    $this->mandatory = $mand;
  }
}

class CReportField {
  var $fieldno;
  var $title; # title for this field
  var $formid = '';
  var $summable;
  var $fconv; # '' or function name to make presentation string from value
  var $format; # r=right, c=center, money= "money fmt",...
  var $calcfunc = false; # any field can be "calculated" - here is the user callback function name to compute value
  function CReportField($title='',$summable=0, $fconv='', $format='', $calcfunc=false) {
    $this->title=$title;
    $this->summable=$summable;
    $this->fconv=$fconv;
    $this->format=$format;
    $this->calcfunc=$calcfunc;
  }
}
class CReporTool { // main class def.

  const ASREPT_ERR_WRONGXMLFILE = -101;
  const ASREPT_ERR_WRONGFORMAT  = -102;
  const ASREPT_ERR_WRONGPHP     = -201;
  const CRLF = "\r\n";
  var $_title = '';
  var $_query = array(); # main query, that gets all info for report
  var $_srcfile = '';
  var $_headers = ''; # header rows for the top header
  var $_rfields = array(); # fields to draw or calculate
  var $_grpfields = array();
  var $_sumfields = array();
  var $_totals = array(); # internal use - accumulated totals
  var $_summary = array();
  private $_totdf = false; # if TRUE, plain tab-delimited body will be created (for importing into spreadsheets like MS Excel
  private $_xlsFmt = false; # if TRUE, decimal point will convert to ","
  private $extcss = array();
  private $_groupedHeadings = array(); # describes grouping of some heading columns
  private $_enumGroups = false; # print group's Number in the group headers (since: 1.23
  var $_multitotals = '';
  private $_grp_curval = array();
  private $_grpNo = array();
  var $_supress_eg = true; # supress printing headers and subtotals for zeroed grouping field values
  var $_debug = 0; # set to positive integer N to break reporting after N-th line
  var $_summarytitle = ''; # if not empty, summary totals line will be printed at report's bottom
  var $_summaryQuery = ''; # Query to calculate total summary, instead of simple summing row data
  var $_fontstyles = '';
  var $_rowcount = 0;
  var $errorcode = 0;
  var $_errormessage = 0;
  var $_delim_tho = ','; # delimiters for number_format
  var $_delim_dec = '.';
  var $_outcharset = '';
  var $_incharset = '';
  var $_nodefhead = 0;
  var $_suppresscss = false;
  var $_muids = array(); # in multi-totals case here will be saved all total "key" values
  var $_rownumbers = false; # print row  at first cell
  var $bg_totals = '#fff';
  var $bg_headings = '#eee';
  var $bg_newgroup = '#eaeaea';
  var $bordercolor = '#111';
  static $_backenduri = '';
  var $_rowbg_callback = ''; # User callback function to dynamically create data row background color
  var $_params = array(); # CReportParam object array
  private $_tofilelink = array(); # 'title'-> linktitle, 'url' => address to get "exported" text version of report
  private $_outbuf = '';
  private $_outname = '';
  private $rdata = array(); # raw data to draw in report (user can pass ready-to-print assoc.array by setReportData())
  /**
  * constructor
  *
  * @param string $filename passed XML file name to load report definition from
  * @return CReporTool
  */
  function CReporTool($par=false, $cset=null) {
      self::$_backenduri = $_SERVER['PHP_SELF'];
      if(is_array($par)) {
          if(isset($par['filename']))    $this->_srcfile    = trim($par['filename']);
          if(isset($par['outcharset']))  $this->_outcharset = trim($par['outcharset']);
          if(isset($par['backend']))     self::$_backenduri = trim($par['backend']);
          if(isset($par['bg_totals']))   $this->bg_totals   = trim($par['bg_totals']);
          if(isset($par['bg_headings'])) $this->bg_headings = trim($par['bg_headings']);
          if(isset($par['bg_newgroup'])) $this->bg_newgroup = trim($par['bg_newgroup']);
          if(isset($par['bordercolor'])) $this->bordercolor = trim($par['bordercolor']);
          if(isset($par['func_rowbg'])) $this->_rowbg_callback = trim($par['func_rowbg']);
          if(isset($par['tofilelink'])) $this->_tofilelink = $par['tofilelink'];
          if(isset($par['decimalPoint'])) $this->_delim_dec = (string)$par['decimalPoint'];
          if(!empty($par['xlsFmt'])) $this->_xlsFmt = true;
          if(!empty($par['totdf'])) $this->_totdf = true;
          if(!empty($par['groupnumbers'])) $this->_enumGroups = true;
          elseif(!empty($_GET['_tofile']) || !empty($_POST['_tofile'])) $this->_totdf = true;
          $this->_outname = (!empty($par['outname']))? trim($par['outname']) : 'created-report.txt';
          $this->_rownumbers = isset($par['rownumbers']) ? !empty($par['rownumbers']) : false;
      }
      elseif(is_scalar($par)) {
          $this->_srcfile = $par;
          $this->_outcharset = $cset;
      }

      if(is_string($this->_srcfile) && file_exists($this->_srcfile) && function_exists('simplexml_load_file')) {
          $this->LoadFromXml($this->_srcfile);
      }
  }
  private function _InGroupHead($grp, $fid) {
      $fcount =1;
  }
  function SetBackendURI($parm) { self::$_backenduri = $parm; }

  /**
  * Adds field list that must be grouped in the heading with desired "group" title
  * @since 1.20
  * @param mixed $groupTitle
  * @param mixed $fieldlist
  */
  function AddHeadingGroup($groupTitle, $field, $fcount=2) {
      if($fcount>1) {
          $this->_groupedHeadings[] = array($groupTitle, $field, intval($fcount));
      }
  }
  function LoadFromXml($filename, $outcharset=null) {
    # parse xml file and load all report parameters
    ini_set('zend.ze1_compatibility_mode', 0);
    if(!function_exists('simplexml_load_file')) {
        $this->_errorcode = self::ASREPT_ERR_WRONGPHP;
        return false;
    }
    $xml = @simplexml_load_file($filename);
    $this->_outcharset = ($outcharset!==null) ? $outcharset : 'UTF-8';
    if(!$xml) {
#      echo '<pre>'.htmlspecialchars(file_get_contents($filename)).'</pre>';
      $this->_errorcode = self::ASREPT_ERR_WRONGXMLFILE;
      $this->_errormessage = "$filename has wrong XML format or non XML at all!";
      echo "Error {$this->_errorcode} - {$this->_errormessage}<br />";
      return false;
    }
    $this->_errorcode=0;  $this->_errormessage='';
    $this->_query = array();
    $goodfmt = false;
    if(isset($xml->headings)) {
      $header = $this->DecodeCharValue($xml->headings);
      $this->SetHeadings($header);
    }
    foreach($xml->children() as $cid=>$obj) {
      switch($cid) {
        case 'title':
          $this->_title = $this->DecodeCharValue($obj);
          break;

        case 'parameter':
          $id = isset($obj['id'])? trim($obj['id']) : '';
          $itype = isset($obj['inputtype'])? trim($obj['inputtype']) : 'text';
          $type = isset($obj['type'])? trim($obj['type']) : '';
          $prompt = isset($obj['prompt'])? $this->DecodeCharValue($obj['prompt']) : $id;
          $ffunc = isset($obj['fillfunc'])? trim($obj['fillfunc']) : '';
          $expression = "$obj"; # tag value is "expression" to put into SQL, "%value%" will be replaced with user entered value
          if($expression=='') $expression="$id=%value%";
          $mand = isset($obj['mandatory'])? trim($obj['mandatory']) : 0;
          if(!empty($id)) $this->_params[] = new CReportParam($id,$itype,$type,$prompt,$ffunc,$expression,$mand);
          break;

        case 'paramform':
          $id = isset($obj['id'])? trim($obj['id']) : '';
          $this->formid = $id;
          break;
        case 'query':
          $this->AddQuery($obj);
          break;
        case 'grpfield':
          $fldid = isset($obj['name'])?  trim($obj['name']) : '';
          $fconv = isset($obj['fconv'])? trim($obj['fconv']) : '';
          $title = isset($obj['title'])? $this->DecodeCharValue($obj['title']) : '';
          $ttitle = isset($obj['totaltitle'])? $this->DecodeCharValue($obj['totaltitle']) : '';
          if(!empty($fldid)) { $this->AddGroupingField($fldid,$fconv,$title,$ttitle); }
          break;

        case 'field':
          $fldid = isset($obj['name'])?  trim($obj['name']) : '';
          $title = isset($obj['title'])? $this->DecodeCharValue($obj['title']) : '';
          $summa = isset($obj['summable'])? trim($obj['summable']) : '';
          $fconv = isset($obj['fconv'])? trim($obj['fconv']) : '';
          $fmt = isset($obj['format'])? trim($obj['format']) : '';
          if(!empty($fldid)) {
            $this->AddField($fldid,$title,$summa,$fconv, $fmt);
            $goodfmt = true;
          }
          break;

        case 'nodefaultheadings':
          $this->_nodefhead = isset($obj['value'])? trim($obj['value']) : 0;
          break;

        case 'headinggroup': # <headinggroup title="Group title" startfield="fieldname" fcount="4" />
          $hd_field = isset($obj['startfield'])?  trim((string)$obj['startfield']) : '';
          $hd_fcount = isset($obj['fcount'])?  trim((string)$obj['fcount']) : 0;
          $hd_title = isset($obj['title'])?  trim((string)$obj['title']) : "Group:$hd_field";
          if($hd_field!='' && $hd_fcount>1) $this->AddHeadingGroup($hd_title,$hd_field,$hd_fcount);
          break;

        case 'fontstyles':
          $this->_fontstyles = (!empty($obj['value']))? $obj['value'] : '';
          break;

        case 'delimiters':
          $dec = (!empty($obj['decimal']))? $obj['decimal'] : '';
          $tho = (!empty($obj['thousand']))? $obj['thousand'] : '';
          if($dec) $this->_delim_dec = $dec;
          if($tho) $this->_delim_tho = $tho;
          break;
        case 'multitotals':
          $this->_multitotals = isset($obj['byfield'])? trim((string)$obj['byfield']) : '';
          break;
        case 'summary':
          $title = (!empty($obj['title']))? $this->DecodeCharValue((string)$obj['title']) : 'summary';
          $this->SetSummary($title);
          break;
      }
    }

    if(!$goodfmt) {
      $this->_errorcode = self::ASREPT_ERR_WRONGFORMAT;
      $this->_errormessage = "$filename is not AS_REPORTOOL or empty XML file !";
    }
    unset($xml);
    return true;
  }
  function DecodeCharValue($strg) {
    $ret = $strg;
    if($this->_outcharset!='' && $this->_outcharset!='UTF-8'  && function_exists('mb_convert_encoding'))
        $ret = @mb_convert_encoding($ret,$this->_outcharset,'UTF-8');
    $ret = str_replace(array('&gt;','&lt;'), array('>','<'),$ret);
    return $ret;
  }
  function SetCharSet($par) {
    $this->_outcharset=strtoupper($par);
    if(count($this->_rfields)>0 && $this->_outcharset!='' && $this->_incharset!='' && $this->_outcharset!=$this->_incharset)
    {
      @mb_convert_variables($this->_outcharset,'UTF-8',$this->_rfields, $this->_grpfields);
      $this->_summarytitle = mb_convert_encoding($this->_summarytitle,$this->_outcharset,'UTF-8');
    }
  }
  /**
  * register one field to draw in report page
  *
  * @param mixed $fldid field name in the query
  * @param mixed $fldtitle title in the headers for the field
  * @param mixed $summable if not 0, make sub-totals for this field
  * @param mixed $fconv function name for converting field value to something readable, if needed (name for id etc.)
  * @param mixed $calcfunc user callback function to calculate field value
  */
  function AddField($fldid,$fldtitle='',$summable=0,$fconv='', $format='', $calcfunc=false) {
    # if(empty($fldtitle)) $fldtitle=$fldid;
    $this->_rfields[$fldid] = new CReportField($fldtitle,$summable,$fconv,$format, $calcfunc);
    if($summable) $this->_sumfields[] = $fldid;
  }
  /**
  * Passing user data (instead of performing SQL queries)
  *
  * @param mixed $userdata multi-row array, each row should be an assoc.array with keys according to printed field definitions
  */
  function setReportData($userdata) {
      $this->rdata = is_array($userdata) ? $userdata : array();
  }
  function SetHeaders($par,$no_defaultheader=0) { $this->_headers = $par; $this->_nodefhead=$no_defaultheader; }
  function SetHeadings($par,$no_defaultheader=0) { $this->_headers = $par; $this->_nodefhead=$no_defaultheader; }
  function SetQuery($sqlquery) { $this->_query=array();  $this->_query[]=$sqlquery; }
  function AddQuery($sqlquery) { $this->_query[]=$sqlquery; }
  function SetSummary($par='Totals:', $sumQry=false) {
      $this->_summarytitle = $par;
      if(!empty($sumQry)) $this->_summaryQuery = $sumQry;
  }
  function SetFontStyles($par='') { $this->_fontstyles = $par; }
  function AddGroupingField($fldid,$fconv='',$title='',$totaltitle='') {
    $this->_grpfields[$fldid] = array('fconv'=>$fconv,'title'=>$title,'ttitle'=>$totaltitle);
  }

  /**
  * sets decimal and thousand delimiters for number_format()
  *
  * @param mixed $dec
  * @param mixed $tho
  */
  function SetNumberDelimiters($dec,$tho='') {
    $this->_delim_dec = $dec;
    $this->_delim_tho = $tho;
  }

  function SetMultiTotals($fieldname) { $this->_multitotals = $fieldname; }

  /**
  * Describes "linked" cell, that will become a href
  *
  * @param mixed $fieldid - field id
  * @param mixed $options - associative array with options: <ul>
  * <li>uri - URI for the href or '@func_name' to evaluate URI dinamically</li>
  * <li>idparts - comma delimited field list that should be placed in URIinstead of {1},{2} and so on</li>
  * <li>'target' =>'_blank' to open sub-report in a new window</li>
  * <li>'title' =>'title for the link'
  * </ul>
  * @since 1.12
  */
  function SetLinkedCell($fieldid, $options) {
      if(isset($options['uri'])) $this->linkedCells[$fieldid] = $options;
  }
  /**
  * Defines rule for ceratin "<a href" link for "group" headers, to allow "nested" sub-reports
  *
  * @param mixed $options must be an assoc/array with elements: 'href'=> URL with "{1},{2}" to be substituted by current value,
  * 'idparts' for a list of values to substitute 'title'=>optional title string
  */
  function SetGroupLink($fieldid, $options) {
      if(is_array($options)) $this->groupLinks[$fieldid] = $options;
  }

  # adds custom css file to include into report HTML headers
  function AddCssFile($cssuri) {
      $this->extcss[] = $cssuri;
  }

  /**
  * Turns off output of builtin css block
  *
  * @param mixed $par if non-empty value, css won't be printed
  */
  function SuppressCss($par=true) { $this->_suppresscss = $par; }

  function getFieldOffset($fldname) {
      $ret = array_search($fldname, array_keys($this->_rfields));
      return $ret;
  }
  /**
  * Rendering report headings, including possible "grouping" of some fields headings
  * @since 1.20
  */
  function DrawReportHeadings() {
      if(!count($this->_groupedHeadings) || ($this->_totdf)) {
          $thead = $this->_totdf ? '' : '<tr class="rep_heading">';
          if($this->_rownumbers) $thead .= $this->_totdf ? "\t" : "<td class='rep_ltrb cnt'></td>";
          foreach($this->_rfields as $fid=>$fld) {
            if($fld->title!='') $thead .= $this->_totdf ? (strip_tags($fld->title)."\t") : "<td class=\"rep_ltrb\">{$fld->title}</td>";
          }
          $thead .= $this->_totdf ? self::CRLF : '</tr>';
      }
      else { # There are groupings in header columns, so we draw a "complex" headings
          $hdrs = array();
          foreach($this->_rfields as $fid=>$fld) {
              $hdrs[$fid] = 0; # how many times field exist in griouping
          }
          $hRows = array();
          $hRows[] = array();

          $fids = array_keys($this->_rfields);
          foreach($this->_groupedHeadings as $grh) {
              $fieldId = $grh[1];
              $fOffset = $this->getFieldOffset($grh[1]);
              $cnt = $grh[2];
#              $hRows[] = array();
              $thisrowNo = 0;
              for($kk=$fOffset; $kk<$fOffset+$cnt;$kk++) {
                  $thisrowNo = max($thisrowNo,$hdrs[$fids[$kk]]);
                  $hdrs[$fids[$kk]]+=1;
              }
              if(!isset($hRows[$thisrowNo])) $hRows[$thisrowNo] = array();
              $hRows[$thisrowNo][$fOffset] = array('text'=>$grh[0],'colspan'=>$cnt,'rowspan'=>1);
          }

          $maxrowspan = count($hRows); # Fields that not in any grouped heaqder, will have this rowspan
          $rownumspan = $maxrowspan+1;
          $thead = '';
          for($iRow=0; $iRow<=$maxrowspan; $iRow++) {
              $rowcontent = ($iRow==0 && $this->_rownumbers) ? "<td class='rep_ltrb cnt' rowspan='$rownumspan'></td>": '';
              for($ifld=0; $ifld<count($this->_rfields); $ifld++) {
                if(isset($hRows[$iRow][$ifld])) {
                    $rspan = $hRows[$iRow][$ifld]['rowspan']>1 ? " rowspan='".$hRows[$iRow][$ifld]['rowspan']."'" : '';
                    $cspan = $hRows[$iRow][$ifld]['colspan']>1 ? " colspan='".$hRows[$iRow][$ifld]['colspan']."'" : '';
                    $rowcontent .= "<td class='rep_ltrb'{$rspan}{$cspan}>".$hRows[$iRow][$ifld]['text'].'</td>';
                }
                else { # normal field title
                    $fid = $fids[$ifld];
                    if($iRow == $hdrs[$fid]) {
                        $rowspan = $maxrowspan - $iRow+1;
                        $rspan = $rowspan>1 ? " rowspan='$rowspan'" : '';
                        $rowcontent .= "<td class='rep_ltrb'{$rspan}>".$this->_rfields[$fid]->title.'</td>';
                    }
                }
              }
              if($rowcontent) $thead .= '<tr class="rep_heading">' .$rowcontent. '</tr>';
          }
      }

      $this->_outbuf .= $thead;
  }

  /**
  * runs SQL query and echoes resulting report
  *
  */
  function DrawReport($title='', $buffered = false) {
    global $as_dbengine;
    $this->_outbuf = '';
    $this->_grp_curval = $this->_grpNo = array();
    $this->_totals = array();
    if(empty($title)) $title = $this->_title;
    foreach($this->_grpfields as $grpfld=>$grp) {
        $this->_grp_curval[$grpfld]='{*}';
        $this->_grpNo[$grpfld]=0;
    }
    foreach($this->_rfields as $fid=>$fld) { # init subtotals values array
      $this->_totals[$fid] = array();
      foreach($this->_sumfields as $sumf) { $this->_totals[$fid][$sumf] = ($this->_multitotals)? array():0; }
    }
    foreach($this->_sumfields as $sumf) { $this->_summary[$sumf] = ($this->_multitotals)? array():0; }
    $fnt = ($this->_fontstyles=='')? '': $this->_fontstyles;
    if(count($this->extcss)) foreach($this->extcss as $cssuri) {
        $this->_outbuf .= "<link rel=\"stylesheet\" type=\"text/css\" href=\"$cssuri\" />\n";
    }
    if(!$this->_suppresscss && !$this->_totdf) {
        $this->_outbuf .= <<< EOCSS

<style type="text/css">
/** styles for report drawing **/
   table.as_report { border-collapse: collapse; }
   table.as_report td { border-spacing: 4px; border: 1px solid {$this->bordercolor}; padding: 3px;}
   td.rep_ltrb { border: 1px solid; text-align:center; $fnt}
   td.rep_lrb { border-left:1px solid; border-top:none; border-right: 1px solid #000; border-bottom: 1px solid #000; text-align:left; $fnt}
   td.rep_rb  { border-left:none; border-top:none; border-right: 1px solid; border-bottom: 1px solid; text-align:left; $fnt }
   .rep_totals { background-color:{$this->bg_totals}; $fnt }
   .rep_heading { background-color: {$this->bg_headings}; $fnt }
   td.num { text-align: right; }
   td.cnt { text-align: center; margin-left:auto;margin-right:auto;}
   td.newgroup { background: {$this->bg_newgroup}; $fnt }
</style>
EOCSS;

    }
    $colsp = count($this->_rfields);
    if(!$this->_totdf) {
        $toXlsLink = isset($this->_tofilelink['url']) ? ("<a href='".$this->_tofilelink['url']."' target='_blank'>".$this->_tofilelink['title'].'</a>') : '';
        $this->_outbuf .= "<h4 style='text-align:center; font-weight:bold;'>$title $toXlsLink </h4>\n <center><table class='as_report'>{$this->_headers}";
    }
    else $this->_outbuf .= $title . self::CRLF.self::CRLF;
    # draw report field headers
    if(empty($this->_nodefhead)) {
      $this->DrawReportHeadings();
    }
    $iquery = -1;
    $rlink = count($this->rdata)>0 ? -2 : -1;
    $rdatarow = 0;
    if(is_array($this->rdata)) reset($this->rdata);
    $this->_rowcount = 0;
    while(true) {
      if($rlink===-1) {
        $iquery++;
        if(!isset($this->_query[$iquery])) break;
        $rlink = $as_dbengine->sql_query($this->_query[$iquery]);
        if(!is_resource($rlink)) { $rlink=-1; continue; } # try next query
      }
      if($rlink == -2) {
          $r = current($this->rdata);
          next($this->rdata);
          $rdatarow++;
      }
      else $r=$as_dbengine->fetch_assoc($rlink);
      if(!$r) {
          if($rlink == -2) $rlink = -1;
          elseif($rlink>0) { $as_dbengine->free_result($rlink); $rlink = -1; }
          continue;
      } # process next query in queue

      foreach($this->_rfields as $fid=>$fdef) { # compute "auto-calculated" fields
          if(!empty($fdef->calcfunc) && is_callable($fdef->calcfunc)) {
              $r[$fid] = @call_user_func($fdef->calcfunc,$r);
          }
      }

      # check if some "grouping" field values changed, draw subtotals if so
      $this->_rowcount++;
      if($this->_multitotals && !in_array($r[$this->_multitotals],$this->_muids)) $this->_muids[] = $r[$this->_multitotals];
      $grp_ffield = ''; # becomes a name of the "major" grouping field that has changed
      foreach($this->_grp_curval as $fid=>$curval) {
        if(isset($r[$fid]) && $curval !== $r[$fid]) {
          if($curval!=='{*}') { # draw and reset all accumulated sub-totals
            $this->__DrawSubtotals($fid);
          }
          $this->_grp_curval[$fid]=$r[$fid];
          $grp_ffield=$fid;
          $b_tmp = false;
          foreach($this->_grpfields as $grid=>$grp) {
            if($grid==$grp_ffield) { $b_tmp=true; continue; }
            if($b_tmp) {
                $this->_grp_curval[$grid]='{*}';
                $this->_grpNo[$grid] = 0;
            }
          }
          if(!$b_tmp) $grp_ffield='';
          break;
        }
      }
      if($grp_ffield!=='') {
        # draw headers for next sub-total group
        if($this->_debug>0 && $this->_rowcount >= $this->_debug) break; # debug stop
        $ingrp = false;
        $leftoff='';
        foreach($this->_grpfields as $fid=>$grp) {
          if($fid==$grp_ffield) $ingrp=true;
          if($ingrp) {
            if(($this->_supress_eg) && empty($r[$fid])) continue; # don't print header for empty grouping value
            $fldval = (!empty($grp['fconv']) && function_exists($grp['fconv']))? call_user_func($grp['fconv'],$r[$fid],$r): $r[$fid];
            $txt = $grp['title'].' '.$fldval;
            if($this->_enumGroups) {
                ++$this->_grpNo[$fid];
                $txt = $this->_grpNo[$fid] . ' : ' . $txt;
            }
            $colsp = count($this->_rfields) + ($this->_rownumbers ? 1:0);

# make linked group header ?
            $grpstring = $leftoff . $txt;

            if(isset($this->groupLinks[$fid])) { # make "a href" linked group header
                $href = $this->_eval($this->groupLinks[$fid]['uri'], $r);
                if($href!='') {
                    $idparts = isset($this->groupLinks[$fid]['idparts']) ? explode(',',$this->groupLinks[$fid]['idparts']) : array();
                    $replarray = array();
                    for($ii=0;$ii<count($idparts); $ii++) {
                        $i2 = $ii+1;
                        $replarray["{{$i2}}"] = isset($r[$idparts[$ii]]) ? $r[$idparts[$ii]] : $i2;
                    }
                    if(count($replarray)) $href = str_replace(array_keys($replarray), array_values($replarray), $href);
                    $hopts1 = empty($this->groupLinks[$fid]['title']) ? '' : " title='". $this->_eval($this->groupLinks[$fid]['title'],$r)."'";
                    $hopts2 = empty($this->groupLinks[$fid]['target']) ? '' : " target='".$this->groupLinks[$fid]['target']."'";
                    $grpstring = "<a href=\"$href\"{$hopts1}{$hopts2}>$grpstring</a>";
                }
            }

# make linked group header ?
            if($this->_totdf) $this->_outbuf .= $leftoff . $txt . self::CRLF;
            else $this->_outbuf .= "<tr><td class=\"rep_lrb newgroup\" colspan=\"$colsp\">{$grpstring}</td></tr>\n";
            $this->_grp_curval[$fid]=$r[$fid];
          }
          $leftoff.=' &nbsp;&nbsp;';
        }
      }
      # now draw "normal" report row
      $color = ($this->_rowbg_callback)  && function_exists($this->_rowbg_callback) ? call_user_func($this->_rowbg_callback, $r) : '';
      $trattr = $tdattr = $class = $styles = '';
      if(!empty($r['_row_class_'])) $class = $r['_row_class_'];
      if(($color) && empty($this->_totdf)) {
          if(is_string($color)) $trattr = " style='background-color:$color'";
          elseif(is_array($color)) {
              $attr = array(
                'style' => (isset($color['style']) ? $color['style'] : '')
               ,'background-color' => (isset($color['background-color']) ? $color['background-color'] : '')
              );
              if(isset($color['class'])) $class = $color['class'];

              # css class can be passed as '_row_class_' element in data array:
              foreach($attr as $atkey=>$atval) {
                  if($atval) $styles.= "$atkey:$atval;";
              }
              if(!empty($color['font'])) $tdattr = " style='font:$color[font]'";
          }
      }
      if($class) $trattr .= " class='$class'";
      if($styles) $trattr = " style='$styles'";

      $strow = $this->_totdf ? '': "<tr{$trattr}>";
      $colno = 0;
      if($this->_rownumbers) $strow .= $this->_totdf ? ($this->_rowcount . "\t") : "<td class='rep_lrb cnt'$tdattr>{$this->_rowcount}</td>";

      foreach($this->_rfields as $fid=>$fld) {
        $cls = ((++$colno>1) or $this->_rownumbers) ? 'rep_rb':'rep_lrb'; # Needed only for MSIE 6 that doesn't know css collapse ?

        if(in_array($fld->format, array('c','d','%'))) $cls .= ' cnt';
        elseif(in_array($fld->format, array('r','right','money','i','d2'))) $cls .= ' num';

        if(isset($r[$fid])) {
          if (!empty($fld->fconv) && function_exists($fld->fconv)) $value=call_user_func($fld->fconv,$r[$fid],$r);
          else $value = $this->FormatValue($fid,$r[$fid]);
          if(in_array($fid,$this->_sumfields)) {
            if($this->_multitotals) {
              $muid=$this->_multitotals;
              if(isset($this->_summary[$fid][$r[$muid]])) $this->_summary[$fid][$r[$muid]]+=floatval($r[$fid]);
              else $this->_summary[$fid][$r[$muid]]=floatval($r[$fid]);
            }
            else $this->_summary[$fid] +=floatval($r[$fid]);
            # if(!isset($this->_totals[$fid])) $this->_totals[$fid] = 0;
            foreach($this->_grpfields as $grpid=>$gr) {
              if(empty($this->_multitotals)) {
                if(!isset($this->_totals[$grpid][$fid])) $this->_totals[$grpid][$fid]=0;
                $this->_totals[$grpid][$fid] += floatval($r[$fid]);
              }
              else { #<5>
                $mulid = $r[$this->_multitotals];
                if(!isset($this->_totals[$grpid][$fid][$mulid])) $this->_totals[$grpid][$fid][$mulid]=floatval($r[$fid]);
                else $this->_totals[$grpid][$fid][$mulid] += floatval($r[$fid]);
              } #<5>
            }
#            $cls .= ' num';
          }
        }
        else {
          $value = @function_exists($fld->fconv)? call_user_func($fld->fconv,0,$r) : ($this->_debug ? "($fid)" : ' ');
        }
        if($value==='') $value='&nbsp;';
        if(isset($this->linkedCells[$fid])) { # make linked cell
            $href = $this->_eval($this->linkedCells[$fid]['uri'],$r);
            if($href!='') {
                $idparts = isset($this->linkedCells[$fid]['idparts']) ? explode(',',$this->linkedCells[$fid]['idparts']) : array();
                $replarray = array();
                for($ii=0;$ii<count($idparts); $ii++) {
                    $i2 = $ii+1;
                    $replarray["{{$i2}}"] = isset($r[$idparts[$ii]]) ? $r[$idparts[$ii]] : $i2;
                }
                if(count($replarray)) $href = str_replace(array_keys($replarray), array_values($replarray), $href);
                $ttl = empty($this->linkedCells[$fid]['title']) ? '' : " title='".$this->_eval($this->linkedCells[$fid]['title'],$r)."'";
                $target = empty($this->linkedCells[$fid]['target']) ? '' : " target='".$this->linkedCells[$fid]['target']."'";
                $value = "<a href=\"$href\"{$ttl}{$target}>$value</a>";
            }
        }
        $strow .= ($this->_totdf)? "$value\t" : "<td class=\"$cls\"{$tdattr}>$value</td>";
      }
      $strow .= ($this->_totdf) ? self::CRLF : "</tr>\n";
      $this->_outbuf .= $strow;
    }
    if(is_resource($rlink)) $as_dbengine->free_result($rlink);
    if(count($this->_grpfields)) $this->__DrawSubtotals();
    if(!empty($this->_summarytitle)) $this->__DrawSummaryTotals();
    if(!$this->_totdf) $this->_outbuf .= "</table></center>";
    if($this->_totdf) {
        $this->_outbuf = strip_tags($this->_outbuf); # delete all generated <a href...>
        $this->_outbuf = str_replace('&nbsp;',' ',$this->_outbuf);

        if(!headers_sent()) {
            Header('Content-Type: text/plain');
            Header("Content-Disposition: attachment; filename=\"$this->_outname\"");
            Header('Content-Length: '.strlen($this->_outbuf));
        }
        exit($this->_outbuf);
    }
    elseif($buffered) return $this->_outbuf;
    else echo $this->_outbuf;
  }
  /**
  * Evaluates possible '@function_name' by calling user function
  *
  * @param mixed $param
  * @param mixed $args
  */
  private function _eval($param,$args=false) {
      $ret = $param;
      if(substr($param,0,1)=='@') {
          $fncname = substr($param,1);
          if(function_exists($fncname)) $ret = call_user_func($fncname, $args);
      }
      return $ret;
  }
  /**
  * internal function, draws all sub-totals from lowest level to $gfield level
  *
  * @param string $gfield upper level grouping field
  */
  function __DrawSubtotals($gfield='') {
    if($this->_totdf) return;
    $grfields = array_reverse($this->_grpfields); # go up from "inner" subtotal level
    $b_draw= true;
    foreach($grfields as $fid=>$grp) {
      # .. draw sub-total row;
      if(($this->_supress_eg) && empty($this->_grp_curval[$fid])) continue; # don't print subtotals if empty grouping value

      $ttval = $this->FormatValue($fid,$this->_grp_curval[$fid], $grp['fconv']);
      $ttpl = empty($grp['ttitle']) ? 'Total for %name%': $grp['ttitle'];
      $totitle = str_replace('%name%',$ttval,$grp['ttitle']);
      $cspan=0;
      foreach($this->_rfields as $fldid=>$fld) { if($fld->summable) break; $cspan++; }
      if($this->_rownumbers) $cspan++;
      if($cspan<1) $txt = "<tr class='rep_totals'><td class=\"rep_lrb\" colspan=10 >$totitle</td></tr><tr>";
      else $txt = "<tr class='rep_totals'><td class=\"rep_lrb\" colspan=\"$cspan\">$totitle</td>";

      $b_strt = false;
      foreach($this->_rfields as $flddid=>$fld) {
        if(!$fld->summable && !$b_strt) continue;
        if($fld->summable) {
          $b_strt = true;
          if($this->_multitotals) {
            $value='';
            foreach($this->_muids as $muid) $value .= ($value==''? '':'<br />').
             $this->FormatValue($flddid,(isset($this->_totals[$fid][$flddid][$muid])?$this->_totals[$fid][$flddid][$muid]:0));
          }
          else $value = isset($this->_totals[$fid][$flddid]) ? $this->FormatValue($flddid,$this->_totals[$fid][$flddid]) : '';
          $txt .= "<td class=\"rep_rb num\" nowrap=\"nowrap\">$value</td>";

          if($this->_multitotals) { foreach($this->_muids as $muid) $this->_totals[$fid][$flddid][$muid]=0; }
          else $this->_totals[$fid][$flddid] = 0;
        }
        elseif($flddid===$this->_multitotals) {
          $value = ''; foreach($this->_muids as $muid) $value.=($value===''?'':'<br />').$muid;
          $txt.= "<td class=\"rep_rb\">$value</td>";
        }
        else $txt .= '<td class="rep_rb num">&nbsp;</td>';
      }
      $txt .="</tr>\n";
      $this->_outbuf .= $txt;
      if(!empty($gfield) && $fid==$gfield) break; # upper level of subtotal reached
    }
  }
  function __DrawSummaryTotals() {
    global $as_dbengine;
    if($this->_totdf) return;
    if($this->_summaryQuery) { # recalculate grand-totals by query executing
       $this->_summary = $as_dbengine->sql_query($this->_summaryQuery,true,1,0);
    }
    $cspan=0;
    $title = str_replace('%rowcount%',$this->_rowcount,$this->_summarytitle);
    foreach($this->_rfields as $fldid=>$fld) { if($fld->summable) break; $cspan++; }
    $colcnt = count($this->_rfields);
    if($this->_rownumbers) { $colcnt++; $cspan++; }
    if($cspan<1) $txt = "<tr class='rep_totals'><td class=\"rep_lrb\" colspan=\"$colcnt\" >$title</td></tr><tr>";
    else $txt = "<tr class='rep_totals'><td class=\"rep_lrb\" colspan=\"$cspan\">$title</td>";

    $b_strt = false;
    foreach($this->_rfields as $flddid=>$fld) {
      if(!$fld->summable && !$b_strt) continue;
      if($fld->summable) {
        $b_strt = true;
        if($this->_multitotals) {
          $value = '';
          foreach($this->_muids as $muid) $value .= ($value==''? '':'<br />').
           $this->FormatValue($flddid,(isset($this->_summary[$flddid][$muid])?$this->_summary[$flddid][$muid]:0));
        }
        else  $value = $this->FormatValue($flddid,$this->_summary[$flddid]);
        $txt .= "<td class=\"rep_rb num\" nowrap=\"nowrap\">$value</td>";
      }
      elseif($flddid===$this->_multitotals) {
        $value = ''; foreach($this->_muids as $muid) $value.=($value===''?'':'<br />').$muid;
        $txt.= "<td class=\"rep_rb cnt\">$value</td>";
      }
      else $txt .= '<td class="rep_rb cnt">&nbsp;</td>';
    }
    $txt .="</tr>\n";
    $this->_outbuf .= $txt;
  }
  function HasParameters(){ return (count($this->_params)>0); }
  function FormatValue($fieldid, $value) {
    $conv = !empty($this->_rfields[$fieldid]->fconv) ? $this->_rfields[$fieldid]->fconv :
     (!empty($this->_grpfields[$fieldid]['fconv']) ? $this->_grpfields[$fieldid]['fconv']:'');
    $fmt =  !empty($this->_rfields[$fieldid]->format) ? $this->_rfields[$fieldid]->format : '';
    if(!empty($conv) && function_exists($conv)) $ret = call_user_func($conv,$value);
    elseif($fmt==='money' or $fmt==='d2') {
        if($this->_xlsFmt) $ret = number_format($value,2,$this->_delim_dec, '');
        elseif(!$this->_totdf) $ret = number_format($value,2,$this->_delim_dec, $this->_delim_tho);
        else $ret = floatval($value);
    }
    elseif($fmt==='i') {
        if($this->_xlsFmt) $ret = number_format($value,0,$this->_delim_dec, '');
        elseif(empty($this->_totdf)) $ret = number_format($value,0,$this->_delim_dec, $this->_delim_tho);
        else $ret = floatval($value);
    }
    elseif($fmt==='%') { # convert to percents, with decimal with 2 digits after dec.char
        if($value!=0) {
            if($this->_xlsFmt) $ret = number_format($value*100,2,$this->_delim_dec, '');
            elseif(empty($this->_totdf)) $ret = number_format($value*100,2,$this->_delim_dec, $this->_delim_tho);
            else $ret = floatval($value*100);
        }
        else $ret = $this->_totdf ? 0 : '&nbsp;';
    }
    else $ret = $value;
    return $ret;
  }

  static function DrawJsCode($nostarttag=false, $to_var=false) {
    global $as_iface;
    $err_fill = isset($as_iface['err_emptymandatoryparam']) ? $as_iface['err_emptymandatoryparam']:'Not all mandatory parameters set';
    $bkuri = self::_backenduri;
    $jscode = $nostarttag ? '': "<script type=\"text/javascript\">\n";
    $jscode .= <<<ENDCRIPT

var asrep_activefrm = '';
var reptbkend = "$bkuri";
function asReportShowForm(formid) {
  if(asrep_activefrm!='') $("#"+asrep_activefrm).hide();
  asrep_activefrm = formid;
  if(formid!="") $("#"+asrep_activefrm).show();
  return false;
}

function asOpenReParamForm(divid) {
   if(jQuery("#"+divid).get(0)) {
       jQuery("#"+divid).show();
       return;
   }
    jQuery("body").floatWindow(
    {
       url: reptbkend + "?showform="+divid
       ,userData: { showform:divid }
//       ,closeobj: "btncancel"
       ,title: "Enter parameters!"
    });

}

function asReportExec(formid) {
  var bfill=true;
  var pars = "";
  alert(formid);
//  $("#"+formid+" input").each(function() {
//    var vl = $(this).val();
//    alert(this.name+" "+vl);
//    if(vl=="") bfill=false;
//    if(this.name) pars += '&'+this.name+"="+encodeURIComponent(vl);
//  });
//  if(!bfill) { alert("<?=$err_fill?>"); return false; }
//  var wnd=window.open('{self::_backenduri}?reportdef='+name+pars, "_report","location=1,menubar=1,resizable=1,scrollbars=1,status=0,toolbar=1,top=40,left=40");
//  wnd.focus();
}
ENDCRIPT;
      if(!$nostarttag) $jscode .= "</script>\n";
      if($to_var) return $jscode;
      echo $jscode;

  }
  /**
  * returns HTML code with <form...> containing all parameters for input
  *
  * @param string $formid desired form's name and id
  */
  function DrawParametersForm($formid='') {
    global $as_iface;
    $frmid = ($formid)? $formid : $this->formid;
    if(count($this->_params)<1) return '';
    $ret = "<div id=\"div_$frmid\"><form name=\"$frmid\" id=\"$frmid\"><table border=0 cellspacing=1 cellpadding=2>";
    foreach($this->_params as $no=>$prm) {
      $prompt = $prm->prompt;
      $itag = '';
      switch($prm->inputtype) {
        case 'text':
          $width = ($prm->type=='char')? '180':'70';
          $itag = "<input type=\"text\" name=\"{$prm->id}\" id=\"{$prm->id}\" style='width:{$width}px' class='ibox'/>";
          break;
        case 'select':
          $opts = @call_user_func($prm->fillfunc);
          $itag = "<select name=\"{$prm->id}\" >".DrawSelectOptions($opts,0,1).'</select>';
          break;
        case 'const':
          $val = ($prm->fillfunc) ? @call_user_func($prm->fillfunc) : 0;
          $ret.= "<input type=\"hidden\" name=\"{$prm->id}\" id=\"{$prm->id}\" value=\"$val\" />";
          break;
      }
      if($prm->inputtype!=='const') $ret .= "<tr><td>$prompt</td><td>$itag</td></tr>";
    }
    $submtxt = isset($as_iface['prompt_submit'])? $as_iface['prompt_submit']: 'submit';
    $ret .="<tr><td>&nbsp;</td><td><input type=\"button\" name=\"submit\" class=\"button\" value=\"$submtxt\" onclick=\"asReportExec('$formid')\"/></td></tr></table></form></div>";
    return $ret;
  }
} # CReporTool end