<?php
/**
 * The model file of action module of ZDOO.
 *
 * @copyright   Copyright 2009-2018 青岛易软天创网络科技有限公司(QingDao Nature Easy Soft Network Technology Co,LTD, www.cnezsoft.com)
 * @license     ZPL (http://zpl.pub/page/zplv12.html)
 * @author      Yidong Wang <yidong@cnezsoft.com>
 * @package     action
 * @version     $Id: model.php 5028 2013-07-06 02:59:41Z wyd621@gmail.com $
 * @link        http://www.zdoo.com
 */
?>
<?php
class actionModel extends model
{
    const BE_UNDELETED  = 0;    // The deleted object has been undeleted.
    const CAN_UNDELETED = 1;    // The deleted object can be undeleted.
    const BE_HIDDEN     = 2;    // The deleted object has been hidded.

    /**
     * Create an action.
     *
     * @param  string $objectType
     * @param  int    $objectID
     * @param  string $actionType
     * @param  string $comment
     * @param  mix    $extra        the extra info of this action, like customer, contact, order etc.  according to different modules and actions, can set different extra.
     * @param  string $actor
     * @param  int    $customer
     * @param  int    $contact
     * @access public
     * @return int
     */
    public function create($objectType, $objectID, $actionType, $comment = '', $extra = '', $actor = '', $customer = 0, $contact = 0)
    {
        $action = new stdclass();

        $action->objectType = strtolower($objectType);
        $action->objectID   = $objectID;
        $action->customer   = $customer;
        $action->contact    = $contact;
        $action->actor      = $actor ? $actor : $this->app->user->account;
        $action->action     = strtolower($actionType);
        $action->date       = helper::now();
        $action->comment    = trim(fixer::stripDataTags($comment));
        $action->extra      = $extra;

        /* Process action. */
        $action = $this->loadModel('file')->processImgURL($action, 'comment', $this->post->uid);

        /* If objectType is customer or contact, save objectID as customer id or contact id. */
        if($objectType == 'customer' || $objectType == 'provider') $action->customer = $objectID;
        if($objectType == 'contact')  $action->contact  = $objectID;

        $this->dao->insert(TABLE_ACTION)
            ->data($action, $skip = 'files,labels')
            ->batchCheckIF($actionType == 'record', 'contact, comment', 'notempty')
            ->exec();
        $actionID = $this->dao->lastInsertID();

        $this->loadModel('notify')->send($objectType, $objectID, $actionType, $actionID, $actor);

        return $actionID;
    }

    /**
     * Save a record of an order.
     *
     * @param  object    $order
     * @access public
     * @return void
     */
    public function createRecord($objectType, $objectID, $customer, $contact)
    {
        $actionID = $this->create($objectType, $objectID, $action = 'record', $this->post->comment, $this->post->date, $actor = null, $customer, $contact);

        if(!$actionID) return false;

        /* Create todo for user who contact the contacts. */
        if($objectType == 'order' or $objectType == 'customer')
        {
            $customerName = $this->dao->findById($customer)->from(TABLE_CUSTOMER)->fetch('name');

            $todo = new stdclass();
            $todo->account = $this->post->contactedBy;
            $todo->date    = $this->post->nextDate;
            $todo->name    = $customerName;
            $todo->begin   = '2400';
            $todo->end     = '2400';
            $todo->type    = $objectType;
            $todo->idvalue = $objectID;
            $todo->pri     = '3';
            $todo->status  = 'wait';

            $this->dao->insert(TABLE_TODO)->data($todo)->autoCheck()->exec();
        }

        $this->loadModel('file')->saveUpload('action', $actionID);

        $nextDate = $this->post->nextDate;
        $this->updateDating($objectType, $objectID, $customer, $contact, $actionID);

        /* Set the min next date as the post value. */
        if($this->post->nextDate) $nextDate = $this->getMinDatingDate($objectType, $objectID);
        $this->syncContactInfo($objectType, $objectID, $customer, $contact, $nextDate);

        return $actionID;
    }

    /**
     * Update dating.
     *
     * @param  string $objectType
     * @param  int    $objectID
     * @param  int    $customer
     * @param  int    $contact
     * @param  int    $action
     * @access public
     * @return bool
     */
    public function updateDating($objectType, $objectID, $customer, $contact, $action)
    {
        $dating = new stdclass();
        $dating->status     = 'done';
        $dating->editedBy   = $this->app->user->account;
        $dating->editedDate = helper::now();

        /* 本次联系人小于等于今天的下次联系记录全部完成。*/
        $this->dao->update(TABLE_DATING)->data($dating)
            ->where('objectType')->eq($objectType)
            ->andWhere('objectID')->eq($objectID)
            ->andWhere('date')->le(date('Y-m-d'))
            ->andWhere('account')->eq($this->app->user->account)
            ->andWhere('contact')->eq($this->post->contact)
            ->exec();

        if(!$this->post->nextDate) return false;

        $nextContact = $this->post->nextContact == 'ditto' ? $this->post->contact : ($this->post->nextContact ? $this->post->nextContact : 0);

        /* 下次联系人的大于今天的下次联系记录全部完成。*/
        $this->dao->update(TABLE_DATING)->data($dating)
            ->where('objectType')->eq($objectType)
            ->andWhere('objectID')->eq($objectID)
            ->andWhere('date')->gt(date('Y-m-d'))
            ->andWhere('account')->eq($this->app->user->account)
            ->andWhere('contact')->eq($nextContact)
            ->exec();

        $dating = new stdclass();
        $dating->objectType  = $objectType;
        $dating->objectID    = $objectID;
        $dating->action      = $action;
        $dating->customer    = $customer;
        $dating->contact     = $nextContact;
        $dating->account     = $this->post->contactedBy;
        $dating->date        = $this->post->nextDate;
        $dating->desc        = $this->post->desc;
        $dating->createdBy   = $this->app->user->account;
        $dating->createdDate = helper::now();

        /* 新增下次联系人的下次联系记录。*/
        $this->dao->insert(TABLE_DATING)->data($dating)->autoCheck()->exec();

        if(dao::isError()) return false;

        if(isset($this->config->action->datingTables[$objectType]))
        {
            $table  = $this->config->action->datingTables[$objectType];
            $object = $this->dao->select('*')->from($table)->where('id')->eq($objectID)->fetch();
            if($object)
            {
                $extra = $this->post->nextDate;
                if($this->post->nextContact && $objectType != 'contact' && $objectType != 'leads')
                {
                    $extra .= ' ' . $this->dao->select('realname')->from(TABLE_CONTACT)->where('id')->eq($nextContact)->fetch('realname');
                }
                $actionID = $this->create($objectType, $objectID, $action = 'dating', $this->post->desc, $extra, $actor = null, $customer, $contact);
                return !dao::isError();
            }
        }

        return false;
    }

