584 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			584 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * ---------------------------------------------------------------------
 | |
|  * GLPI - Gestionnaire Libre de Parc Informatique
 | |
|  * Copyright (C) 2015-2020 Teclib' and contributors.
 | |
|  *
 | |
|  * http://glpi-project.org
 | |
|  *
 | |
|  * based on GLPI - Gestionnaire Libre de Parc Informatique
 | |
|  * Copyright (C) 2003-2014 by the INDEPNET Development Team.
 | |
|  *
 | |
|  * ---------------------------------------------------------------------
 | |
|  *
 | |
|  * LICENSE
 | |
|  *
 | |
|  * This file is part of GLPI.
 | |
|  *
 | |
|  * GLPI is free software; you can redistribute it and/or modify
 | |
|  * it under the terms of the GNU General Public License as published by
 | |
|  * the Free Software Foundation; either version 2 of the License, or
 | |
|  * (at your option) any later version.
 | |
|  *
 | |
|  * GLPI is distributed in the hope that it will be useful,
 | |
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
|  * GNU General Public License for more details.
 | |
|  *
 | |
|  * You should have received a copy of the GNU General Public License
 | |
|  * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
 | |
|  * ---------------------------------------------------------------------
 | |
|  */
 | |
| 
 | |
| if (!defined('GLPI_ROOT')) {
 | |
|    die("Sorry. You can't access this file directly");
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Ticket Recurrent class
 | |
|  *
 | |
|  * @since 0.83
 | |
| **/
 | |
| class TicketRecurrent extends CommonDropdown {
 | |
| 
 | |
|    // From CommonDBTM
 | |
|    public $dohistory              = true;
 | |
| 
 | |
|    // From CommonDropdown
 | |
|    public $first_level_menu       = "helpdesk";
 | |
|    public $second_level_menu      = "ticketrecurrent";
 | |
| 
 | |
|    public $display_dropdowntitle  = false;
 | |
| 
 | |
|    static $rightname              = 'ticketrecurrent';
 | |
| 
 | |
|    public $can_be_translated      = false;
 | |
| 
 | |
| 
 | |
| 
 | |
|    static function getTypeName($nb = 0) {
 | |
|       return __('Recurrent tickets');
 | |
|    }
 | |
| 
 | |
| 
 | |
|    static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) {
 | |
| 
 | |
|       switch ($item->getType()) {
 | |
|          case 'TicketRecurrent' :
 | |
|             switch ($tabnum) {
 | |
|                case 1 :
 | |
|                   $item->showInfos();
 | |
|                   return true;
 | |
|             }
 | |
|             break;
 | |
|       }
 | |
|       return false;
 | |
|    }
 | |
| 
 | |
| 
 | |
|    function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) {
 | |
| 
 | |
|       if (Session::haveRight('itiltemplate', READ)) {
 | |
|          switch ($item->getType()) {
 | |
|             case 'TicketRecurrent' :
 | |
|                $ong[1] = _n('Information', 'Information', Session::getPluralNumber());
 | |
|                return $ong;
 | |
|          }
 | |
|       }
 | |
|       return '';
 | |
|    }
 | |
| 
 | |
| 
 | |
|    function defineTabs($options = []) {
 | |
| 
 | |
|       $ong = [];
 | |
|       $this->addDefaultFormTab($ong);
 | |
|       $this->addStandardTab(__CLASS__, $ong, $options);
 | |
|       $this->addStandardTab('Log', $ong, $options);
 | |
| 
 | |
|       return $ong;
 | |
|    }
 | |
| 
 | |
| 
 | |
|    function prepareInputForAdd($input) {
 | |
| 
 | |
|       $input['next_creation_date'] = $this->computeNextCreationDate($input['begin_date'],
 | |
|                                                                     $input['end_date'],
 | |
|                                                                     $input['periodicity'],
 | |
|                                                                     $input['create_before'],
 | |
|                                                                     $input['calendars_id']);
 | |
|       return $input;
 | |
|    }
 | |
| 
 | |
| 
 | |
