<?php
/**
 * phpaga
 *
 * Operations functionality.
 *
 * This file contains the necessary routines to manage operations.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @version $Id: operations.php 878 2007-10-31 22:11:00Z florian $
 *
 * Copyright (c) 2002, Florian Lanthaler <florian@phpaga.net>
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in
 *      the documentation and/or other materials provided with the
 *      distribution.
 *
 *    * Neither the name of Florian Lanthaler nor the names of his
 *      contributors may be used to endorse or promote products derived
 *      from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


/**
 * Checks if all fields of an operation contain valid values.
 *
 * @param  array  $opInfo  Array containing the operation's information.
 *
 * @return bool   $errors  True. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_operations_validdata ($opInfo)
{

    $errors = array();

    if (!is_array($opInfo))
        $opInfo = array($opInfo);

    if (!phpaga_checkdate($opInfo["op_startdate"]))
        $errors[] = PHPAGA_ERR_OP_STARTDATE;

    if (!phpaga_checkdate($opInfo["op_enddate"]))
        $errors[] = PHPAGA_ERR_OP_ENDDATE;

    if (!phpaga_checkhour ($opInfo["op_starthours"]))
        $errors[] = PHPAGA_ERR_OP_STARTHOURS;

    if (!phpaga_checkhour ($opInfo["op_endhours"]))
        $errors[] = PHPAGA_ERR_OP_ENDHOURS;

    if (!isset($opInfo["op_duration"]) || !strlen($opInfo["op_duration"]) ||
        !phpaga_checkduration ($opInfo["op_duration"]))

        $errors[] = PHPAGA_ERR_OP_DURATION;

    if (!isset($opInfo["op_desc"]) || !strlen(trim($opInfo["op_desc"])))
        $errors[] = PHPAGA_ERR_OP_DESC;

    if (isset($opInfo["prj_id"]) && !is_numeric($opInfo["prj_id"]))
        $errors[] = PHPAGA_ERR_PROJECT_INVALIDID;

    if (isset($opInfo["pe_id"]) && !is_numeric($opInfo["pe_id"]))
        $errors[] = PHPAGA_ERR_PERSON_INVALIDID;

    if (isset($opInfo["opcat_id"]) && !is_numeric($opInfo["opcat_id"]))
        $errors[] = PHPAGA_ERR_OPCAT_INVALIDID;

    if (count($errors))
        return PhPagaError::raiseErrorByCode($errors);

    return true;
}


/**
 * Adds an operation to the database
 *
 * @param array  $opInfo Array containing the operation information
 *
 * @return int   $op_id  ID of the freshly inserted operation. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_operations_add($opInfo)
{

    $op_id = null;

    $result = phpaga_operations_validdata($opInfo);

    if (PhPagaError::isError($result))
        return $result;

    $d = new PhPagaDbData;

    $op_id = $d->nextId('operations_op_id');

    if (!isset($opInfo['op_desc']) || !strlen($opInfo['op_desc']))
        $opInfo['op_desc'] = null;

    if (!isset($opInfo['op_timestamp_start']) || !strlen($opInfo['op_timestamp_start']))
        $opInfo['op_timestamp_start'] = null;

    if (!isset($opInfo['op_timestamp_end']) || !strlen($opInfo['op_timestamp_end']))
        $opInfo['op_timestamp_end'] = null;

    if (!isset($opInfo['op_duration']) || !strlen($opInfo['op_duration']))
        $opInfo['op_duration'] = null;
    else
        $opInfo['op_duration'] = phpaga_duration2minutes($opInfo['op_duration']);

    if (!isset($opInfo['op_billabletype']) || ($opInfo['op_billabletype'] != 0))
        $opInfo['op_billabletype'] = 10;
    else
        $opInfo['op_billabletype'] = 0;

    $fields = array('op_id' => $op_id,
                    'pe_id' => $opInfo['pe_id'],
                    'prj_id' => $opInfo['prj_id'],
                    'opcat_id' => $opInfo['opcat_id'],
                    'op_desc' => $opInfo['op_desc'],
                    'op_timestamp_start' => $opInfo['op_timestamp_start'],
                    'op_timestamp_end' => $opInfo['op_timestamp_end'],
                    'op_duration' => $opInfo['op_duration'],
                    'op_billabletype' => $opInfo['op_billabletype'],
                    'bill_id' => null);

    $r = $d->insert('operations', $fields);

    if (PhPagaError::isError($r))
        return $r;

    return $op_id;
}


/**
 * Update an operation's information
 *
 * @param array  $opInfo Array containing the operation information
 *
 * @return bool   $result  True, or PhPagaError
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_operations_update($opInfo)
{

    $result = phpaga_operations_validdata($opInfo);

    if (PhPagaError::isError($result))
        return $result;

    /*  Note that prj_id and billing info do not get updated this way.
     *  This is intentional.
     */

    if (!isset($opInfo['op_desc']) || !strlen($opInfo['op_desc']))
        $opInfo['op_desc'] = NULL;

    if (!isset($opInfo['op_timestamp_start']) || !strlen($opInfo['op_timestamp_start']))
        $opInfo['op_timestamp_start'] = NULL;

    if (!isset($opInfo['op_timestamp_end']) || !strlen($opInfo['op_timestamp_end']))
        $opInfo['op_timestamp_end'] = NULL;

    if (!isset($opInfo['op_duration']) || !strlen($opInfo['op_duration']))
        $opInfo['op_duration'] = NULL;
    else
        $opInfo['op_duration'] = phpaga_duration2minutes($opInfo['op_duration']);

    if (!isset($opInfo['op_billabletype']) || ($opInfo['op_billabletype'] != 0))
        $opInfo['op_billabletype'] = 10;
    else
        $opInfo['op_billabletype'] = 0;

    $fields = array('pe_id' => $opInfo['pe_id'],
                    'opcat_id' => $opInfo['opcat_id'],
                    'op_desc' => $opInfo['op_desc'],
                    'op_timestamp_start' => $opInfo['op_timestamp_start'],
                    'op_timestamp_end' => $opInfo['op_timestamp_end'],
                    'op_duration' => $opInfo['op_duration'],
                    'op_billabletype' => $opInfo['op_billabletype']);

    $d = new PhPagaDbData;
    $r = $d->update('operations', $fields, 'op_id = ?', $opInfo['op_id']);

    if (PhPagaError::isError($r))
        return $r;

    return true;
}