    /**
     * Set next date of origin table.
     *
     * @param  string $objectType
     * @param  int    $objectID
     * @access public
     * @return bool
     */
    public function updateOriginTable($objectType, $objectID, $customer = 0)
    {
        $table    = $this->config->action->datingTables[$objectType];
        $nextDate = $this->getMinDatingDate($objectType, $objectID);
        $this->dao->update($table)->set('nextDate')->eq($nextDate)->where('id')->eq($objectID)->exec();

        if(($objectType == 'contract' || $objectType == 'order' || $objectType == 'contact') && $customer)
        {
            $nextDate = $this->getMinDatingDate('customer', $customer);
            $this->dao->update(TABLE_CUSTOMER)->set('nextDate')->eq($nextDate)->where('id')->eq($customer)->exec();
        }

        return !dao::isError();
    }

    /**
     * Sync contact info.
     *
     * @param  string $objectType
     * @param  int    $objectID
     * @param  int    $customer
     * @param  int    $contact
     * @param  string $nextDate
     * @access public
     * @return bool
     */
    public function syncContactInfo($objectType, $objectID, $customer, $contact, $nextDate)
    {
        $contactInfo['contactedDate'] = $this->post->date;
        $contactInfo['contactedBy']   = $this->app->user->account;
        $contactInfo['editedDate']    = helper::now();

        $this->dao->update(TABLE_CUSTOMER)->data($contactInfo)->where('id')->eq($customer)->andWhere('contactedDate')->lt($this->post->date)->exec();
        $this->dao->update(TABLE_CONTACT)->data($contactInfo)->where('id')->eq($contact)->andWhere('contactedDate')->lt($this->post->date)->exec();

        if($objectType == 'order')    $this->dao->update(TABLE_ORDER)->data($contactInfo)->where('id')->eq($objectID)->andWhere('contactedDate')->lt($this->post->date)->exec();
        if($objectType == 'contract') $this->dao->update(TABLE_CONTRACT)->data($contactInfo)->where('id')->eq($objectID)->andWhere('contactedDate')->lt($this->post->date)->exec();

        $table = $this->config->action->datingTables[$objectType];
        $this->dao->update($table)->set('nextDate')->eq($nextDate)->where('id')->eq($objectID)->exec();

        if($objectType == 'order' or $objectType == 'contract') $this->dao->update(TABLE_CUSTOMER)->set('nextDate')->eq($nextDate)->where('id')->eq($customer)->exec();

        return !dao::isError();
    }

    /**
     * Get actions of an object.
     *
     * @param  string $objectType
     * @param  int    $objectID
     * @param  string $action
     * @param  object $pager
     * @access public
     * @return array
     */
    public function getList($objectType, $objectID, $action = '', $pager = null, $origin = '')
    {
        $orderBy = $origin == '' ? 'id' : '`date`_desc';
        $actions = $this->dao->select('*')->from(TABLE_ACTION)
            ->where('1 = 1')
            ->beginIF($objectType == 'customer' || $objectType == 'provider')->andWhere('customer')->eq($objectID)->fi()
            ->beginIF($objectType == 'contact')->andWhere('contact')->eq($objectID)->fi()
            ->beginIF($objectType != 'customer' and $objectType != 'provider' and $objectType != 'contact')
              ->andWhere('objectType')->eq($objectType)
              ->andWhere('objectID')->eq($objectID)
            ->fi()
            ->beginIF($action)->andWhere('action')->eq($action)->fi()
            ->orderBy($orderBy)
            ->page($pager)
            ->fetchAll('id');

        $histories = $this->getHistory(array_keys($actions));
        $contacts  = $this->loadModel('contact', 'crm')->getPairs(0, '', false);
        $this->loadModel('file');

        foreach($actions as $actionID => $action)
        {
            $action->history = isset($histories[$actionID]) ? $histories[$actionID] : array();
            $action->files   = $this->file->getByObject('action', $actionID);
            if($action->action == 'record') $action->contact = isset($contacts[$action->contact]) ? $contacts[$action->contact] : '';
            $action = $this->file->replaceImgURL($action, 'comment');
            $actions[$actionID] = $action;
        }

        return $actions;
    }

    /**
     * Get an action record.
     *
     * @param  int    $actionID
     * @access public
     * @return object
     */
    public function getById($actionID)
    {
        $action = $this->dao->findById((int)$actionID)->from(TABLE_ACTION)->fetch();
        if(!$action) return false;
        $action->files = $this->loadModel('file')->getByObject('action', $actionID);
        return $action;
    }