|    function prepareInputForUpdate($input) {
 | |
| 
 | |
|       if (isset($input['begin_date'])
 | |
|           && isset($input['periodicity'])
 | |
|           && isset($input['create_before'])) {
 | |
| 
 | |
|          $input['next_creation_date'] = $this->computeNextCreationDate($input['begin_date'],
 | |
|                                                                        $input['end_date'],
 | |
|                                                                        $input['periodicity'],
 | |
|                                                                        $input['create_before'],
 | |
|                                                                        $input['calendars_id']);
 | |
|       }
 | |
|       return $input;
 | |
|    }
 | |
| 
 | |
| 
 | |
|    /**
 | |
|     * Return Additional Fileds for this type
 | |
|    **/
 | |
|    function getAdditionalFields() {
 | |
| 
 | |
|       return [['name'  => 'is_active',
 | |
|                          'label' => __('Active'),
 | |
|                          'type'  => 'bool',
 | |
|                          'list'  => false],
 | |
|                    ['name'  => 'tickettemplates_id',
 | |
|                          'label' => _n('Ticket template', 'Ticket templates', 1),
 | |
|                          'type'  => 'dropdownValue',
 | |
|                          'list'  => true],
 | |
|                    ['name'  => 'begin_date',
 | |
|                          'label' => __('Start date'),
 | |
|                          'type'  => 'datetime',
 | |
|                          'list'  => false],
 | |
|                    ['name'  => 'end_date',
 | |
|                          'label' => __('End date'),
 | |
|                          'type'  => 'datetime',
 | |
|                          'list'  => false],
 | |
|                    ['name'  => 'periodicity',
 | |
|                          'label' => __('Periodicity'),
 | |
|                          'type'  => 'specific_timestamp',
 | |
|                          'min'   => DAY_TIMESTAMP,
 | |
|                          'step'  => DAY_TIMESTAMP,
 | |
|                          'max'   => 2*MONTH_TIMESTAMP],
 | |
|                    ['name'  => 'create_before',
 | |
|                          'label' => __('Preliminary creation'),
 | |
|                          'type'  => 'timestamp',
 | |
|                          'max'   => 7*DAY_TIMESTAMP,
 | |
|                          'step'  => HOUR_TIMESTAMP],
 | |
|                    ['name'  => 'calendars_id',
 | |
|                          'label' => _n('Calendar', 'Calendars', 1),
 | |
|                          'type'  => 'dropdownValue',
 | |
|                          'list'  => true],
 | |
|                   ];
 | |
|    }
 | |
| 
 | |
| 
 | |
|    /**
 | |
|     * @since 0.83.1
 | |
|     *
 | |
|     * @see CommonDropdown::displaySpecificTypeField()
 | |
|    **/
 | |
|    function displaySpecificTypeField($ID, $field = []) {
 | |
| 
 | |
|       switch ($field['name']) {
 | |
|          case 'periodicity' :
 | |
|             $possible_values = [];
 | |
|             for ($i=1; $i<24; $i++) {
 | |
|                $possible_values[$i*HOUR_TIMESTAMP] = sprintf(_n('%d hour', '%d hours', $i), $i);
 | |
|             }
 | |
|             for ($i=1; $i<=30; $i++) {
 | |
|                $possible_values[$i*DAY_TIMESTAMP] = sprintf(_n('%d day', '%d days', $i), $i);
 | |
|             }
 | |
| 
 | |
|             for ($i=1; $i<12; $i++) {
 | |
|                $possible_values[$i.'MONTH'] = sprintf(_n('%d month', '%d months', $i), $i);
 | |
|             }
 | |
| 
 | |
|             for ($i=1; $i<11; $i++) {
 | |
|                $possible_values[$i.'YEAR'] = sprintf(_n('%d year', '%d years', $i), $i);
 | |
|             }
 | |
| 
 | |
|             Dropdown::showFromArray($field['name'], $possible_values,
 | |
|                                     ['value' => $this->fields[$field['name']]]);
 | |
|             break;
 | |
|       }
 | |
|    }
 | |
| 
 | |
|    /**
 | |
|     * @since 0.84
 | |
|     *
 | |
|     * @param $field
 | |
|     * @param $values
 | |
|     * @param $options   array
 | |
|    **/
 | |