/**
 * Gets a list of operation categories from the database and returns
 * them as the content of an array.
 *
 * @return array $result  Array containing the results. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_operations_getCategoryList()
{
    $opcatList = array();

    $d = new PhPagaDbData;

    $r = $d->queryAll('SELECT opcat_id, opcat_title FROM operationcat ORDER BY opcat_title');

    if (PhPagaError::isError($r))
        return $r;

    foreach ($r as $qrow) {
        phpaga_sanitizearray($qrow);
        $opcatList[$qrow["opcat_id"]] = $qrow["opcat_title"];
    }

    return $opcatList;
}


/**
 * Gets an operation's information.
 *
 * @param int    $opID    Operation's ID
 *
 * @return array  $opInfo  Array containing the results. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_operations_getInfo($opID)
{
    $opInfo = array();

    if (!isset($opID) || (!is_numeric($opID)))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_OPERATION_INVALIDID);

    $query = 'SELECT op_id, operations.pe_id, operations.opcat_id,
        operations.prj_id, prj_title, opcat_title,
        op_desc, op_timestamp_start, op_timestamp_end,
        op_duration, bill_id, persons.pe_lastname,
        persons.pe_firstname, op_billabletype
        FROM operations
        LEFT JOIN projects ON projects.prj_id = operations.prj_id
        LEFT JOIN operationcat on operationcat.opcat_id = operations.opcat_id
        LEFT JOIN persons on persons.pe_id = operations.pe_id
        WHERE op_id = ?';

    $d = new PhPagaDbData;

    $opInfo = $d->queryRow($query, $opID);

    if (PhPagaError::isError($opInfo) || !count($opInfo))
        return $opInfo;

    $opInfo['op_person'] = phpaga_persons_formatname($opInfo['pe_lastname'], $opInfo['pe_firstname']);

    if (strlen($opInfo['op_timestamp_start'])) {
        $opInfo['op_startdate'] = date('Y-m-d', $opInfo['op_timestamp_start']);
        $opInfo['op_starthours'] = date('H:i', $opInfo['op_timestamp_start']);
    } else {
        $opInfo['op_startdate'] = '';
        $opInfo['op_starthours'] = '';
    }

    if (strlen($opInfo['op_timestamp_end'])) {
        $opInfo['op_enddate'] = date('Y-m-d', $opInfo['op_timestamp_end']);
        $opInfo['op_endhours'] = date('H:i', $opInfo['op_timestamp_end']);
    } else {
        $opInfo['op_enddate'] = '';
        $opInfo['op_endhours'] = '';
    }

    if (strlen($opInfo['op_duration'])) {
        $opInfo['op_duration'] = phpaga_minutes2duration($opInfo['op_duration']);
    }

    return $opInfo;
}


/**
 * Search operations matching search criteria.
 *
 * @param int    $count     Number of records matching the query
 * @param string $duration  Total duration of matching operations
 * @param array  $opInfo    Operation information
 * @param array  $opIds     IDs of the resulting operations
 * @param int    $order     Specifies the order by field
 * @param int    $offset    Offset
 * @param int    $limit     Limit
 *
 * @return array $rows      Array containing the results. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_operations_search(&$count,
                                  &$duration,
                                  &$opIds,
                                  $opInfo,
                                  $order = 0,
                                  $offset =null,
                                  $limit = null)
{

    $count = 0;
    $rows = array();
    $opIds = array();
    $where = array();
    $qparam = array();
    $where_clause = null;
    $duration = '0:00';

    $countquery = 'SELECT COUNT(op_id) AS count_op, SUM(op_duration) as duration
        FROM operations LEFT JOIN projects ON projects.prj_id = operations.prj_id ';

    $query = 'SELECT op_id, operations.prj_id, prj_title, opcat_title,
        op_desc, op_timestamp_start, op_timestamp_end,
        op_duration, op_billabletype, bill_id, operations.pe_id, persons.pe_lastname,
        persons.pe_firstname, projects.cpn_id_issuebill,
        projects.prj_status, prjstat_title, prj_deadline,
        case when (ph.phr_hourlyrate IS NOT NULL) then ph.phr_hourlyrate
        ELSE case when prjmem_hourlyrate IS NOT NULL THEN prjmem_hourlyrate ELSE phn.phr_hourlyrate END END as applicable_hourlyrate,
        (case when (ph.phr_hourlyrate IS NOT NULL) then ph.phr_hourlyrate
        ELSE case when prjmem_hourlyrate IS NOT NULL THEN prjmem_hourlyrate ELSE phn.phr_hourlyrate END END * ';

    /* Mysql cannot cast to FLOAT */
    if (PHPAGA_DB_SYSTEM == 'mysql')
        $query .= '(op_duration/ 60))  AS amount ';
    else
        $query .= '(cast(op_duration AS FLOAT)/ 60))  AS amount ';

    $query .= 'FROM operations
        LEFT JOIN projects ON projects.prj_id = operations.prj_id
        LEFT JOIN projectstatus ON projectstatus.prjstat_id = projects.prj_status
        LEFT JOIN operationcat on operationcat.opcat_id = operations.opcat_id
        LEFT JOIN project_members pm ON (pm.prj_id = operations.prj_id AND pm.pe_id = operations.pe_id)
        LEFT JOIN persons on persons.pe_id = operations.pe_id
        LEFT JOIN projects_hourlyrates ph ON (ph.opcat_id = operations.opcat_id and ph.prj_id = operations.prj_id AND ph.pe_id = operations.pe_id)
        LEFT JOIN projects_hourlyrates phn ON (phn.opcat_id = operations.opcat_id and phn.prj_id = operations.prj_id AND phn.pe_id IS NULL) ';

    /* Check whether the user has the permission to search for other
     * people's operations */

    $viewother = false;

    if (phpaga_user_hasperm(PHPAGA_PERM_VIEW_ALLTASKS) || phpaga_user_hasperm(PHPAGA_PERM_MANAGE_ALLTASKS))
        $viewother = true;
    else
        if (phpaga_user_hasperm(PHPAGA_PERM_VIEW_OTHERMEMTASKS))
            if (isset($opInfo['prj_id']) && is_numeric($opInfo['prj_id']))
                $viewother = phpaga_project_is_member($opInfo['prj_id'], $_SESSION['auth_user']['pe_id']);

    if (!$viewother) {
        $where[] = 'operations.pe_id = ?';
        $qparam[] = $_SESSION['auth_user']['pe_id'];
    } else if (isset($opInfo['pe_id']) && is_numeric($opInfo['pe_id'])) {
        $where[] = 'operations.pe_id = ?';
        $qparam[] = $opInfo['pe_id'];
    }

    if (isset($opInfo['bill_id']) && is_numeric($opInfo['bill_id'])) {
        $where[] = 'operations.bill_id = ?';
        $qparam[] = $opInfo['bill_id'];
    }

    if (isset($opInfo['prj_id']) && is_numeric($opInfo['prj_id'])) {
        $where[] = 'operations.prj_id = ?';
        $qparam[] = $opInfo['prj_id'];
    }

    if (isset($opInfo['prjcat_id']) && is_numeric($opInfo['prjcat_id'])) {
        $where[] = 'projects.prjcat_id = ?';
        $qparam[] = $opInfo['prjcat_id'];
    }

    if (isset($opInfo['jcat_id']) && is_numeric($opInfo['jcat_id'])) {
        $where[] = 'persons.jcat_id = ?';
        $qparam[] = $opInfo['jcat_id'];
    }

    if (isset($opInfo['cpn_id']) && is_numeric($opInfo['cpn_id'])) {
        $where[] = 'projects.cpn_id = ?';
        $qparam[] = $opInfo['cpn_id'];
    }

    if (isset($opInfo['cpn_id_issuebill']) && is_numeric($opInfo['cpn_id_issuebill'])) {
        $where[] = 'projects.cpn_id_issuebill = ?';
        $qparam[] = $opInfo['cpn_id_issuebill'];
    }

    if (isset($opInfo['prj_status']) && is_numeric($opInfo['prj_status'])) {
        $where[] = 'projects.prj_status = ?';
        $qparam[] = $opInfo['prj_status'];
    }

    if (isset($opInfo['prj_billabletype']) && is_numeric($opInfo['prj_billabletype']))
        if ($opInfo['prj_billabletype'] == 0)
            $where[] = '(prj_billabletype IS NULL OR prj_billabletype = 0)';
        else {
            $where[] = 'prj_billabletype = ?';
            $qparam[] = $opInfo['prj_billabletype'];
        }

    if (isset($opInfo['op_billabletype']) && is_numeric($opInfo['op_billabletype']))
        if ($opInfo['op_billabletype'] == 0)
            $where[] = '(op_billabletype IS NULL OR op_billabletype = 0)';
        else {
            $where[] = 'op_billabletype = ?';
            $qparam[] = $opInfo['op_billabletype'];
        }

    if (isset($opInfo['op_billed']) && is_numeric($opInfo['op_billed'])) {

        switch ($opInfo['op_billed']) {

            case PHPAGA_BILL_NOTBILLED:
                $where[] = 'operations.bill_id IS NULL';
                break;

            case PHPAGA_BILL_BILLED:
                $where[] = 'operations.bill_id IS NOT NULL';
                break;

            default:
                break;
        }
    }

    if (isset($opInfo['opcat_id']) && is_numeric($opInfo['opcat_id'])) {
        $where[] = 'operations.opcat_id = ?';
        $qparam[] = $opInfo['opcat_id'];
    }

    if (isset($opInfo['op_desc']) && ($opInfo['op_desc'] != '')) {
        $where[] = 'operations.op_desc LIKE ?';
        $qparam[] = '%'.$opInfo['op_desc'].'%';
    }

    if (isset($opInfo['op_startdatefrom']) && ($opInfo['op_startdatefrom'] != '')) {
        $where[] = 'operations.op_timestamp_start >= ?';
        $qparam[] = strtotime($opInfo['op_startdatefrom']);
    }

    if (isset($opInfo['op_startdateto']) && ($opInfo['op_startdateto'] != '')) {
        $where[] = 'operations.op_timestamp_start <= ?';
        $qparam[] = strtotime($opInfo['op_startdateto'].' 23:59:59');
    }

    if (isset($opInfo['opidlist']) && strlen($opInfo['opidlist'])) {
        $where[] = sprintf('op_id IN (%s)', $opInfo['opidlist']);
    }

    if (count($where)) {
        $h = sprintf('WHERE %s ', implode(' AND ', $where));
        $countquery .= $h;
        $query .= $h;
    }

    $query .= ' GROUP BY op_id, operations.prj_id, prj_title, opcat_title, op_desc,
        op_timestamp_start, op_timestamp_end, op_duration, op_billabletype, bill_id, operations.pe_id,
        persons.pe_lastname, persons.pe_firstname, projects.cpn_id_issuebill,
        projects.prj_status, prjstat_title, prj_deadline, prjmem_hourlyrate, ph.phr_hourlyrate, phn.phr_hourlyrate
        ORDER BY ';

    if (!strlen($order) || ($order == 0) || !is_numeric($order))
        $order = ORDER_OP_TIMESTAMP_START_DESC;

    switch ($order) {

        case ORDER_OP_PE_ASC:
            $query .= 'pe_lastname ASC, pe_firstname ASC';
            break;
        case ORDER_OP_PE_DESC:
            $query .= 'pe_lastname DESC, pe_firstname DESC';
            break;
        case ORDER_OP_PR_ASC:
            $query .= 'prj_title ASC';
            break;
        case ORDER_OP_PR_DESC:
            $query .= 'prj_title DESC';
            break;
        case ORDER_OP_OPCAT_ASC:
            $query .= 'opcat_title ASC';
            break;
        case ORDER_OP_OPCAT_DESC:
            $query .= 'opcat_title DESC';
            break;
        case ORDER_OP_DURATION_ASC:
            $query .= 'op_duration ASC';
            break;
        case ORDER_OP_DURATION_DESC:
            $query .= 'op_duration DESC';
            break;
        case ORDER_OP_TIMESTAMP_START_ASC:
            $query .= 'op_timestamp_start ASC';
            break;
        default:
            $query .= 'op_timestamp_start DESC ';
            break;
    }

    $d = new PhPagaDbData;

    $r = $d->queryRow($countquery, $qparam);

    if (!PhPagaError::isError($r) && count($r)) {
        $count = $r['count_op'];

        if (isset($r['duration']) && is_numeric($r['duration']))
            $duration = phpaga_minutes2duration($r['duration']);
    }

    $operations = $d->queryAll($query, $qparam, $offset, $limit);

    if (PhPagaError::isError($operations) || !count($operations))
        return $operations;

    $perm_tasks = phpaga_user_hasperm(PHPAGA_PERM_MANAGE_ALLTASKS);

    foreach ($operations as $qrow) {

        phpaga_sanitizearray($qrow);

        /* The user can edit the task it has not been billed and
         * either she has overall task management rights or it is her
         * task */

        if ((!isset($qrow['bill_id']) || !strlen($qrow['bill_id']))
            && ($perm_tasks || (isset($qrow['pe_id']) && ($qrow['pe_id'] == $_SESSION['auth_user']['pe_id']))))

            $qrow['op_canedit'] = true;
        else
            $qrow['op_canedit'] = false;

        $qrow['op_duration_hrs'] = phpaga_minutes2duration ($qrow['op_duration']);
        $qrow['op_person'] = phpaga_persons_formatname ($qrow['pe_lastname'], $qrow['pe_firstname']);

        if (strlen($qrow['op_timestamp_start'])) {
            $qrow['op_startdate'] = date('Y-m-d', $qrow['op_timestamp_start']);
            $qrow['op_starthours'] = date('H:i', $qrow['op_timestamp_start']);
        } else {
            $qrow['op_startdate'] = '';
            $qrow['op_starthours'] = '';
        }

        if (strlen($qrow['op_timestamp_end'])) {
            $qrow['op_enddate'] = date('Y-m-d', $qrow['op_timestamp_end']);
            $qrow['op_endhours'] = date('H:i', $qrow['op_timestamp_end']);
        } else {
            $qrow['op_enddate'] = '';
            $qrow['op_endhours'] = '';
        }

        if ($qrow['op_duration'] != '')
            $qrow['duration'] = phpaga_minutes2duration($qrow['op_duration']);

        $rows[] = $qrow;
        $opIds[] = $qrow['op_id'];
    }

    return $rows;
}