    /**
     * Get deleted objects.
     *
     * @param  string    $type all|hidden
     * @param  string    $orderBy
     * @param  object    $pager
     * @access public
     * @return array
     */
    public function getTrashes($type, $orderBy, $pager)
    {
        $extra = $type == 'hidden' ? self::BE_HIDDEN : self::CAN_UNDELETED;
        $trashes = $this->dao->select('*')->from(TABLE_ACTION)
            ->where('action')->eq('deleted')
            ->andWhere('extra')->eq($extra)
            ->orderBy($orderBy)->page($pager)->fetchAll();
        if(!$trashes) return array();

        $this->app->loadLang('tree');
        $this->app->loadLang('user');
        $categoryTypes = $this->dao->select('DISTINCT type')->from(TABLE_CATEGORY)->fetchPairs();
        foreach($categoryTypes as $categoryType)
        {
            $objectType = $categoryType . '_category';
            $this->config->objectTables[$objectType]             = TABLE_CATEGORY;
            $this->config->action->objectNameFields[$objectType] = 'name';
            $this->config->action->objectAppNames[$objectType]   = 'sys';
            $this->lang->action->objectTypes[$objectType]        = isset($this->lang->$categoryType->common) ? $this->lang->$categoryType->common : $this->lang->tree->common;
        }

        /* Group trashes by objectType, and get there name field. */
        foreach($trashes as $object)
        {
            $object->objectType = str_replace('`', '', $object->objectType);
            $typeTrashes[$object->objectType][] = $object->objectID;
        }

        foreach($typeTrashes as $objectType => $objectIds)
        {
            $objectIds = array_unique($objectIds);
            $table     = $this->config->objectTables[$objectType];
            $field     = $this->config->action->objectNameFields[$objectType];

            if(!$table) continue;
            $objectNames[$objectType] = $this->dao->select("id, $field AS name")->from($table)->where('id')->in($objectIds)->fetchPairs();

            /* Get titles if objectType is order. */
            if($objectType == 'order')
            {
                $this->app->loadLang('order', 'crm');
                $orders = $this->loadModel('order', 'crm')->getByIdList($objectIds);
                foreach($orders as $order) $objectNames['order'][$order->id] = $order->title;
            }
        }

        /* Add name field to the trashes. */
        foreach($trashes as $trash) $trash->objectName = isset($objectNames[$trash->objectType][$trash->objectID]) ? $objectNames[$trash->objectType][$trash->objectID] : $trash->objectID;
        return $trashes;
    }

    /**
     * Get histories of an action.
     *
     * @param  int    $actionID
     * @access public
     * @return array
     */
    public function getHistory($actionID)
    {
        return $this->dao->select('*')->from(TABLE_HISTORY)->where('action')->in($actionID)->orderBy('id')->fetchGroup('action');
    }

    /**
     * Get dating by id.
     *
     * @param  int    $id
     * @access public
     * @return object
     */
    public function getDatingById($id)
    {
        return $this->dao->select('*')->from(TABLE_DATING)->where('id')->eq($id)->fetch();
    }

    /**
     * Get dating list by objectType and objectID.
     *
     * @param  string $objectType
     * @param  int    $objectID
     * @access public
     * @return void
     */
    public function getDatingList($objectType, $objectID)
    {
        return $this->dao->select('*')->from(TABLE_DATING)
            ->where('status')->eq('wait')

            ->beginIF($objectType == 'customer')
            ->andWhere('customer', true)->eq($objectID)
            ->orWhere('(objectType')->eq('customer')
            ->andWhere('objectID')->eq($objectID)
            ->markRight(2)
            ->fi()

            ->beginIF($objectType != 'customer')
            ->andWhere('objectType')->eq($objectType)
            ->andWhere('objectID')->eq($objectID)
            ->fi()

            ->orderBy('date, id')
            ->fetchAll();
    }

    /**
     * Get dating of this week.
     *
     * @param  string $account
     * @param  string $objectType
     * @access public
     * @return array
     */
    public function getDatingOfThisWeek($account = '', $objectType = '')
    {
        $thisWeek = date::getThisWeek();
        if($account == '') $account = $this->app->user->account;

        return $this->dao->select('*')
            ->from(TABLE_DATING)
            ->where('date')->between($thisWeek['begin'], $thisWeek['end'])
            ->andWhere('account')->eq($account)
            ->andWhere('objectType')->eq($objectType)
            ->andWhere('status')->eq('wait')
            ->fetchAll('objectID');
    }

    /**
     * Get min next date by objectType and objectID.
     *
     * @param  string $objectType
     * @param  int    $objectID
     * @access public
     * @return string
     */
    public function getMinDatingDate($objectType, $objectID)
    {
        return $this->dao->select('MIN(date) AS date')->from(TABLE_DATING)
            ->where('status')->eq('wait')

            ->beginIF($objectType == 'customer')
            ->andWhere('customer')->eq($objectID)
            ->fi()

            ->beginIF($objectType != 'customer')
            ->andWhere('objectType')->eq($objectType)
            ->andWhere('objectID')->eq($objectID)
            ->fi()

            ->fetch('date');
    }

    /**
     * Log histories for an action.
     *
     * @param  int    $actionID
     * @param  array  $changes
     * @access public
     * @return void
     */
    public function logHistory($actionID, $changes)
    {
        foreach($changes as $change)
        {
            $change['action'] = $actionID;
            $this->dao->insert(TABLE_HISTORY)->data($change)->exec();
        }
    }

    /**
     * Print actions of an object.
     *
     * @param  array    $action
     * @access public
     * @return void
     */
    public function printAction($action)
    {
        $objectType = $action->objectType;
        $actionType = strtolower($action->action);

        /**
         * Set the desc string of this action.
         *
         * 1. If the module of this action has defined desc of this actionType, use it.
         * 2. If no defined in the module language, search the common action define.
         * 3. If not found in the lang->action->desc, use the $lang->action->desc->common or $lang->action->desc->extra as the default.
         */
        if(isset($this->lang->$objectType->action->$actionType))
        {
            $desc = $this->lang->$objectType->action->$actionType;
        }
        elseif(isset($this->lang->action->desc->$actionType))
        {
            $desc = $this->lang->action->desc->$actionType;
        }
        else
        {
            $desc = $action->extra ? $this->lang->action->desc->extra : $this->lang->action->desc->common;
        }

        if($this->app->getViewType() == 'mhtml') $action->date = formatTime($action->date, DT_MONTHTIME1);

        if($this->app->getModuleName() == 'customer' && $actionType == 'dating')
        {
            if($objectType == 'order')
            {
                $desc = $this->lang->customer->action->orderDating;
                $action->order = html::a(helper::createLink('order', 'view', "orderID=" . $action->objectID), $action->objectID);
            }
            if($objectType == 'contract')
            {
                $contract = $this->loadModel('contract', 'crm')->getByID($action->objectID);
                $desc = $this->lang->customer->action->contractDating;
                $action->contract = html::a(helper::createLink('contract', 'view', "contractID=" . $action->objectID), $contract->name);
            }
        }

        /* Cycle actions, replace vars. */
        foreach($action as $key => $value)
        {
            if($key == 'history' or $key == 'files') continue;

            /* Desc can be an array or string. */
            if(is_array($desc))
            {
                if($key == 'extra') continue;
                $desc['main'] = str_replace('$' . $key, $value, $desc['main']);
            }
            else
            {
                $desc = str_replace('$' . $key, $value, $desc);
            }
        }

        /* If the desc is an array, process extra. Please bug/lang. */
        if(is_array($desc))
        {
            $extra = strtolower($action->extra);
            if(isset($desc['extra'][$extra]))
            {
                echo str_replace('$extra', $desc['extra'][$extra], $desc['main']);
            }
            else
            {
                echo str_replace('$extra', $action->extra, $desc['main']);
            }
        }
        else
        {
            echo $desc;
        }
    }