|    static function getSpecificValueToDisplay($field, $values, array $options = []) {
 | |
| 
 | |
|       if (!is_array($values)) {
 | |
|          $values = [$field => $values];
 | |
|       }
 | |
| 
 | |
|       switch ($field) {
 | |
|          case 'periodicity' :
 | |
|             if (preg_match('/([0-9]+)MONTH/', $values[$field], $matches)) {
 | |
|                return sprintf(_n('%d month', '%d months', $matches[1]), $matches[1]);
 | |
|             }
 | |
|             if (preg_match('/([0-9]+)YEAR/', $values[$field], $matches)) {
 | |
|                return sprintf(_n('%d year', '%d years', $matches[1]), $matches[1]);
 | |
|             }
 | |
|             return Html::timestampToString($values[$field], false);
 | |
|          break;
 | |
|       }
 | |
|       return parent::getSpecificValueToDisplay($field, $values, $options);
 | |
|    }
 | |
| 
 | |
| 
 | |
|    function rawSearchOptions() {
 | |
|       $tab = parent::rawSearchOptions();
 | |
| 
 | |
|       $tab[] = [
 | |
|          'id'                 => '11',
 | |
|          'table'              => $this->getTable(),
 | |
|          'field'              => 'is_active',
 | |
|          'name'               => __('Active'),
 | |
|          'datatype'           => 'bool'
 | |
|       ];
 | |
| 
 | |
|       $tab[] = [
 | |
|          'id'                 => '12',
 | |
|          'table'              => 'glpi_tickettemplates',
 | |
|          'field'              => 'name',
 | |
|          'name'               => _n('Ticket template', 'Ticket templates', 1),
 | |
|          'datatype'           => 'itemlink'
 | |
|       ];
 | |
| 
 | |
|       $tab[] = [
 | |
|          'id'                 => '13',
 | |
|          'table'              => $this->getTable(),
 | |
|          'field'              => 'begin_date',
 | |
|          'name'               => __('Start date'),
 | |
|          'datatype'           => 'datetime'
 | |
|       ];
 | |
| 
 | |
|       $tab[] = [
 | |
|          'id'                 => '17',
 | |
|          'table'              => $this->getTable(),
 | |
|          'field'              => 'end_date',
 | |
|          'name'               => __('End date'),
 | |
|          'datatype'           => 'datetime'
 | |
|       ];
 | |
| 
 | |
|       $tab[] = [
 | |
|          'id'                 => '15',
 | |
|          'table'              => $this->getTable(),
 | |
|          'field'              => 'periodicity',
 | |
|          'name'               => __('Periodicity'),
 | |
|          'datatype'           => 'specific'
 | |
|       ];
 | |
| 
 | |
|       $tab[] = [
 | |
|          'id'                 => '14',
 | |
|          'table'              => $this->getTable(),
 | |
|          'field'              => 'create_before',
 | |
|          'name'               => __('Preliminary creation'),
 | |
|          'datatype'           => 'timestamp'
 | |
|       ];
 | |
| 
 | |
|       $tab[] = [
 | |
|          'id'                 => '18',
 | |
|          'table'              => 'glpi_calendars',
 | |
|          'field'              => 'name',
 | |
|          'name'               => _n('Calendar', 'Calendars', 1),
 | |
|          'datatype'           => 'itemlink'
 | |
|       ];
 | |
| 
 | |
|       return $tab;
 | |
|    }
 | |
| 
 | |
| 
 | |
|    /**
 | |
|     * Show next creation date
 | |
|     *
 | |
|     * @return void
 | |
|    **/
 | |
|    function showInfos() {
 | |
| 
 | |
|       if (!is_null($this->fields['next_creation_date'])) {
 | |
|          echo "<div class='center'>";
 | |
|          //TRANS: %s is the date of next creation
 | |
|          echo sprintf(__('Next creation on %s'),
 | |
|                       Html::convDateTime($this->fields['next_creation_date']));
 | |
|          echo "</div>";
 | |
|       }
 | |
|    }
 | |
| 
 | |
| 
 | |