/**
 * Gets task category statistics
 *
 * @param array  $opInfo        Operation information
 * @param int    $sum_duration  Total duration in minutes
 * @param bool   $groupby       Shall the results be grouped?
 *                              Possible groupings are:
 *                                by project: PHPAGA_OPCGRP_PROJECT
 *                                by person:  PHPAGA_OPCGRP_PERSON
 *
 * @return array $opcatStats   Array containing the results. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_operations_getOpcatStats($opInfo, &$sum_duration, $groupby = 0)
{
    $opcatStats = array();
    $sum_duration = 0;
    $where = array();
    $qparam = array();
    $tmp_opcatStats = array();
    $tmp_prj_id  = array();
    $tmp_opcat_id = array();
    $dummy1 = 0;
    $dummy2 = 0;
    $opc = array();
    $prj = array();
    $query_extragrp = null;
    $query_extraorder = null;
    $query_extrajoin = null;

    $query = 'SELECT operations.opcat_id, opcat_title, opcat_color, SUM(op_duration) AS opcat_duration ';

    switch($groupby)
    {
        case PHPAGA_OPCGRP_PROJECT:
            $query .=', operations.prj_id, prj_title ';
            $query_extragrp = 'operations.prj_id, prj_title, ';
            $query_extraorder = 'opcat_title, prj_title, ';
            break;

        case PHPAGA_OPCGRP_PERSON:
            $query .=', operations.pe_id, pe_lastname, pe_firstname ';
            $query_extragrp = 'operations.pe_id, pe_lastname, pe_firstname, ';
            $query_extraorder = 'opcat_title, pe_lastname, pe_firstname, ';
            $query_extrajoin = 'LEFT JOIN persons p ON p.pe_id = operations.pe_id ';
            break;

        default:
            break;
    }

    $query .= 'FROM operations
        LEFT JOIN projects ON projects.prj_id = operations.prj_id
        LEFT JOIN operationcat on operationcat.opcat_id = operations.opcat_id '.
        $query_extrajoin;

    if (isset($opInfo['pe_id']) && is_numeric($opInfo['pe_id'])) {
        $where[] = 'operations.pe_id = ?';
        $qparam[] = $opInfo['pe_id'];
    }

    if (isset($opInfo['prj_id']) && is_numeric($opInfo['prj_id'])) {
        $where[] = 'operations.prj_id = ?';
        $qparam[] = $opInfo['prj_id'];
    }

    if (isset($opInfo['cpn_id']) && is_numeric($opInfo['cpn_id'])) {
        $where[] = 'projects.cpn_id = ?';
        $qparam[] = $opInfo['cpn_id'];
    }

    if (isset($opInfo['opcat_id']) && is_numeric($opInfo['opcat_id'])) {
        $where[] = 'operations.opcat_id = ?';
        $qparam[] = $opInfo['opcat_id'];
    }

    if (isset($opInfo['op_desc']) && strlen($opInfo['op_desc'])) {
        $where[] = 'operations.op_desc LIKE ?';
        $qparam[] = '%'.$opInfo['op_desc'].'%';
    }

    if (isset($opInfo['op_startdatefrom']) && strlen($opInfo['op_startdatefrom'])) {
        $where[] = 'operations.op_timestamp_start >= ?';
        $qparam[] = strtotime($opInfo['op_startdatefrom']);
    }

    if (isset($opInfo['op_startdateto']) && strlen($opInfo['op_startdateto'])) {
        $where[] = 'operations.op_timestamp_start <= ?';
        $qparam[] = strtotime($opInfo['op_startdateto']);
    }

    if (count ($where) > 0)
        $query .= 'WHERE '.implode(' AND ', $where).' ';

    $query .= ' GROUP BY '.$query_extragrp.' operations.opcat_id, opcat_title, opcat_color
        ORDER BY '.$query_extraorder.' opcat_duration DESC';

    $d = new PhPagaDbData;

    $r = $d->queryAll($query, $qparam);

    if (PhPagaError::isError($r) || !count($r))
        return $r;

    foreach ($r as $qrow) {

        $sum_duration += $qrow['opcat_duration'];

        /* If any grouping is selected we need to fill the helper
         * arrays that will be required to create the matrix array
         */

        switch($groupby) {

            case PHPAGA_OPCGRP_PROJECT:

                $tmp_opcatStats['rows'][$qrow['opcat_id']][$qrow['prj_id']] = $qrow;

                if (!isset($tmp_prj_id[$qrow['prj_id']]) || !is_array($tmp_prj_id[$qrow['prj_id']])) {

                    $tmp_prj_id[$qrow['prj_id']] = array('prj_id' => $qrow['prj_id'],
                                                         'prj_title' => $qrow['prj_title'],
                                                         'duration' => $qrow['opcat_duration']);
                } else
                    $tmp_prj_id[$qrow['prj_id']]['duration'] += $qrow['opcat_duration'];

                if (!isset($tmp_opcat_id[$qrow['opcat_id']]) || !is_array($tmp_opcat_id[$qrow['opcat_id']])) {

                    $tmp_opcat_id[$qrow['opcat_id']] = array('opcat_id' => $qrow['opcat_id'],
                                                             'opcat_title' => $qrow['opcat_title'],
                                                             'duration' => $qrow['opcat_duration']);
                } else
                    $tmp_opcat_id[$qrow['opcat_id']]['duration'] += $qrow['opcat_duration'];

                break;


            case PHPAGA_OPCGRP_PERSON:

                $qrow['pe_person'] = phpaga_persons_formatname($qrow['pe_lastname'], $qrow['pe_firstname']);

                $tmp_opcatStats['rows'][$qrow['opcat_id']][$qrow['pe_id']] = $qrow;

                if (!isset($tmp_pe_id[$qrow['pe_id']]) || !is_array($tmp_pe_id[$qrow['pe_id']])) {

                    $tmp_pe_id[$qrow['pe_id']] = array('pe_id' => $qrow['pe_id'],
                                                        'pe_person' => $qrow['pe_person'],
                                                        'duration' => $qrow['opcat_duration']);
                } else
                    $tmp_pe_id[$qrow['pe_id']]['duration'] += $qrow['opcat_duration'];

                if (!isset($tmp_opcat_id[$qrow['opcat_id']]) || !is_array($tmp_opcat_id[$qrow['opcat_id']])) {

                    $tmp_opcat_id[$qrow['opcat_id']] = array('opcat_id' => $qrow['opcat_id'],
                                                             'opcat_title' => $qrow['opcat_title'],
                                                             'duration' => $qrow['opcat_duration']);
                } else
                    $tmp_opcat_id[$qrow['opcat_id']]['duration'] += $qrow['opcat_duration'];

                break;

            default:
                $opcatStats[] = $qrow;
                break;

        }
    }

    switch($groupby) {

        case PHPAGA_OPCGRP_PROJECT:

            /* Build matrix */

            $opcatStats['opcats'] = $tmp_opcat_id;
            $opcatStats['projects'] = $tmp_prj_id;

            foreach ($tmp_opcat_id as $dummy1 => $opc)
                foreach ($tmp_prj_id as $dummy2 => $prj)
                    if (!isset($tmp_opcatStats['rows'][$opc['opcat_id']][$prj['prj_id']])
                        || !is_array($tmp_opcatStats['rows'][$opc['opcat_id']][$prj['prj_id']]))

                        $opcatStats['rows'][$opc['opcat_id']][$prj['prj_id']] = array(
                            'opcat_title' => '',
                            'opcat_duration' => '',
                            'prj_id' => '',
                            'prj_title' => ''
                            );
                    else
                        $opcatStats['rows'][$opc['opcat_id']][$prj['prj_id']] =
                            $tmp_opcatStats['rows'][$opc['opcat_id']][$prj['prj_id']];
            break;


        case PHPAGA_OPCGRP_PERSON:

            /* Build matrix */

            $opcatStats['opcats'] = $tmp_opcat_id;
            $opcatStats['persons'] = $tmp_pe_id;

            foreach ($tmp_opcat_id as $dummy1 => $opc)
                foreach ($tmp_pe_id as $dummy2 => $pe)
                    if (!isset($tmp_opcatStats['rows'][$opc['opcat_id']][$pe['pe_id']])
                        || !is_array($tmp_opcatStats['rows'][$opc['opcat_id']][$pe['pe_id']]))

                        $opcatStats['rows'][$opc['opcat_id']][$pe['pe_id']] = array(
                            'opcat_title' => '',
                            'opcat_duration' => '',
                            'pe_id' => '',
                            'peperson' => ''
                            );
                    else
                        $opcatStats['rows'][$opc['opcat_id']][$pe['pe_id']] =
                            $tmp_opcatStats['rows'][$opc['opcat_id']][$pe['pe_id']];
            break;

        default:
            break;

    }

    return $opcatStats;
}