    /**
     * Get actions as dynamic.
     *
     * @param  string $account
     * @param  string $period
     * @param  string $orderBy
     * @param  object $pager
     * @access public
     * @return array
     */
    public function getDynamic($account = 'all', $period = 'all', $orderBy = 'date_desc', $pager = null, $date = '', $direction = 'next')
    {
        if($this->session->myQuery == false) $this->session->set('myQuery', ' 1 = 1');
        $myQuery = $this->loadModel('search')->replaceDynamic($this->session->myQuery);

        /* Computer the begin and end date of a period. */
        $begin = $end = '';
        if($period != 'bysearch')
        {
            $beginAndEnd = $this->computeBeginAndEnd($period);
            extract($beginAndEnd);
        }

        /* Get actions. */
        $actions = $this->dao->select('*')->from(TABLE_ACTION)
            ->where(1)
            ->andWhere('objectType')->notin('attend,refund,leave,overtime,lieu,makeup,trip,egress,action,usercontact')
            ->beginIF($period != 'bysearch' && $period  != 'all')->andWhere('date')->gt($begin)->fi()
            ->beginIF($period != 'bysearch' && $period  != 'all')->andWhere('date')->lt($end)->fi()
            ->beginIF($period != 'bysearch' && $account != 'all')->andWhere('actor')->eq($account)->fi()
            ->beginIF($period == 'bysearch')->andWhere($myQuery)->fi()
            ->beginIF($date)->andWhere('date' . ($direction == 'next' ? '<' : '>') . "'{$date}'")->fi()
            ->orderBy($orderBy)
            ->page($pager)
            ->fetchAll();

        $this->session->set('actionOrderBy', $orderBy);
        $this->session->set('actionQueryCondition', $this->dao->get());

        if(!$actions) return array();

        $actions = $this->transformActions($actions);

        foreach($actions as $key => $action)
        {
            if($action->action == 'moved2customerpool') unset($actions[$key]);
            if(!$this->checkPriv($action)) unset($actions[$key]);
        }

        return $actions;
    }

    /**
     * Transform the actions for display.
     *
     * @param  int    $actions
     * @access public
     * @return void
     */
    public function transformActions($actions)
    {
        /* Group actions by objectType, and get there name field. */
        foreach($actions as $object) $objectTypes[$object->objectType][] = $object->objectID;
        $origins = array();
        foreach($objectTypes as $objectType => $objectIds)
        {
            if(!isset($this->config->objectTables[$objectType])) continue;    // If no defination for this type, omit it.

            $objectIds = array_unique($objectIds);
            $table     = $this->config->objectTables[$objectType];
            $field     = $this->config->action->objectNameFields[$objectType];
            $origins[$objectType] = array();
            if(in_array($table, $this->config->action->needOriginTables))
            {
                $datas = $this->dao->select("id, $field AS name, createdBy")->from($table)->where('id')->in($objectIds)->fetchAll('id');
                foreach($datas as $id => $data)
                {
                    $objectNames[$objectType][$id] = $data->name;
                    $origins[$objectType][$id]     = $data->createdBy;
                }
            }
            elseif($table != TABLE_TODO and $table != TABLE_TRADE)
            {
                $objectNames[$objectType] = $this->dao->select("id, $field AS name")->from($table)->where('id')->in($objectIds)->fetchPairs();
                if($objectType == 'order')
                {
                    $orders = $this->loadModel('order', 'crm')->getByIdList($objectIds);
                    foreach($orders as $order) $objectNames[$objectType][$order->id] = $order->title;
                }
            }
            elseif($table == TABLE_TODO)
            {
                $todos = $this->dao->select("id, $field AS name, account, private, type, idvalue")->from($table)->where('id')->in($objectIds)->fetchAll('id');
                foreach($todos as $id => $todo)
                {
                    if($todo->type == 'task') $todo->name = $this->dao->findById($todo->idvalue)->from(TABLE_TASK)->fetch('name');
                    if($todo->type == 'customer') $todo->name = $this->dao->findById($todo->idvalue)->from(TABLE_CUSTOMER)->fetch('name');
                    if($todo->type == 'order')
                    {
                        $order = $this->dao->select('c.name, o.createdDate')
                            ->from(TABLE_ORDER)->alias('o')
                            ->leftJoin(TABLE_CUSTOMER)->alias('c')->on('o.customer=c.id')
                            ->where('o.id')->eq($todo->idvalue)
                            ->fetch();
                        $todo->name = $order->name . '|' . date('Y-m-d', strtotime($order->createdDate));
                    }
                    if(isset($this->lang->action->objectTypes[$todo->type])) $todo->name = $this->lang->action->objectTypes[$todo->type] . ':' . $todo->name;

                    if($todo->private == 1 and $todo->account != $this->app->user->account)
                    {
                       $objectNames[$objectType][$id] = $this->lang->todo->thisIsPrivate;
                    }
                    else
                    {
                       $objectNames[$objectType][$id] = $todo->name;
                    }
                }
            }
            else
            {
                $this->app->loadLang('trade', 'cash');
                $trades = $this->dao->select("id, type, money, currency")->from($table)->where('id')->in($objectIds)->fetchAll('id');
                foreach($trades as $id => $trade)
                {
                    $objectNames[$objectType][$id] = zget($this->lang->trade->typeList, $trade->type, '') . zget($this->lang->currencySymbols, $trade->currency, '') . $trade->money;
                }
            }
        }
        $objectNames['user'][0] = 'guest';    // Add guest account.

        foreach($actions as $action)
        {
            if($action->actor == 'system') $action->actor = $this->lang->action->system;

            /* Add name field to the actions. */
            $action->objectName = isset($objectNames[$action->objectType][$action->objectID]) ? $objectNames[$action->objectType][$action->objectID] : '';

            $actionType = strtolower($action->action);
            $objectType = strtolower($action->objectType);
            $action->originalDate = $action->date;
            $action->date         = formatTime($action->date, DT_MONTHTIME2);
            $action->actionLabel  = isset($this->lang->action->label->$actionType) ? $this->lang->action->label->$actionType : $action->action;
            $action->objectLabel  = $objectType;
            if(isset($this->lang->action->label->$objectType))
            {
                $objectLabel = $this->lang->action->label->$objectType;
                if(is_array($objectLabel))
                {
                    if($actionType == 'reviewed' or $actionType == 'reviewedBack')
                    {
                        if(isset($origins[$action->objectType][$action->objectID]) && $origins[$action->objectType][$action->objectID] == $this->app->user->account)
                        {
                            $action->objectLabel = $objectLabel[$actionType];
                        }
                        else
                        {
                            $action->objectLabel = $objectLabel['common'];
                        }
                    }
                    else
                    {
                        if(isset($objectLabel['common']))    $action->objectLabel = $objectLabel['common'];
                        if(isset($objectLabel[$actionType])) $action->objectLabel = $objectLabel[$actionType];
                    }
                }
                else
                {
                    $action->objectLabel = $objectLabel;
                }
            }

            /* app name. */
            $action->appName = '';
            if(isset($this->config->action->objectAppNames[$objectType])) $action->appName = $this->config->action->objectAppNames[$objectType];

            /* Open object by modal or not. */
            $action->toggle = '';
            if(strpos($this->config->action->objectModalLinks, ",{$objectType},") !== false or strpos($this->config->action->actionModalLinks, ",{$action->action},") !== false) $action->toggle = 'data-toggle = "modal"';

            /* Other actions, create a link. */
            if(strpos($action->objectLabel, '|') !== false)
            {
                list($objectLabel, $moduleName, $methodName, $vars) = explode('|', $action->objectLabel);
                $vars = empty($vars) ? '' : sprintf($vars, $action->objectID);
                if(!empty($action->appName)) $moduleName = "{$action->appName}.{$moduleName}";
                $action->objectLink  = helper::createLink($moduleName, $methodName, $vars);
                $action->objectLabel = $objectLabel;
            }
            else
            {
                $action->objectLink = '';
            }

            $action->major = (isset($this->config->action->majorList[$action->objectType]) && in_array($action->action, $this->config->action->majorList[$action->objectType])) ? 1 : 0;
        }

        return $actions;
    }