|    /**
 | |
|     * Compute next creation date of a ticket.
 | |
|     *
 | |
|     * @param string         $begin_date     Begin date of the recurrent ticket in 'Y-m-d H:i:s' format.
 | |
|     * @param string         $end_date       End date of the recurrent ticket in 'Y-m-d H:i:s' format,
 | |
|     *                                       or 'NULL' or empty value.
 | |
|     * @param string|integer $periodicity    Periodicity of creation, could be:
 | |
|     *                                        - an integer corresponding to seconds,
 | |
|     *                                        - a string using "/([0-9]+)(MONTH|YEAR)/" pattern.
 | |
|     * @param integer        $create_before  Anticipated creation delay in seconds.
 | |
|     * @param integer|null   $calendars_id   ID of the calendar to use to restrict creation to working hours,
 | |
|     *                                       or 0 / null for no calendar.
 | |
|     *
 | |
|     * @return string  Next creation date in 'Y-m-d H:i:s' format.
 | |
|     *
 | |
|     * @since 0.84 $calendars_id parameter added
 | |
|     */
 | |
|    function computeNextCreationDate($begin_date, $end_date, $periodicity, $create_before,
 | |
|                                     $calendars_id) {
 | |
| 
 | |
|       $now = time();
 | |
|       $periodicity_pattern = '/([0-9]+)(MONTH|YEAR)/';
 | |
| 
 | |
|       if (false === DateTime::createFromFormat('Y-m-d H:i:s', $begin_date)) {
 | |
|          // Invalid begin date.
 | |
|          return 'NULL';
 | |
|       }
 | |
| 
 | |
|       $has_end_date = false !== DateTime::createFromFormat('Y-m-d H:i:s', $end_date);
 | |
|       if ($has_end_date && strtotime($end_date) < $now) {
 | |
|          // End date is in past.
 | |
|          return 'NULL';
 | |
|       }
 | |
| 
 | |
|       if (!is_int($periodicity) && !preg_match('/^\d+$/', $periodicity)
 | |
|           && !preg_match($periodicity_pattern, $periodicity)) {
 | |
|          // Invalid periodicity.
 | |
|          return 'NULL';
 | |
|       }
 | |
| 
 | |
|       // Compute periodicity values
 | |
|       $periodicity_as_interval = null;
 | |
|       $periodicity_in_seconds = $periodicity;
 | |
|       $matches = [];
 | |
|       if (preg_match($periodicity_pattern, $periodicity, $matches)) {
 | |
|          $periodicity_as_interval = "{$matches[1]} {$matches[2]}";
 | |
|          $periodicity_in_seconds  = $matches[1]
 | |
|             * MONTH_TIMESTAMP
 | |
|             * ('YEAR' === $matches[2] ? 12 : 1);
 | |
|       } else if ($periodicity % DAY_TIMESTAMP == 0) {
 | |
|          $periodicity_as_interval = ($periodicity / DAY_TIMESTAMP) . ' DAY';
 | |
|       } else {
 | |
|          $periodicity_as_interval = ($periodicity / HOUR_TIMESTAMP) . ' HOUR';
 | |
|       }
 | |
| 
 | |
|       // Check that anticipated creation delay is greater than periodicity.
 | |
|       if ($create_before > $periodicity_in_seconds) {
 | |
|          Session::addMessageAfterRedirect(
 | |
|             __('Invalid frequency. It must be greater than the preliminary creation.'),
 | |
|             false,
 | |
|             ERROR
 | |
|          );
 | |
|          return 'NULL';
 | |
|       }
 | |
| 
 | |
|       $calendar = new Calendar();
 | |
|       $is_calendar_valid = $calendars_id && $calendar->getFromDB($calendars_id) && $calendar->hasAWorkingDay();
 | |
| 
 | |
|       if (!$is_calendar_valid || $periodicity_in_seconds >= DAY_TIMESTAMP) {
 | |
|          // Compute next occurence without using the calendar if calendar is not valid
 | |
|          // or if periodicity is at least one day.
 | |
| 
 | |
|          // First occurence of creation
 | |
|          $occurence_time = strtotime($begin_date);
 | |
|          $creation_time  = $occurence_time - $create_before;
 | |
| 
 | |
|          // Add steps while creation time is in past
 | |
|          while ($creation_time < $now) {
 | |
|             $creation_time  = strtotime("+ $periodicity_as_interval", $creation_time);
 | |
|             $occurence_time = $creation_time + $create_before;
 | |
| 
 | |
|             // Stop if end date reached
 | |
|             if ($has_end_date && $occurence_time > strtotime($end_date)) {
 | |
|                return 'NULL';
 | |
|             }
 | |
|          }
 | |
| 
 | |
|          if ($is_calendar_valid) {
 | |
|             // Jump to next working day if occurence is outside working days.
 | |
|             while ($calendar->isHoliday(date('Y-m-d', $occurence_time))
 | |
|                    || !$calendar->isAWorkingDay($occurence_time)) {
 | |
|                $occurence_time = strtotime('+ 1 day', $occurence_time);
 | |
|             }
 | |
|             // Jump to next working hour if occurence is outside working hours.
 | |
|             if (!$calendar->isAWorkingHour($occurence_time)) {
 | |
|                $occurence_date = $calendar->computeEndDate(
 | |
|                   date('Y-m-d', $occurence_time),
 | |
|                   0 // 0 second delay to get the first working "second"
 | |
|                );
 | |
|                $occurence_time = strtotime($occurence_date);
 | |
|             }
 | |
|             $creation_time  = $occurence_time - $create_before;
 | |
|          }
 | |
|       } else {
 | |
|          // Base computation on calendar if calendar is valid
 | |
| 
 | |
|          $occurence_date = $calendar->computeEndDate(
 | |
|             $begin_date,
 | |
|             0 // 0 second delay to get the first working "second"
 | |
|          );
 | |
|          $occurence_time = strtotime($occurence_date);
 | |
|          $creation_time  = $occurence_time - $create_before;
 | |
| 
 | |
|          while ($creation_time < $now) {
 | |
|             $occurence_date = $calendar->computeEndDate(
 | |
|                date('Y-m-d H:i:s', $occurence_time),
 | |
|                $periodicity_in_seconds,
 | |
|                0,
 | |
|                $periodicity_in_seconds >= DAY_TIMESTAMP
 | |
|             );
 | |
|             $occurence_time = strtotime($occurence_date);
 | |
|             $creation_time  = $occurence_time - $create_before;
 | |
| 
 | |
|             // Stop if end date reached
 | |
|             if ($has_end_date && $occurence_time > strtotime($end_date)) {
 | |
|                return 'NULL';
 | |
|             }
 | |
|          };
 | |
|       }
 | |
| 
 | |
|       return date("Y-m-d H:i:s", $creation_time);
 | |
|    }
 | |