/**
 * Delete a single operation.
 *
 * @param int    $opID    Operation ID
 *
 * @return bool  $result  True. A PhPagaError on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_operations_delete($opID)
{

    if (!isset($opID) || !is_numeric($opID))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_OPERATION_INVALIDID);

    $d = new PhPagaDbData;

    /* check for dependencies: an operation cannot be deleted if it
     * has already been invoiced */

    $depqueries = array('SELECT COUNT(op_id) FROM operations WHERE op_id = ? AND bill_id IS NOT NULL',
                        'SELECT COUNT(lit_id) FROM lineitems WHERE op_id = ? AND bill_id IS NOT NULL');

    foreach ($depqueries as $dpq) {

        $n = $d->queryOneVal($dpq, $opID);

        if (PhPagaError::isError($n))
            return $n;

        if ($n > 0)
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DEL_DEPENDENCIES);
    }

    $result = $d->execute('DELETE FROM operations WHERE op_id = ?', $opID);

    if (PhPagaError::isError($result))
        return $result;

    return true;
}


/**
 * Calculate manhours. Manhours can be calculated by person, project, invoice,
 * company, operation category, or overall. A timeframe must be given, and a
 * display type (daily, calendar weeks, monthly).
 *
 * @param array  $opInfo    Operation information (search parameters)
 * @param int    $type      Specifies the way data has to be grouped (displayed)
 *
 * @return array $rows      Array containing the results. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_operations_getManhours($opInfo, $type = PHPAGA_MANHOURS_DISPLAY_MONTH)
{
    $rows = array();
    $select = array();
    $where = array();
    $qparam = array();
    $group = null;

    /* Build the query depending on the selected type and on the underlying rdbms */
    switch ($type) {

        case PHPAGA_MANHOURS_DISPLAY_YEAR:

            switch(PHPAGA_DB_SYSTEM) {

                case 'pgsql':
                    $select[] = "TO_CHAR(op_timestamp_start::abstime::timestamp without time zone, 'iw YYYY') AS mhrs_date ";
                    $group = "GROUP BY TO_CHAR(op_timestamp_start::abstime::timestamp without time zone, 'iw YYYY') ";

                    if (isset($opInfo['op_startdatefrom']) && strlen($opInfo['op_startdatefrom'])) {
                        $where[] = "date_part('year', op_timestamp_start::abstime::timestamp without time zone) = ?";
                        $qparam[] = date('Y', strtotime($opInfo['op_startdatefrom']));
                    }

                    break;

                case "mysql":
                    $select[] = "FROM_UNIXTIME(op_timestamp_start, '%v %x') AS mhrs_date ";
                    $group = "GROUP BY FROM_UNIXTIME(op_timestamp_start, '%v %x') ";

                    if (isset($opInfo["op_startdatefrom"]) && ($opInfo["op_startdatefrom"])) {
                        $where[] = "FROM_UNIXTIME(op_timestamp_start, '%x') = ?";
                        $qparam[] = date('Y', strtotime($opInfo['op_startdatefrom']));
                    }

                    break;

                default:
                    /* We should never reach this */
                    break;

            }

            break;

        default:
            /* Month view */

            switch(PHPAGA_DB_SYSTEM) {

                case 'pgsql':
                    $select[] = "TO_CHAR(op_timestamp_start::abstime::timestamp without time zone, 'YYYY-mm-dd') AS mhrs_date ";
                    $group = "GROUP BY TO_CHAR(op_timestamp_start::abstime::timestamp without time zone, 'YYYY-mm-dd') ";

                    if (isset($opInfo["op_startdatefrom"]) && strlen($opInfo["op_startdatefrom"])) {
                        $where[] = "TO_CHAR(op_timestamp_start::abstime::timestamp without time zone, 'YYYY-mm') = ?";
                        $qparam[] = date('Y-m', strtotime($opInfo['op_startdatefrom']));
                    }

                    break;

                case 'mysql':
                    $select[] = "FROM_UNIXTIME(op_timestamp_start, \"%Y-%m-%d\") AS mhrs_date ";
                    $group = "GROUP BY FROM_UNIXTIME(op_timestamp_start, \"%Y-%m-%d\") ";

                    if (isset($opInfo["op_startdatefrom"]) && strlen($opInfo["op_startdatefrom"])) {
                        $where[] = "FROM_UNIXTIME(op_timestamp_start, '%Y-%m') = ?";
                        $qparam[] = date("Y-m", strtotime($opInfo["op_startdatefrom"]));
                    }

                    break;

                default:
                    /* We should never reach this */
                    break;
            }

            break;
    }

    $select[] = 'SUM(op_duration) AS mhrs_duration';

    if (isset($opInfo['pe_id']) && is_numeric($opInfo['pe_id'])) {
        $where[] = 'operations.pe_id = ?';
        $qparam[] = $opInfo['pe_id'];
    }

    if (isset($opInfo['bill_id']) && is_numeric($opInfo['bill_id'])) {
        $where[] = 'operations.bill_id = ?';
        $qparam[] = $opInfo['bill_id'];
    }

    if (isset($opInfo['prj_id']) && is_numeric($opInfo['prj_id'])) {
        $where[] = 'operations.prj_id = ?';
        $qparam[] = $opInfo['prj_id'];
    }

    if (isset($opInfo['cpn_id']) && is_numeric($opInfo['cpn_id'])) {
        $where[] = 'projects.cpn_id = ?';
        $qparam[] = $opInfo['cpn_id'];
    }

    if (isset($opInfo['opcat_id']) && is_numeric($opInfo['opcat_id'])) {
        $where[] = 'operations.opcat_id = ?';
        $qparam[] = $opInfo['opcat_id'];
    }

    if (count ($select) > 0)
        $query = 'SELECT '.implode(', ', $select).' ';

    $query .= 'FROM operations
        LEFT JOIN projects ON projects.prj_id = operations.prj_id
        LEFT JOIN operationcat on operationcat.opcat_id = operations.opcat_id
        LEFT JOIN persons on persons.pe_id = operations.pe_id ';

    if (count ($where) > 0)
        $query .= 'WHERE '.implode(' AND ', $where).' ';

    $query .= $group.' ORDER BY mhrs_date';

    $d = new PhPagaDbData;

    $r = $d->queryAll($query, $qparam);

    if (PhPagaError::isError($r) || !count($r))
        return $r;

    foreach ($r as $qrow) {

        phpaga_sanitizearray($qrow);

        if (strlen($qrow['mhrs_duration'])) {
            $qrow['duration'] = phpaga_minutes2duration($qrow['mhrs_duration']);
            $qrow['mhrs_duration'] = $qrow['mhrs_duration'] / 60;
        }

        $rows[] = $qrow;
    }

    return $rows;
}