    /**
     * Print changes of every action.
     *
     * @param  string    $objectType
     * @param  array     $histories
     * @param  string    $action
     * @access public
     * @return void
     */
    public function printChanges($objectType, $histories, $action)
    {
        if(empty($histories)) return;

        $maxLength            = 0;          // The max length of fields names.
        $historiesWithDiff    = array();    // To save histories without diff info.
        $historiesWithoutDiff = array();    // To save histories with diff info.

        /* Diff histories by hasing diff info or not. Thus we can to make sure the field with diff show at last. */
        foreach($histories as $history)
        {
            if($history->field == 'assignedTo')
            {
                $users = $this->loadModel('user')->getPairs();
                $history->old = $users[$history->old];
                $history->new = $users[$history->new];
            }

            $fieldName = $history->field;
            $history->fieldLabel = isset($this->lang->$objectType->$fieldName) ? $this->lang->$objectType->$fieldName : $fieldName;
            if(isset($this->config->action->actionModules[$action]))
            {
                $module = $this->config->action->actionModules[$action];
                $history->fieldLabel = isset($this->lang->$module->$fieldName) ? $this->lang->$module->$fieldName : $fieldName;
            }
            if($objectType == 'contact') $history->fieldLabel = isset($this->lang->contact->$fieldName) ? $this->lang->contact->$fieldName : (isset($this->lang->resume->$fieldName) ? $this->lang->resume->$fieldName : $fieldName);
            if(!is_string($history->fieldLabel)) $history->fieldLabel = $fieldName;
            if(($length = strlen($history->fieldLabel)) > $maxLength) $maxLength = $length;
            $history->diff ? $historiesWithDiff[] = $history : $historiesWithoutDiff[] = $history;
        }
        $histories = array_merge($historiesWithoutDiff, $historiesWithDiff);

        foreach($histories as $history)
        {
            $history->fieldLabel = str_pad($history->fieldLabel, $maxLength, $this->lang->action->label->space);
            if($history->diff != '')
            {
                $history->diff      = str_replace(array('<ins>', '</ins>', '<del>', '</del>'), array('[ins]', '[/ins]', '[del]', '[/del]'), $history->diff);
                $history->diff      = ($history->field != 'subversion' and $history->field != 'git') ? htmlspecialchars($history->diff) : $history->diff;   // Keep the diff link.
                $history->diff      = str_replace(array('[ins]', '[/ins]', '[del]', '[/del]'), array('<ins>', '</ins>', '<del>', '</del>'), $history->diff);
                $history->diff      = nl2br($history->diff);
                $history->noTagDiff = preg_replace('/&lt;\/?([a-z][a-z0-9]*)[^\/]*\/?&gt;/Ui', '', $history->diff);
                printf($this->lang->action->desc->diff2, $history->fieldLabel, $history->noTagDiff, $history->diff);
            }
            else
            {
                printf($this->lang->action->desc->diff1, $history->fieldLabel, $history->old, $history->new);
            }
        }
    }

    /**
     * Undelete a record.
     *
     * @param  object $action
     * @param  string $table
     * @access public
     * @return bool
     */
    public function undelete($action, $table)
    {
        /* Update deleted field in object table. */
        $this->dao->update($table)->set('deleted')->eq(0)->where('id')->eq($action->objectID)->exec();
        if($action->objectType == 'project' && !dao::isError())
        {
            $this->dao->update(TABLE_TASK)->set('deleted')->eq('0')->where('project')->eq($action->objectID)->exec();
            $this->dao->update(TABLE_DOCLIB)->set('deleted')->eq('0')->where('project')->eq($action->objectID)->exec();
        }

        /* Update action record in action table. */
        $this->dao->update(TABLE_ACTION)->set('extra')->eq(ACTIONMODEL::BE_UNDELETED)->where('id')->eq($action->id)->exec();
        $this->action->create($action->objectType, $action->objectID, 'undeleted');

        return !dao::isError();
    }