| 
 | |
| 
 | |
|    /**
 | |
|     * Give cron information
 | |
|     *
 | |
|     * @param $name : task's name
 | |
|     *
 | |
|     * @return array of information
 | |
|    **/
 | |
|    static function cronInfo($name) {
 | |
| 
 | |
|       switch ($name) {
 | |
|          case 'ticketrecurrent' :
 | |
|             return ['description' => self::getTypeName(Session::getPluralNumber())];
 | |
|       }
 | |
|       return [];
 | |
|    }
 | |
| 
 | |
| 
 | |
|    /**
 | |
|     * Cron for ticket's automatic close
 | |
|     *
 | |
|     * @param $task : crontask object
 | |
|     *
 | |
|     * @return integer (0 : nothing done - 1 : done)
 | |
|    **/
 | |
|    static function cronTicketRecurrent($task) {
 | |
|       global $DB;
 | |
| 
 | |
|       $tot = 0;
 | |
| 
 | |
|       $iterator = $DB->request([
 | |
|          'FROM'   => 'glpi_ticketrecurrents',
 | |
|          'WHERE'  => [
 | |
|             'next_creation_date' => ['<', new \QueryExpression('NOW()')],
 | |
|             'is_active'          => 1,
 | |
|             'OR'                 => [
 | |
|                ['end_date' => null],
 | |
|                ['end_date' => ['>', new \QueryExpression('NOW()')]]
 | |
|             ]
 | |
|          ]
 | |
|       ]);
 | |
| 
 | |
|       while ($data = $iterator->next()) {
 | |
|          if (self::createTicket($data)) {
 | |
|             $tot++;
 | |
|          } else {
 | |
|             //TRANS: %s is a name
 | |
|             $task->log(sprintf(__('Failed to create recurrent ticket %s'),
 | |
|                                $data['name']));
 | |
|          }
 | |
|       }
 | |
| 
 | |
|       $task->setVolume($tot);
 | |
|       return ($tot > 0 ? 1 : 0);
 | |
|    }
 | |