/**
 * Loads all operation categories into an array. If opcat_id is
 * specified, only the matching opcat is loaded.
 *
 * @param int    $opcat_id    Operation category ID (optional)
 * @param bool   $byid        If set to true, the returned array
 *                            is indexed by opcat_id (optional)
 *
 * @return array  $opcatInfo  Array containing the results. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_operations_getOpcats($opcat_id = null, $byid = false)
{
    $opcatInfo = array();
    $param = null;

    $d = new PhPagaDbData;

    $query = 'SELECT opcat_id, opcat_title, opcat_color, opcat_hourlyrate FROM operationcat ';

    if (isset($opcat_id) && is_numeric($opcat_id)) {
        $query .= 'WHERE opcat_id = ?';
        $param = $opcat_id;
    }

    $query .= ' ORDER BY opcat_title';

    $r = $d->queryAll($query, $param);

    if (PhPagaError::isError($r) || !count($r))
        return $r;

    foreach ($r as $qrow) {

        phpaga_sanitizearray($qrow);

        if ($byid)
            $opcatInfo[$qrow['opcat_id']] = $qrow;
        else
            $opcatInfo[] = $qrow;
    }

    return $opcatInfo;
}


/**
 * Add a new operation category.
 *
 * @param  string  $opcat_title        Operation category to be added
 * @param  string  $opcat_color        Color to be displayed in graphs (optional)
 * @param  float   $opcat_hourlyrate   Default hourly rate
 *
 * @return int     $id                 Category Id. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_operations_addOpcat($opcat_title, $opcat_color = null, $opcat_hourlyrate = null)
{

    if (!isset($opcat_title) || !strlen ($opcat_title))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_OPCAT_INVALIDTITLE);

    if (strlen($opcat_hourlyrate) && !is_numeric($opcat_hourlyrate))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_INVALID_PARAMTYPE);

    $d = new PhPagaDbData;
    $id = $d->nextId('operationcat_opcat_id');

    $fields = array('opcat_id' => $id,
                    'opcat_title' => $opcat_title,
                    'opcat_color' => $opcat_color,
                    'opcat_hourlyrate' => $opcat_hourlyrate);

    $r = $d->insert('operationcat', $fields);

    if (PhPagaError::isError($r))
        return $r;

    return $id;
}


/**
 * Update an operation category.
 *
 * @param  string  $opcat_id           Operation category ID
 * @param  string  $opcat_title        Title
 * @param  string  $opcat_color        Color to be displayed in graphs
 * @param  float   $opcat_hourlyrate   Default hourly rate
 *
 * @return bool   $result  True. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_operations_updateOpcat($opcat_id, $opcat_title, $opcat_color, $opcat_hourlyrate)
{

    if (!isset($opcat_id) || !strlen ($opcat_id))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_OPCAT_INVALIDID);

    if (!isset($opcat_title) || !strlen ($opcat_title))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_OPCAT_INVALIDTITLE);

    if (strlen($opcat_hourlyrate) && !is_numeric($opcat_hourlyrate))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_INVALID_PARAMTYPE);

    if (!strlen(trim($opcat_color)))
        $opcat_color = null;

    $fields = array('opcat_title' => $opcat_title,
                    'opcat_color' => $opcat_color,
                    'opcat_hourlyrate' => $opcat_hourlyrate);

    $d = new PhPagaDbData;
    $r = $d->update('operationcat', $fields, 'opcat_id = ?', $opcat_id);

    if (PhPagaError::isError($r))
        return $r;

    return true;
}


/**
 * Delete an operation category.
 *
 * @param  int     $opcat_id    Operation category to be deleted
 *
 * @return bool    $result      True if successful. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_operations_deleteOpcat($opcat_id)
{
    if (!isset($opcat_id) || !is_numeric($opcat_id))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_OPCAT_INVALIDID);

    $d = new PhPagaDbData;

    $n = $d->queryOneVal('SELECT COUNT(op_id) FROM operations WHERE opcat_id = ?', $opcat_id);

    if (PhPagaError::isError($n))
        return $n;

    if ($n > 0)
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DEL_DEPENDENCIES);

    $result = $d->execute('DELETE FROM operationcat WHERE opcat_id = ?', $opcat_id);

    if (PhPagaError::isError($result))
        return $result;

    return true;
}


/**
 * Fetches the human cost for operations.
 *
 * The hourly rate is fetched according to the following precedence:
 *
 *  1. Member's hourly rate for a specific task category and project
 *  2. Member's hourly wage rate for the given project, independent of the task category
 *  3. Default rate for the given task category and project
 *
 * @param array  $opIds         Array containing operations IDs
 *
 * @return array $humancost     Array containing the cost information. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.2
 */