    /**
     * Update an action.
     *
     * @param  object    $action
     * @param  int       $actionID
     * @access public
     * @return void
     */
    public function update($action, $actionID)
    {
        $this->dao->update(TABLE_ACTION)->data($action, $skip = 'referer')->autoCheck()->where('id')->eq($actionID)->exec();
        return !dao::isError();
    }

    /**
     * Update comment of a action.
     *
     * @param  int    $actionID
     * @param  string $comment
     * @access public
     * @return bool
     */
    public function updateComment($actionID, $comment)
    {
        $action = new stdclass();
        $action->comment = $comment;

        /* Process action. */
        $action = $this->loadModel('file')->processImgURL($action, 'comment', $this->post->uid);

        $this->dao->update(TABLE_ACTION)
            ->set('date')->eq(helper::now())
            ->set('comment')->eq($action->comment)
            ->where('id')->eq($actionID)
            ->exec();

        return !dao::isError();
    }

    /**
     * Hide an object.
     *
     * @param  int    $actionID
     * @access public
     * @return void
     */
    public function hideOne($actionID)
    {
        $action = $this->getById($actionID);
        if($action->action != 'deleted') return;

        $this->dao->update(TABLE_ACTION)->set('extra')->eq(self::BE_HIDDEN)->where('id')->eq($actionID)->exec();
        $this->create($action->objectType, $action->objectID, 'hidden');
    }

    /**
     * Hide all deleted objects.
     *
     * @access public
     * @return void
     */
    public function hideAll()
    {
        $this->dao->update(TABLE_ACTION)
            ->set('extra')->eq(self::BE_HIDDEN)
            ->where('action')->eq('deleted')
            ->andWhere('extra')->eq(self::CAN_UNDELETED)
            ->exec();
    }

    /**
     * update a action read status to read.
     *
     * @param  int    $actionID
     * @param  string $type         action 系统通知 | order  过期订单
     * @access public
     * @return bool
     */
    public function read($actionID, $type = 'action')
    {
        /* Save read status to session if type isn't action. */
        if($type != 'action')
        {
            if(!isset($this->app->user->readNotices)) $this->app->user->readNotices = array();
            $this->app->user->readNotices[$actionID] = $actionID;
            return true;
        }

        /* Delete the reader. */
        $this->dao->delete()->from(TABLE_ACTIONREADER)->where('action')->eq($actionID)->andWhere('account')->eq($this->app->user->account)->exec();

        return !dao::isError();
    }

    /**
     * Build date group for actions.
     *
     * @param  array   $actions
     * @param  string  $direction
     * @access public
     * @return array
     */
    public function buildDateGroup($actions, $direction = 'next')
    {
        $dateGroup = array();
        foreach($actions as $action)
        {
            $timeStamp    = strtotime(isset($action->originalDate) ? $action->originalDate : $action->date);
            $date         = $this->app->getClientLang() == 'en' ? date('M d', $timeStamp) : date(DT_DATE4, $timeStamp);
            $action->time = date(DT_TIME2, $timeStamp);
            $dateGroup[$date][] = $action;
        }

        if($dateGroup)
        {
            $queryCondition = explode('WHERE', $this->session->actionQueryCondition);
            $queryCondition = isset($queryCondition[1]) ? $queryCondition[1] : '';
            if($queryCondition)
            {
                $queryCondition = explode('ORDER', $queryCondition);
                $queryCondition = $queryCondition[0];
            }

            $lastDateActions = $this->dao->select('*')->from(TABLE_ACTION)->where($queryCondition)->andWhere('`date`')->like(substr($action->originalDate, 0, 10) . '%')->orderBy($this->session->actionOrderBy)->fetchAll('id');

            foreach($lastDateActions as $key => $lastDateAction)
            {
                if($lastDateAction->action == 'moved2customerpool') unset($lastDateActions[$key]);

                $objectType = strtolower($lastDateAction->objectType);
                $lastDateAction->appName = isset($this->config->action->objectAppNames[$objectType]) ? $this->config->action->objectAppNames[$objectType] : '';

                if(!$this->checkPriv($lastDateAction)) unset($lastDateActions[$key]);
            }

            if(count($dateGroup[$date]) < count($lastDateActions))
            {
                unset($dateGroup[$date]);
                $lastDateActions = $this->transformActions($lastDateActions);
                foreach($lastDateActions as $action)
                {
                    $timeStamp    = strtotime(isset($action->originalDate) ? $action->originalDate : $action->date);
                    $date         = date(DT_DATE4, $timeStamp);
                    $action->time = date(DT_TIME2, $timeStamp);
                    $dateGroup[$date][] = $action;
                }
            }
        }

        if($direction != 'next') $dateGroup = array_reverse($dateGroup);
        return $dateGroup;
    }

    /**
     * Check Has pre or next.
     *
     * @param  string $date
     * @param  string $direction
     * @access public
     * @return bool
     */
    public function hasPreOrNext($date, $direction = 'next')
    {
        $queryCondition = explode('WHERE', $this->session->actionQueryCondition);
        $queryCondition = isset($queryCondition[1]) ? $queryCondition[1] : '';
        if($queryCondition)
        {
            $queryCondition = explode('ORDER', $queryCondition);
            $queryCondition = $queryCondition[0];
        }

        /* Remove date condition for direction. */
        $queryCondition = preg_replace("/AND +date[\<\>]'\d{4}\-\d{2}\-\d{2}'/", '', $queryCondition);
        $count = $this->dao->select('count(*) as count')->from(TABLE_ACTION)->where($queryCondition)
            ->andWhere('date' . ($direction == 'next' ? '<' : '>') . "'{$date}'")
            ->fetch('count');
        return $count > 0;
    }