| 
 | |
| 
 | |
|    /**
 | |
|     * Create a ticket based on ticket recurrent infos
 | |
|     *
 | |
|     * @param $data array data of a entry of glpi_ticketrecurrents
 | |
|     *
 | |
|     * @return boolean
 | |
|    **/
 | |
|    static function createTicket($data) {
 | |
| 
 | |
|       $result = false;
 | |
|       $tt     = new TicketTemplate();
 | |
| 
 | |
|       // Create ticket based on ticket template and entity information of ticketrecurrent
 | |
|       if ($tt->getFromDB($data['tickettemplates_id'])) {
 | |
|          // Get default values for ticket
 | |
|          $input = Ticket::getDefaultValues($data['entities_id']);
 | |
|          // Apply itiltemplates predefined values
 | |
|          $ttp        = new TicketTemplatePredefinedField();
 | |
|          $predefined = $ttp->getPredefinedFields($data['tickettemplates_id'], true);
 | |
| 
 | |
|          if (count($predefined)) {
 | |
|             foreach ($predefined as $predeffield => $predefvalue) {
 | |
|                $input[$predeffield] = $predefvalue;
 | |
|             }
 | |
|          }
 | |
|          // Set date to creation date
 | |
|          $createtime    = strtotime($data['next_creation_date'])+$data['create_before'];
 | |
|          $input['date'] = date('Y-m-d H:i:s', $createtime);
 | |
|          if (isset($predefined['date'])) {
 | |
|             $input['date'] = Html::computeGenericDateTimeSearch($predefined['date'], false,
 | |
|                                                                 $createtime);
 | |
|          }
 | |
|          // Compute time_to_resolve if predefined based on create date
 | |
|          if (isset($predefined['time_to_resolve'])) {
 | |
|             $input['time_to_resolve'] = Html::computeGenericDateTimeSearch($predefined['time_to_resolve'], false,
 | |
|                                                                     $createtime);
 | |
|          }
 | |
| 
 | |
|          // Compute internal_time_to_resolve if predefined based on create date
 | |
|          if (isset($predefined['internal_time_to_resolve'])) {
 | |
|             $input['internal_time_to_resolve'] = Html::computeGenericDateTimeSearch($predefined['internal_time_to_resolve'], false,
 | |
|                                                                            $createtime);
 | |
|          }
 | |
|          // Set entity
 | |
|          $input['entities_id'] = $data['entities_id'];
 | |
|          $input['_auto_import'] = true;
 | |
| 
 | |
|          $ticket = new Ticket();
 | |
|          $input  = Toolbox::addslashes_deep($input);
 | |
|          if ($tid = $ticket->add($input)) {
 | |
|             $msg = sprintf(__('Ticket %d successfully created'), $tid);
 | |
|             $result = true;
 | |
|          } else {
 | |
|             $msg = __('Ticket creation failed (check mandatory fields)');
 | |
|          }
 | |
|       } else {
 | |
|          $msg = __('Ticket creation failed (no template)');
 | |
|       }
 | |
|       $changes[0] = 0;
 | |
|       $changes[1] = '';
 | |
|       $changes[2] = addslashes($msg);
 | |
|       Log::history($data['id'], __CLASS__, $changes, '', Log::HISTORY_LOG_SIMPLE_MESSAGE);
 | |
| 
 | |
|       // Compute next creation date
 | |
|       $tr = new self();
 | |
|       if ($tr->getFromDB($data['id'])) {
 | |
|          $input                       = [];
 | |
|          $input['id']                 = $data['id'];
 | |
|          $input['next_creation_date'] = $tr->computeNextCreationDate($data['begin_date'],
 | |
|                                                                      $data['end_date'],
 | |
|                                                                      $data['periodicity'],
 | |
|                                                                      $data['create_before'],
 | |
|                                                                      $data['calendars_id']);
 | |
|          $tr->update($input);
 | |
|       }
 | |
| 
 | |
|       return $result;
 | |
|    }
 | |
| 
 | |
| 
 | |
|    static function getIcon() {
 | |
|       return "fas fa-stopwatch";
 | |
|    }
 | |
| 
 | |
| }
 |