function phpaga_operations_gethumancost($opIds)
{

    $humancost = array();
    $h_details = array();

    $humancost['total_amount'] = 0;
    $humancost['total_duration'] = 0;

    if (!isset($opIds) || !is_array($opIds) || !(count($opIds) > 0))
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_INVALIDSEARCHPARAM);

    $query = 'SELECT op.pe_id, pe_lastname, pe_firstname, SUM(op_duration) AS sum_duration,
        SUM(case when (ph.phr_hourlyrate IS NOT NULL) then ph.phr_hourlyrate
        ELSE case when prjmem_hourlyrate IS NOT NULL THEN prjmem_hourlyrate ELSE phn.phr_hourlyrate END END * ';

    /* MySQL cannot cast to FLOAT */
    if (PHPAGA_DB_SYSTEM == 'mysql')
        $query .= '(op_duration/ 60))  AS amount ';
    else
        $query .= '(cast(op_duration AS FLOAT)/ 60))  AS amount ';

    $query .= 'FROM
        operations op
        LEFT JOIN project_members pm ON (pm.prj_id = op.prj_id AND pm.pe_id = op.pe_id)
        LEFT JOIN persons pe ON pe.pe_id = op.pe_id
        LEFT JOIN projects_hourlyrates ph ON (ph.opcat_id = op.opcat_id and ph.prj_id = op.prj_id AND ph.pe_id = op.pe_id)
        LEFT JOIN projects_hourlyrates phn ON (phn.opcat_id = op.opcat_id and phn.prj_id = op.prj_id AND phn.pe_id IS NULL)
        WHERE op_id IN ('.implode(', ', $opIds).')
        GROUP BY
        op.pe_id,
        pe_lastname,
        pe_firstname
        ORDER BY pe_lastname, pe_firstname ';

    $d = new PhPagaDbData;

    $r = $d->queryAll($query);

    if (PhPagaError::isError($r) || !count($r))
        return $r;

    foreach ($r as $qrow) {

        phpaga_sanitizearray($qrow);

        $qrow['amount'] = (float) $qrow['amount'];
        $qrow['pe_person'] = phpaga_persons_formatname($qrow['pe_lastname'], $qrow['pe_firstname']);

        $h_details[] = $qrow;

        $humancost['total_amount'] += $qrow['amount'];
        $humancost['total_duration'] += $qrow['sum_duration'];
    }

    $humancost['details'] = $h_details;

    return $humancost;
}


/**
 * Sanitizes an array of operation Ids. Only the items containing an
 * Id are taken as valid.
 *
 * @param array $opids       Array of operation ids
 *
 * @return array $san        Sanitized array of operation ids or
 *                           false on error
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_operations_sanitizeopids ($opids)
{
    $san = array();

    if (!is_array($opids))
        return false;

    foreach ($opids as $o => $v)
        if (is_numeric($v) && !isset($san[$v]))
            $san[$v] = $v;

    return $san;
}

?>