    /**
     * Send notice to user. return failed user account.
     *
     * @param  int    $actionID
     * @param  string $reader
     * @param  bool   $onlyNotice       Only send notice to the online users.
     * @access public
     * @return string
     */
    public function sendNotice($actionID, $reader, $onlyNotice = false)
    {
        $readers = is_array($reader) ? $reader : explode(',', trim($reader, ','));
        $failedReaders = array();

        foreach($readers as $key => $account) if($account == '' or $account == $this->app->user->account) unset($readers[$key]);
        foreach($readers as $key => $account)
        {
            if(!$onlyNotice and !$this->loadModel('user')->isOnline($account))
            {
                unset($readers[$key]);
                $failedReaders[] = $account;
            }
        }

        if(!empty($readers))
        {
            /* Save the online users to the reader table. */
            $data = new stdclass();
            $data->action = $actionID;

            foreach($readers as $account)
            {
                $data->account = $account;

                $this->dao->replace(TABLE_ACTIONREADER)->data($data)->exec();
            }
        }

        return join(',', $failedReaders);
    }

    /**
     * Get unread notice for user.
     *
     * @param  string $skipNotice
     * @access public
     * @return array
     */
    public function getUnreadNotice($skipNotice = '')
    {
        $users   = array();
        $actions = array();
        $account = $this->app->user->account;

        $idList = $this->dao->select('action')->from(TABLE_ACTIONREADER)->where('account')->eq($account)->fetchPairs();
        if($idList) $actions = $this->dao->select('*')->from(TABLE_ACTION)->where('id')->in($idList)->fetchAll('id');

        if($actions)
        {
            $users   = $this->loadModel('user')->getPairs();
            $actions = $this->transformActions($actions);
        }

        /* Create action notices. */
        $notices = array();
        foreach($actions as $action)
        {
            $notice = new stdclass();
            $notice->id      = $action->id;
            $notice->title   = $action->objectLabel;
            $notice->content = '<p>' . sprintf($this->lang->action->noticeTitle, $action->objectLink, $action->appName, $action->toggle, $action->objectName) . '</p>';
            $notice->app     = 'action';
            $notice->read    = helper::createLink('action', 'read', "actionID={$notice->id}");

            /* process user and status. */
            if($action->objectType == 'leave')    $this->loadModel('leave', 'oa');
            if($action->objectType == 'overtime') $this->loadModel('overtime', 'oa');
            if($action->objectType == 'egress')   $this->loadModel('egress', 'oa');
            if($action->objectType == 'attend')   $this->loadModel('attend', 'oa');
            if($action->objectType == 'refund')   $this->loadModel('refund', 'oa');
            if(isset($users[$action->actor])) $action->actor = $users[$action->actor];
            if($action->action == 'assigned' and isset($users[$action->extra])) $action->extra = $users[$action->extra];
            if($action->action == 'reviewed' and isset($this->lang->{$action->objectType}->statusList[$action->extra])) $action->extra = $this->lang->{$action->objectType}->statusList[$action->extra];
            if($action->action == 'reviewed' and isset($this->lang->{$action->objectType}->reviewStatusList[$action->extra])) $action->extra = $this->lang->{$action->objectType}->reviewStatusList[$action->extra];

            /* Get contents. */
            ob_start();
            $this->printAction($action);
            $notice->content .= '<p>' . ob_get_contents() . '</p>';
            ob_end_clean();

            $notices[$action->id] = $notice;
        }

        /* Create todo notices. */
        $interval = $this->config->pingInterval;
        $now      = helper::now();
        $link     = helper::createLink('sys.todo', 'calendar');
        $todos    = $this->loadModel('todo')->getList('self', $account, date(DT_DATE1), 'undone');
        if($todos)
        {
            $begins[1]  = date('Hi', strtotime($now));
            $ends[1]    = date('Hi', strtotime("+$interval seconds $now"));
            $begins[10] = date('Hi', strtotime("+10 minute $now"));
            $ends[10]   = date('Hi', strtotime("+10 minute $interval seconds $now"));
            $begins[30] = date('Hi', strtotime("+30 minute $now"));
            $ends[30]   = date('Hi', strtotime("+30 minute $interval seconds $now"));
            foreach($todos as $todo)
            {
                if(empty($todo->begin)) continue;
                $time = str_replace(':', '', $todo->begin);

                $lastTime = 0;
                if((int)$time > (int)$begins[1]  and (int)$time <= (int)$ends[1])  $lastTime = 1;
                if((int)$time > (int)$begins[10] and (int)$time <= (int)$ends[10]) $lastTime = 10;
                if((int)$time > (int)$begins[30] and (int)$time <= (int)$ends[30]) $lastTime = 30;
                if($lastTime)
                {
                    $notice = new stdclass();
                    $notice->id      = 'todo' . $todo->id;
                    $notice->title   = $this->lang->todo->common;
                    $notice->content = sprintf($this->lang->action->noticeTitle, $link, 'dashboard', "{$todo->begin} {$todo->name}");
                    $notice->app     = 'todo';
                    $notice->read    = '';

                    $notices[$notice->id] = $notice;
                }
            }
        }
        else
        {
            $this->app->loadModuleConfig('attend', 'oa');
            $signInLimit = date('Y-m-d ') . $this->config->attend->signInLimit;
            $begin = (int)date('Hi', strtotime("+30 minute $signInLimit"));
            $end   = (int)date('Hi', strtotime("+30 minute $interval seconds $signInLimit"));
            if((int)date('Hi') >= $begin and (int)date('Hi') <= $end)
            {
                $notice = new stdclass();
                $notice->id      = "emptyTodo";
                $notice->title   = $this->lang->todo->common;
                $notice->content = sprintf($this->lang->action->noticeTitle, $link, 'oa', "{$this->lang->todo->emptyTodo}");
                $notice->app     = 'oa';
                $notice->read    = '';

                $notices[$notice->id] = $notice;
            }
        }

        /* Create past order notice. */
        $orders = $this->loadModel('order', 'crm')->getList('past', '', '', $needQueryCondition = false);
        foreach($orders as $order)
        {
            /* Skip not assigned to me, read and showed notice. */
            if($order->assignedTo != $account) continue;
            if(isset($this->app->user->readNotices["order{$order->id}"])) continue;
            if(strpos(",$skipNotice,", ",order{$order->id},") !== false) continue;

            $link = helper::createLink('crm.order', 'view', "orderID=$order->id");
            $notice = new stdclass();
            $notice->id      = 'order' . $order->id;
            $notice->title   = $this->lang->order->record . $this->lang->order->common;
            $notice->content = sprintf($this->lang->action->noticeTitle, $link, 'crm', $order->title);
            $notice->app     = 'crm';
            $notice->read    = helper::createLink('action', 'read', "actionID={$notice->id}&type=order");

            $notices[$notice->id] = $notice;
        }

        return $notices;
    }

    /**
     * Compute the begin date and end date of a period.
     *
     * @param  string    $period   all|today|yesterday|twodaysago|latest2days|thisweek|lastweek|thismonth|lastmonth
     * @access public
     * @return array
     */
    public function computeBeginAndEnd($period)
    {
        $this->app->loadClass('date');

        $today      = date::today();
        $tomorrow   = date::tomorrow();
        $yesterday  = date::yesterday();
        $twoDaysAgo = date::twoDaysAgo();

        $period = strtolower($period);

        if($period == 'all')        return array('begin' => '1970-1-1',  'end' => '2109-1-1');
        if($period == 'today')      return array('begin' => $today,      'end' => $tomorrow);
        if($period == 'yesterday')  return array('begin' => $yesterday,  'end' => $today);
        if($period == 'twodaysago') return array('begin' => $twoDaysAgo, 'end' => $yesterday);
        if($period == 'latest3days')return array('begin' => $twoDaysAgo, 'end' => $tomorrow);

        /* If the period is by week, add the end time to the end date. */
        if($period == 'thisweek' or $period == 'lastweek')
        {
            $func = "get$period";
            extract(date::$func());
            return array('begin' => $begin, 'end' => $end . ' 23:59:59');
        }

        if($period == 'thismonth')  return date::getThisMonth();
        if($period == 'lastmonth')  return date::getLastMonth();
    }

    /**
     * Check privilege for action.
     *
     * @param  object    $action
     * @access public
     * @return bool
     */
    public function checkPriv($action)
    {
        if(!commonModel::hasAppPriv($action->appName)) return false;

        if($action->customer)
        {
            static $customers = array();
            if(empty($customers)) $customers = $this->loadModel('customer')->getCustomersSawByMe();
            if(!in_array($action->customer, $customers)) return false;
        }

        if($action->contact)
        {
            static $contacts = array();
            if(empty($contacts)) $contacts = $this->loadModel('contact', 'crm')->getContactsSawByMe();
            if(!in_array($action->contact, $contacts)) return false;
        }

        if($action->objectType == 'order')
        {
            static $orders = array();
            if(empty($orders)) $orders = $this->loadModel('order', 'crm')->getOrdersSawByMe();
            if(!in_array($action->objectID, $orders)) return false;
        }

        if($action->objectType == 'project')
        {
            if(!($this->loadModel('project', 'proj')->checkPriv($action->objectID))) return false;
        }

        if($action->objectType == 'task')
        {
            $task = $this->loadModel('task')->getByID($action->objectID);
            if(!($this->loadModel('task')->checkPriv($task, 'view'))) return false;
        }

        if($action->objectType == 'trade')
        {
            $trade = $this->loadModel('trade', 'cash')->getByID($action->objectID);
            $rights = $this->app->user->rights;
            if(empty($trade) or ($this->app->user->admin != 'super' and $trade->type == 'out' and (!isset($rights['tradebrowse']['out']) or !$this->loadModel('tree')->hasRight($trade->category)))) return false;
        }

        if($action->objectType == 'todo')
        {
            $todo = $this->loadModel('todo')->getByID($action->objectID);
            if(empty($todo) or ($this->app->user->account != $todo->account && $this->app->user->account != $todo->assignedTo)) return false;
        }

        $canView = false;

        if($action->objectType == 'doc')
        {
            $doc     = $this->loadModel('doc', 'doc')->getById($action->objectID);
            $canView = $this->doc->hasRight($doc);
        }

        if($action->objectType == 'doclib')
        {
            $lib     = $this->loadModel('doc', 'doc')->getLibById($action->objectID);
            $canView = $this->doc->hasRight($lib);
        }

        $objectType = $action->objectType;
        $actionType = $action->action;
        if(isset($this->lang->action->label->$objectType))
        {
            $objectLabel = $this->lang->action->label->$objectType;

            if(is_array($objectLabel)) $objectLabel = zget($objectLabel, $actionType, zget($objectLabel, 'common', ''));

            if(is_string($objectLabel) && strpos($objectLabel, '|') !== false)
            {
                list($label, $moduleName, $methodName, $vars) = explode('|', $objectLabel);
                if($this->loadModel('common')->isOpenMethod($moduleName, $methodName) || commonModel::hasPriv($moduleName, $methodName)) $canView = true;
            }
        }

        return $canView;
    }

    /**
     * Check if the user assigned to next dating has privilege.
     *
     * @param  string $objectType
     * @param  int    $objectID
     * @access public
     * @return bool
     */
    public function checkDatingPrivilege($objectType, $objectID)
    {
        $user = $this->loadModel('user')->getByAccount($this->post->contactedBy);
        $user->rights = $this->user->authorize($user);

        if($user->admin != 'super' && !isset($user->rights['apppriv']['crm'])) return false;

        if($objectType == 'order')
        {
            $orderList = $this->loadModel('order', 'crm')->getOrdersSawByMe($type = 'view', (array)$objectID, $user);
            if(empty($orderList)) return false;
        }
        if($objectType == 'contract')
        {
            $contractList = $this->loadModel('contract', 'crm')->getContractsSawByMe($type = 'view', (array)$objectID, $user);
            if(empty($contractList)) return false;
        }
        if($objectType == 'customer' or $objectType == 'provider')
        {
            $customerList = $this->loadModel('customer')->getCustomersSawByMe($type = 'view', (array)$objectID, $user);
            if(empty($customerList)) return false;
        }
        if($objectType == 'contact' or $objectType == 'leads')
        {
            $contactList = $this->loadModel('contact', 'crm')->getContactsSawByMe($type = 'view', (array)$objectID, $user);
            if(empty($contactList)) return false;
        }

        return true;
    }
}
