Explorar el Código

Ticket #5425 - Tasks: Functionality improvements. Stage 2.

AntonLV hace 7 meses
padre
commit
4f9e063a0b

+ 12 - 0
modules/boonex/russian/data/langs/bx_tasks/ru.xml

@@ -227,10 +227,15 @@
     <string name="_bx_tasks_page_block_title_system_time_manage"><![CDATA[Управление собственным временем]]></string>
     <string name="_bx_tasks_page_block_title_system_time_manage_administration"><![CDATA[Управление всем временем]]></string>
     <string name="_bx_tasks_page_block_title_time_manage"><![CDATA[Управление временем]]></string>
+    <string name="_bx_tasks_page_title_sys_timers"><![CDATA[Таймеры]]></string>
+    <string name="_bx_tasks_page_title_timers"><![CDATA[Таймеры]]></string>
+    <string name="_bx_tasks_page_block_title_system_timers"><![CDATA[Таймеры]]></string>
+    <string name="_bx_tasks_page_block_title_timers"><![CDATA[Таймеры]]></string>
     <string name="_bx_tasks_page_block_title_popular_keywords"><![CDATA[Популярные хэштеги]]></string>
     <string name="_bx_tasks_page_block_title_cats"><![CDATA[Задания Категории]]></string>
     <string name="_bx_tasks_page_block_title_sys_multicats"><![CDATA[Задания Категории(Мульти)]]></string>
     <string name="_bx_tasks_page_block_title_multicats"><![CDATA[Задания Категории]]></string>
+    <string name="_bx_tasks_page_block_title_entry_timer"><![CDATA[Таймер]]></string>
     <string name="_bx_tasks_page_block_title_entry_info"><![CDATA[Инфо]]></string>
     <string name="_bx_tasks_page_block_title_my_entries"><![CDATA[Мои Задания]]></string>
     <string name="_bx_tasks_page_block_title_sys_recent_entries_view_showcase"><![CDATA[Последние Задания (Витринный просмотр)]]></string>
@@ -336,6 +341,7 @@
     <string name="_bx_tasks_menu_item_title_mt_submenu_entries_administration"><![CDATA[Все задания]]></string>
     <string name="_bx_tasks_menu_item_title_mt_submenu_time_common"><![CDATA[Моё время]]></string>
     <string name="_bx_tasks_menu_item_title_mt_submenu_time_administration"><![CDATA[Всё время]]></string>
+    <string name="_bx_tasks_menu_item_title_mt_submenu_timers"><![CDATA[Мои таймеры]]></string>
 
     <!-- Grids -->
     <string name="_bx_tasks_grid_filter_item_title_adm_select_one_filter1"><![CDATA[Все статусы]]></string>
@@ -457,6 +463,12 @@
     <string name="_bx_tasks_txt_estimate"><![CDATA[Выделено времени: {0}]]></string>
     <string name="_bx_tasks_txt_set"><![CDATA[set...]]></string>
 
+    <string name="_bx_tasks_txt_timer_start"><![CDATA[Старт]]></string>
+    <string name="_bx_tasks_txt_timer_stop"><![CDATA[Стоп]]></string>
+    <string name="_bx_tasks_txt_timer_resume"><![CDATA[Возобновить]]></string>
+    <string name="_bx_tasks_txt_timer_clear"><![CDATA[Очистить]]></string>
+    <string name="_bx_tasks_txt_timer_log"><![CDATA[Внести]]></string>
+
     <string name="_bx_tasks_txt_err_cannot_perform_action"><![CDATA[Невозможно выполнить действие. Пожалуйста, оповестите нас об этой проблеме.]]></string>
     <string name="_bx_tasks_txt_err_cannot_perform_action"><![CDATA[Невозможно выполнить действие. Пожалуйста, оповестите нас об этой проблеме.]]></string>
     <string name="_bx_tasks_txt_msg_status_hidden"><![CDATA[Данное задание было скрыто.]]></string>

+ 17 - 5
modules/boonex/tasks/classes/BxTasksConfig.php

@@ -36,6 +36,7 @@ class BxTasksConfig extends BxBaseModTextConfig
             'TABLE_LISTS' => $aModule['db_prefix'] . 'lists',
             'TABLE_TIME' => $aModule['db_prefix'] . 'time',
             'TABLE_TIME_TRACK' => $aModule['db_prefix'] . 'time_track',
+            'TABLE_TIMERS' => $aModule['db_prefix'] . 'timers',
             'TABLE_POLLS' => '',
             'TABLE_ENTRIES_FULLTEXT' => 'title_text',
 
@@ -247,14 +248,16 @@ class BxTasksConfig extends BxBaseModTextConfig
             'manage_tools' => 'BxTasksManageTools',
             'categories' => 'BxDolCategories',
             'tasks' => 'BxTasksView',
-            'time' => 'BxTasksTime'
+            'time' => 'BxTasksTime',
+            'timer' => 'BxTasksTimer'
         ]);
 
         $this->_aJsObjects = array_merge($this->_aJsObjects, [
             'manage_tools' => 'oBxTasksManageTools',
             'categories' => 'oBxDolCategories',
             'tasks' => 'oBxTasksView',
-            'time' => 'oBxTasksTime'
+            'time' => 'oBxTasksTime',
+            'timer' => 'oBxTasksTimer'
         ]);
 
         $this->_aGridObjects = [
@@ -270,6 +273,8 @@ class BxTasksConfig extends BxBaseModTextConfig
         $this->_aHtmlIds = array_merge($this->_aHtmlIds, [
             'time_popup' =>  $sPrefix . '-time-popup',
             'total_popup' =>  $sPrefix . '-total-popup',
+            'timer' => $sPrefix . '-timer-',
+            'timer_actions' =>  $sPrefix . '-timer-actions',
         ]);
 
         $this->_bAttachmentsInTimeline = true;
@@ -347,10 +352,17 @@ class BxTasksConfig extends BxBaseModTextConfig
         return $this->timeA2I(explode(':', $s));
     }
 
-    public function timeI2A($i)
+    public function timeI2A($i, $bUseSeconds = false)
     {
-        $iH = intdiv($i, 60);
-        return [$iH, $i - 60 * $iH];
+        if(!$bUseSeconds) {
+            $iH = intdiv($i, 60);
+
+            return [$iH, $i - 60 * $iH];
+        }
+
+        $iH = intdiv($i, 3600);
+        $iM = intdiv($i - 3600 * $iH, 60);
+        return [$iH, $iM, $i - 3600 * $iH - 60 * $iM];
     }
 
     public function timeI2S($i)

+ 107 - 1
modules/boonex/tasks/classes/BxTasksDb.php

@@ -154,7 +154,7 @@ class BxTasksDb extends BxBaseModTextDb
 
         return count($aResult) == (int)$this->query("UPDATE `" . $CNF['TABLE_ENTRIES'] . "` SET `" . $CNF['FIELD_EXPIRED'] . "` = '1' WHERE `id` IN (" . $this->implode_escape($aResult) . ")") ? $aResult : false;
     }
-    
+
     public function getTimeTracks($aParams = []) 
     {
         $CNF = &$this->_oConfig->CNF;
@@ -191,6 +191,112 @@ class BxTasksDb extends BxBaseModTextDb
 
         return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
     }
+
+    public function insertTimeTrack($aParamsSet)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParamsSet))
+            return false;
+
+        return $this->query("INSERT INTO `" . $CNF['TABLE_TIME_TRACK'] . "` SET " . $this->arrayToSQL($aParamsSet)) ? $this->lastId() : false;
+    }
+
+    public function getTimers($aParams = []) 
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
+        $sSelectClause = '`tt`.*';
+        $sJoinClause = $sWhereClause = $sOrderClause = '';
+
+        if(!empty($aParams))
+            switch($aParams['sample']) {
+                case 'id':
+                    $aMethod['name'] = 'getRow';
+                    $aMethod['params'][1] = [
+                        'id' => $aParams['id']
+                    ];
+
+                    $sWhereClause = "AND `tt`.`id` = :id";
+                    break;
+
+                case 'content_profile_ids':
+                    $aMethod['name'] = 'getRow';
+                    $aMethod['params'][1] = [
+                        'content_id' => $aParams['content_id'],
+                        'profile_id' => $aParams['profile_id']
+                    ];
+
+                    $sWhereClause = "AND `tt`.`content_id` = :content_id AND `tt`.`profile_id` = :profile_id";
+                    break;
+
+                case 'profile_id':
+                    $aMethod['params'][1] = [
+                        'profile_id' => $aParams['profile_id']
+                    ];
+
+                    $sWhereClause = "AND `tt`.`profile_id` = :profile_id";
+
+                    if(isset($aParams['active']) && $aParams['active'] === true) {
+                        $aMethod['name'] = 'getRow';
+
+                        $sWhereClause .= " AND `tt`.`started` <> '0'";
+                    }
+                    break;
+            }
+
+        if(!empty($sOrderClause))
+            $sOrderClause = "ORDER BY " . $sOrderClause;
+
+        $aMethod['params'][0] = "SELECT 
+                " . $sSelectClause . " 
+            FROM `" . $CNF['TABLE_TIMERS'] . "` AS `tt` " . $sJoinClause . " 
+            WHERE 1 " . $sWhereClause . " " . $sOrderClause;
+
+        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
+    }
+
+    public function isTimerStarted($iObjectId, $iAuthorId)
+    {
+        $aTimer = $this->getTimers([
+            'sample' => 'content_profile_ids', 
+            'content_id' => $iObjectId, 
+            'profile_id' => $iAuthorId
+        ]);
+
+        return ($aTimer['started'] ?? 0) != 0;
+    }
+
+    public function insertTimer($aParamsSet)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParamsSet))
+            return false;
+
+        return $this->query("INSERT INTO `" . $CNF['TABLE_TIMERS'] . "` SET " . $this->arrayToSQL($aParamsSet)) ? $this->lastId() : false;
+    }
+    
+    public function updateTimer($aParamsSet, $aParamsWhere)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParamsSet) || empty($aParamsWhere))
+            return false;
+
+        return $this->query("UPDATE `" . $CNF['TABLE_TIMERS'] . "` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND "));
+    }
+
+    public function deleteTimer($aParamsWhere)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParamsWhere))
+            return false;
+
+        return $this->query("DELETE FROM `" . $CNF['TABLE_TIMERS'] . "` WHERE " . $this->arrayToSQL($aParamsWhere, " AND "));
+    }
 }
 
 /** @} */

+ 5 - 0
modules/boonex/tasks/classes/BxTasksGridCommon.php

@@ -25,6 +25,11 @@ class BxTasksGridCommon extends BxBaseModTextGridCommon
         $this->_initFilter(3, $this->_oModule->getContexts());
     }
 
+    protected function _getCellContextModule($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault($this->_oModule->getModuleTitle($mixedValue), $sKey, $aField, $aRow);
+    }
+
     protected function _getFilterControls()
     {
         $sContent = $this->_getFilterSelectOne($this->_sFilter3Name, $this->_sFilter3Value, $this->_aFilter3Values);

+ 151 - 0
modules/boonex/tasks/classes/BxTasksModule.php

@@ -260,6 +260,110 @@ class BxTasksModule extends BxBaseModTextModule implements iBxDolCalendarService
         echo json_encode($aEntries);
     }
 
+    public function actionProcessTimer($sAction, $iContentId, $iProfileId)
+    {
+        if(!$this->_iProfileId || $this->_iProfileId != $iProfileId)
+            return echoJson(['msg' => _t('_sys_txt_access_denied')]);
+
+        $aResult = $this->serviceProcessTimer($sAction, $iContentId, $iProfileId);
+        if($aResult['code'] == 0)
+            $aResult = array_merge($aResult, [
+                'content_id' => $iContentId,
+                'profile_id' => $iProfileId,
+                'content' => $this->_oTemplate->getTimer($iContentId, $iProfileId),
+                'eval' => $this->_oConfig->getJsObject('timer') . '.onPerformAction' . bx_gen_method_name($sAction) . '(oData)'
+            ]);
+
+        return echoJson($aResult);
+    }
+
+    public function serviceGetSafeServices()
+    {
+        return array_merge(parent::serviceGetSafeServices(), [
+            'ProcessTimer' => '',
+        ]);
+    }
+
+    public function serviceProcessTimer($sAction, $iContentId, $iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aResult = ['code' => 1, 'msg' => _t('_sys_txt_error_occured')];
+
+        $iNow = time();
+        switch($sAction) {
+            case 'start':
+                $this->stopTimerByAuthor($iProfileId);
+
+                $iTimer = false;
+                $aTimer = $this->getTimer($iContentId, $iProfileId);
+                if($aTimer && is_array($aTimer)) {
+                    if($this->updateTimerById($aTimer['id'], ['started' => $iNow]))
+                        $iTimer = $aTimer['id'];
+                }
+                else
+                    $iTimer = $this->_oDb->insertTimer(['content_id' => $iContentId, 'profile_id' => $iProfileId, 'started' => $iNow]);
+
+                if($iTimer !== false)
+                    $aResult = ['code' => 0, 'id' => $iTimer];
+                break;
+
+            case 'stop':
+                if($this->stopTimerByAuthor($iProfileId))
+                    $aResult = ['code' => 0];
+                break;
+
+            case 'resume':
+                $this->stopTimerByAuthor($iProfileId);
+
+                $aTimer = $this->getTimer($iContentId, $iProfileId);
+                if($aTimer && is_array($aTimer) && $this->updateTimerById($aTimer['id'], ['started' => $iNow]))
+                    $aResult = ['code' => 0];
+                break;
+
+            case 'log':
+                $aTimer = $this->getTimer($iContentId, $iProfileId);
+                if(!$aTimer || !is_array($aTimer) || !$aTimer['duration'])
+                    break;
+
+                list($iH, $iM) = $this->_oConfig->timeI2A($aTimer['duration'], true);
+
+                $iNow = time();
+                $iTrackId = $this->_oDb->insertTimeTrack([
+                    'object_id' => $iContentId,
+                    'author_id' => $iProfileId,
+                    'author_nip' => $iProfileId == $this->_iProfileId ? bx_get_ip_hash(getVisitorIP()) : 0,
+                    'value' => $this->_oConfig->timeA2I([(int)$iH, (int)$iM + 1]),
+                    'value_date' => $iNow,
+                    'date' => $iNow
+                ]);
+                if(!$iTrackId)
+                    break;
+
+                $oTime = BxDolReport::getObjectInstance($CNF['OBJECT_REPORTS_TIME'], $iContentId);
+                if(!$oTime || !$oTime->isEnabled())
+                    break;
+
+                if(!$oTime->putReport($iContentId, $iProfileId, $iTrackId))
+                    break;
+
+                $this->_oDb->deleteTimer([
+                    'content_id' => $iContentId, 
+                    'profile_id' => $iProfileId
+                ]);
+
+                $aResult = ['code' => 0];
+                break;
+
+            case 'clear':
+                if($this->_oDb->deleteTimer(['content_id' => $iContentId, 'profile_id' => $iProfileId]) !== false)
+                    $aResult = ['code' => 0];
+                break;
+        }
+
+        return $aResult;
+    }
+
     public function serviceManageTools($sType = 'common')
     {
         $CNF = &$this->_oConfig->CNF;
@@ -321,6 +425,16 @@ class BxTasksModule extends BxBaseModTextModule implements iBxDolCalendarService
         return $aResult;
     }
 
+    public function serviceGetBlockTimers()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        return [
+            'content' => $this->_oTemplate->getBlockTimers($this->_iProfileId),
+            'menu' => BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_MANAGE_TOOLS_SUBMENU'])
+        ];
+    }
+
     /**
      * Data for Timeline module
      */
@@ -542,6 +656,16 @@ class BxTasksModule extends BxBaseModTextModule implements iBxDolCalendarService
         return $this->_oTemplate->entryAssignments($aProfiles);
     }
 
+    public function serviceEntityTimer($iContentId = 0)
+    {
+        if(!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if(!$iContentId)
+            return false;
+
+        return $this->_oTemplate->entryTimer($iContentId, $this->_iProfileId);
+    }
+
     public function serviceCheckAllowedCommentsTask($iContentId, $sObjectComments) 
     {
         $CNF = &$this->_oConfig->CNF;
@@ -697,6 +821,33 @@ class BxTasksModule extends BxBaseModTextModule implements iBxDolCalendarService
         return $aEntries;
     }
 
+    public function getTimer($iContentId, $iProfileId)
+    {
+        return $this->_oDb->getTimers(['sample' => 'content_profile_ids', 'content_id' => $iContentId, 'profile_id' => $iProfileId]);
+    }
+
+    public function getTimersByAuthor($iProfileId, $bActive = false)
+    {
+        return $this->_oDb->getTimers(['sample' => 'profile_id', 'profile_id' => $iProfileId, 'active' => $bActive]);
+    }
+
+    public function stopTimerByAuthor($iProfileId)
+    {
+        $aTimer = $this->getTimersByAuthor($iProfileId, true);
+        if(!$aTimer || !is_array($aTimer)) 
+            return false;
+
+        return $this->updateTimerById($aTimer['id'], [
+            'started' => 0,
+            'duration' => $aTimer['duration'] + (time() - $aTimer['started']),
+        ]);
+    }
+
+    public function updateTimerById($iId, $aSet)
+    {
+        return $this->_oDb->updateTimer($aSet, ['id' => (int)$iId]) !== false;
+    }
+    
     public function onExpired($iContentId)
     {
         $CNF = &$this->_oConfig->CNF;

+ 145 - 1
modules/boonex/tasks/classes/BxTasksTemplate.php

@@ -13,13 +13,38 @@
  */
 class BxTasksTemplate extends BxBaseModTextTemplate
 {
-    function __construct(&$oConfig, &$oDb)
+    public function __construct(&$oConfig, &$oDb)
     {
         $this->MODULE = 'bx_tasks';
 
         parent::__construct($oConfig, $oDb);
     }
 
+    public function getJsCode($sType, $aParams = [], $mixedWrap = true)
+    {
+        $aParams = array_merge([
+            'aHtmlIds' => $this->_oConfig->getHtmlIds()
+        ], $aParams);
+
+        return parent::getJsCode($sType, $aParams, $mixedWrap);
+    }
+
+    public function getJsCodeTimer($sType, $aParams = [], $mixedWrap = true)
+    {
+        $aParams = array_merge([
+            'aHtmlIds' => $this->_oConfig->getHtmlIds()
+        ], $aParams);
+
+        if(is_array($mixedWrap))
+            return parent::getJsCode($sType, $aParams, [
+                'wrap' => true,
+                'mask' => "{var} {object} = new {class}({params}); {object}.init({content_id}, {profile_id}, {started});",
+                'mask_markers' => $mixedWrap
+            ]);
+        else
+            parent::getJsCode($sType, $aParams, $mixedWrap);
+    }
+    
     /**
      * Use Gallery image for both because currently there is no Unit types with small thumbnails.
      */
@@ -74,6 +99,125 @@ class BxTasksTemplate extends BxBaseModTextTemplate
         ]) : MsgBox(_t('_sys_txt_empty'));
     }
 
+    public function entryTimer ($iContentId, $iProfileId)
+    {
+        $this->addJs(['timer.js']);
+        return $this->getTimer($iContentId, $iProfileId) . $this->getJsCodeTimer('timer', [], [
+            'content_id' => $iContentId,
+            'profile_id' => $iProfileId,
+            'started' => $this->_oDb->isTimerStarted($iContentId, $iProfileId),
+        ]);
+    }
+
+    public function getBlockTimers ($iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oModule = $this->getModule();
+        $sJsObject = $this->_oConfig->getJsObject('timer');
+
+        $aTimers = $this->_oDb->getTimers(['sample' => 'profile_id', 'profile_id' => $iProfileId]);
+        if(!$aTimers || !is_array($aTimers))
+            return MsgBox(_t('_Empty'));
+
+        $oPermalinks = BxDolPermalinks::getInstance();
+
+        $aTmplVarsSections = [];
+        foreach($aTimers as $aTimer) {
+            $aContentInfo = $this->_oDb->getContentInfoById($aTimer['content_id']);
+            if(!$aContentInfo && !is_array($aContentInfo))
+                continue;
+
+            $sContextType = $sContextTitle = $sContextUrl = '';
+            if(($iAllowViewTo = (int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]) < 0 && ($oContext = BxDolProfile::getInstance(abs($iAllowViewTo))) !== false) {
+                $sContextType = $oContext->getModule();
+                $sContextTitle = $oContext->getDisplayName();
+                $sContextUrl = $oContext->getUrl();
+            }
+
+            if(!isset($aTmplVarsSections[$sContextType]))
+                $aTmplVarsSections[$sContextType] = [
+                    'section_title' => $oModule->getModuleTitle($sContextType),
+                    'bx_repeat:timers' => []
+                ];
+
+            $aTmplVarsSections[$sContextType]['bx_repeat:timers'][] = [
+                'bx_if:show_context' => [
+                    'condition' => $sContextTitle && $sContextUrl,
+                    'content' => [
+                        'title' => $sContextTitle,
+                        'url' => $sContextUrl
+                    ]
+                ],
+                'content_title' => $aContentInfo[$CNF['FIELD_TITLE']],
+                'content_url' => bx_absolute_url($oPermalinks->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']])),
+                'timer' => $this->getTimer($aTimer['content_id'], $iProfileId),
+                'js_code_timer' => $sJsObject . '.init(' . $aTimer['content_id'] . ', ' . $iProfileId . ', ' . ($aTimer['started'] != 0 ? 1 : 0) . ');'
+            ];
+        }
+
+        $this->addJs(['timer.js']);
+        return $this->parseHtmlByName('browse_timers.html', [
+            'js_code' => $this->getJsCode('timer', [
+                'bMulti' => true
+            ]),
+            'bx_repeat:sections' => array_values($aTmplVarsSections)
+        ]);
+    }
+
+    public function getTimer ($iContentId, $iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sPrefix = str_replace('_', '-', $this->_oConfig->getName());
+        $sJsObject = $this->_oConfig->getJsObject('timer');
+
+        $aTimer = $this->_oDb->getTimers(['sample' => 'content_profile_ids', 'content_id' => $iContentId, 'profile_id' => $iProfileId]);
+        $bTimer = $aTimer && is_array($aTimer);
+        $bStarted = $bTimer && (int)$aTimer['started'] > 0;
+
+        $aActions = [];
+        $iHours = $iMinutes = $iSeconds = 0;
+        if($bTimer) {
+            $iDuration = (int)$aTimer['duration'];
+            if($bStarted)
+                $iDuration += time() - (int)$aTimer['started'];
+
+            list($iHours, $iMinutes, $iSeconds) = $this->_oConfig->timeI2A($iDuration, true);
+
+            $aActions = [];
+            if($bStarted)
+                $aActions = [
+                    ['id' => $sPrefix . '-stop', 'name' => $sPrefix . '-stop', 'class' => '', 'link' => 'javascript:void(0)', 'onclick' => 'javascript:' . $sJsObject . '.stop(this, ' . $iContentId . ', ' . $iProfileId . ')', 'target' => '_self', 'title' => _t('_bx_tasks_txt_timer_stop')],
+                ];
+            else
+                $aActions = [
+                    ['id' => $sPrefix . '-resume', 'name' => $sPrefix . '-resume', 'class' => '', 'link' => 'javascript:void(0)', 'onclick' => 'javascript:' . $sJsObject . '.resume(this, ' . $iContentId . ', ' . $iProfileId . ')', 'target' => '_self', 'title' => _t('_bx_tasks_txt_timer_resume')],
+                    ['id' => $sPrefix . '-clear', 'name' => $sPrefix . '-clear', 'class' => '', 'link' => 'javascript:void(0)', 'onclick' => 'javascript:' . $sJsObject . '.clear(this, ' . $iContentId . ', ' . $iProfileId . ')', 'target' => '_self', 'title' => _t('_bx_tasks_txt_timer_clear')],
+                    ['id' => $sPrefix . '-log', 'name' => $sPrefix . '-log', 'class' => '', 'link' => 'javascript:void(0)', 'onclick' => 'javascript:' . $sJsObject . '.log(this, ' . $iContentId . ', ' . $iProfileId . ')', 'target' => '_self', 'title' => _t('_bx_tasks_txt_timer_log')],
+                ];
+        }
+        else {
+            $aActions = [
+                ['id' => $sPrefix . '-start', 'name' => $sPrefix . '-start', 'class' => '', 'link' => 'javascript:void(0)', 'onclick' => 'javascript:' . $sJsObject . '.start(this, ' . $iContentId . ', ' . $iProfileId . ')', 'target' => '_self', 'title' => _t('_bx_tasks_txt_timer_start')],
+            ];
+        }
+
+        $oActions = new BxTemplMenu([
+            'template' => 'menu_buttons_hor.html', 
+            'menu_id' => $this->_oConfig->getHtmlIds('timer_actions'), 
+            'menu_items' => $aActions
+        ]);
+
+        return $this->parseHtmlByName('entry-timer.html', [
+            'html_id' => $this->_oConfig->getHtmlIds('timer') . $iContentId . '-' . $iProfileId,
+            'hours' => sprintf("%02d", $iHours),
+            'minutes' => sprintf("%02d", $iMinutes),
+            'seconds' => sprintf("%02d", $iSeconds),
+            'actions' => $oActions->getCode()
+        ]);
+    }
+
     public function getEntriesList($iContextId)
     {
         $CNF = &$this->_oConfig->CNF;

+ 12 - 0
modules/boonex/tasks/install/langs/en.xml

@@ -227,10 +227,15 @@
     <string name="_bx_tasks_page_block_title_system_time_manage"><![CDATA[Manage Own Time]]></string>
     <string name="_bx_tasks_page_block_title_system_time_manage_administration"><![CDATA[Manage All Time]]></string>
     <string name="_bx_tasks_page_block_title_time_manage"><![CDATA[Manage Time]]></string>
+    <string name="_bx_tasks_page_title_sys_timers"><![CDATA[Timers]]></string>
+    <string name="_bx_tasks_page_title_timers"><![CDATA[Timers]]></string>
+    <string name="_bx_tasks_page_block_title_system_timers"><![CDATA[Timers]]></string>
+    <string name="_bx_tasks_page_block_title_timers"><![CDATA[Timers]]></string>
     <string name="_bx_tasks_page_block_title_popular_keywords"><![CDATA[Popular Hashtags]]></string>
     <string name="_bx_tasks_page_block_title_cats"><![CDATA[Tasks Categories]]></string>
     <string name="_bx_tasks_page_block_title_sys_multicats"><![CDATA[Tasks Categories(Multi)]]></string>
     <string name="_bx_tasks_page_block_title_multicats"><![CDATA[Tasks Categories]]></string>
+    <string name="_bx_tasks_page_block_title_entry_timer"><![CDATA[Timer]]></string>
     <string name="_bx_tasks_page_block_title_entry_info"><![CDATA[Info]]></string>
     <string name="_bx_tasks_page_block_title_my_entries"><![CDATA[My Tasks]]></string>
     <string name="_bx_tasks_page_block_title_sys_recent_entries_view_showcase"><![CDATA[Latest Tasks (Showcase View)]]></string>
@@ -338,6 +343,7 @@
     <string name="_bx_tasks_menu_item_title_mt_submenu_entries_administration"><![CDATA[All Tasks]]></string>
     <string name="_bx_tasks_menu_item_title_mt_submenu_time_common"><![CDATA[My Time]]></string>
     <string name="_bx_tasks_menu_item_title_mt_submenu_time_administration"><![CDATA[All Time]]></string>
+    <string name="_bx_tasks_menu_item_title_mt_submenu_timers"><![CDATA[My Timers]]></string>
 
     <!-- Grids -->
     <string name="_bx_tasks_grid_filter_item_title_adm_select_one_filter1"><![CDATA[All Statuses]]></string>
@@ -460,6 +466,12 @@
     <string name="_bx_tasks_txt_estimate"><![CDATA[Estimate: {0}]]></string>
     <string name="_bx_tasks_txt_set"><![CDATA[set...]]></string>
 
+    <string name="_bx_tasks_txt_timer_start"><![CDATA[Start]]></string>
+    <string name="_bx_tasks_txt_timer_stop"><![CDATA[Stop]]></string>
+    <string name="_bx_tasks_txt_timer_resume"><![CDATA[Resume]]></string>
+    <string name="_bx_tasks_txt_timer_clear"><![CDATA[Clear]]></string>
+    <string name="_bx_tasks_txt_timer_log"><![CDATA[Log]]></string>
+
     <string name="_bx_tasks_txt_err_cannot_perform_action"><![CDATA[Cannot perform action. Please report.]]></string>
     <string name="_bx_tasks_txt_err_cannot_perform_action"><![CDATA[Cannot perform action. Please report.]]></string>
     <string name="_bx_tasks_txt_msg_status_hidden"><![CDATA[This task was hidden.]]></string>

+ 11 - 2
modules/boonex/tasks/install/sql/enable.sql

@@ -58,7 +58,8 @@ INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title_system`, `t
 ('bx_tasks_view_entry', 2, 'bx_tasks', '', '_bx_tasks_page_block_title_entry_author', 13, 2147483647, 'service', 'a:2:{s:6:\"module\";s:8:\"bx_tasks\";s:6:\"method\";s:13:\"entity_author\";}', 0, 0, 1, 1),
 ('bx_tasks_view_entry', 2, 'bx_tasks', '', '_bx_tasks_page_block_title_entry_assignments', 11, 2147483647, 'service', 'a:2:{s:6:\"module\";s:8:\"bx_tasks\";s:6:\"method\";s:18:\"entity_assignments\";}', 0, 0, 1, 2),
 ('bx_tasks_view_entry', 3, 'bx_tasks', '_bx_tasks_page_block_title_sys_entry_context', '_bx_tasks_page_block_title_entry_context', 13, 2147483647, 'service', 'a:2:{s:6:\"module\";s:8:\"bx_tasks\";s:6:\"method\";s:14:\"entity_context\";}', 0, 0, 1, 1),
-('bx_tasks_view_entry', 3, 'bx_tasks', '', '_bx_tasks_page_block_title_entry_info', 11, 2147483647, 'service', 'a:2:{s:6:\"module\";s:8:\"bx_tasks\";s:6:\"method\";s:11:\"entity_info\";}', 0, 0, 1, 2),
+('bx_tasks_view_entry', 3, 'bx_tasks', '', '_bx_tasks_page_block_title_entry_timer', 11, 2147483647, 'service', 'a:2:{s:6:\"module\";s:8:\"bx_tasks\";s:6:\"method\";s:12:\"entity_timer\";}', 0, 0, 1, 2),
+('bx_tasks_view_entry', 3, 'bx_tasks', '', '_bx_tasks_page_block_title_entry_info', 11, 2147483647, 'service', 'a:2:{s:6:\"module\";s:8:\"bx_tasks\";s:6:\"method\";s:11:\"entity_info\";}', 0, 0, 1, 3),
 ('bx_tasks_view_entry', 2, 'bx_tasks', '', '_bx_tasks_page_block_title_entry_all_actions', 13, 2147483647, 'service', 'a:2:{s:6:\"module\";s:8:\"bx_tasks\";s:6:\"method\";s:18:\"entity_all_actions\";}', 0, 0, 1, 3),
 ('bx_tasks_view_entry', 4, 'bx_tasks', '', '_bx_tasks_page_block_title_entry_actions', 13, 2147483647, 'service', 'a:2:{s:6:\"module\";s:8:\"bx_tasks\";s:6:\"method\";s:14:\"entity_actions\";}', 0, 0, 0, 0),
 ('bx_tasks_view_entry', 4, 'bx_tasks', '', '_bx_tasks_page_block_title_entry_social_sharing', 11, 2147483647, 'service', 'a:2:{s:6:\"module\";s:8:\"bx_tasks\";s:6:\"method\";s:21:\"entity_social_sharing\";}', 0, 0, 0, 0),
@@ -129,6 +130,13 @@ INSERT INTO `sys_objects_page`(`object`, `title_system`, `title`, `module`, `lay
 INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title_system`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `order`) VALUES 
 ('bx_tasks_time_administration', 1, 'bx_tasks', '_bx_tasks_page_block_title_system_time_manage_administration', '_bx_tasks_page_block_title_time_manage', 11, 192, 'service', 'a:3:{s:6:"module";s:8:"bx_tasks";s:6:"method";s:21:"get_block_manage_time";s:6:"params";a:1:{i:0;s:14:"administration";}}', 0, 1, 0);
 
+-- PAGE: timers
+INSERT INTO `sys_objects_page`(`object`, `title_system`, `title`, `module`, `layout_id`, `visible_for_levels`, `visible_for_levels_editable`, `uri`, `url`, `meta_description`, `meta_keywords`, `meta_robots`, `cache_lifetime`, `cache_editable`, `deletable`, `override_class_name`, `override_class_file`) VALUES 
+('bx_tasks_timers', '_bx_tasks_page_title_sys_timers', '_bx_tasks_page_title_timers', 'bx_tasks', 5, 192, 1, 'tasks-timers', 'page.php?i=tasks-timers', '', '', '', 0, 1, 0, 'BxTasksPageBrowse', 'modules/boonex/tasks/classes/BxTasksPageBrowse.php');
+
+INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title_system`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `order`) VALUES 
+('bx_tasks_timers', 1, 'bx_tasks', '_bx_tasks_page_block_title_system_timers', '_bx_tasks_page_block_title_timers', 11, 2147483647, 'service', 'a:2:{s:6:"module";s:8:"bx_tasks";s:6:"method";s:16:"get_block_timers";}', 0, 1, 0);
+
 
 
 -- MENU: add to site menu
@@ -231,7 +239,8 @@ INSERT INTO `sys_menu_items`(`set_name`, `module`, `name`, `title_system`, `titl
 ('bx_tasks_manage_tools_submenu', 'bx_tasks', 'tasks-manage', '', '_bx_tasks_menu_item_title_mt_submenu_entries_common', 'page.php?i=tasks-manage', '', '_self', '', '', 2147483647, 1, 0, 1),
 ('bx_tasks_manage_tools_submenu', 'bx_tasks', 'tasks-administration', '', '_bx_tasks_menu_item_title_mt_submenu_entries_administration', 'page.php?i=tasks-administration', '', '_self', '', '', 128, 1, 0, 2),
 ('bx_tasks_manage_tools_submenu', 'bx_tasks', 'tasks-time-manage', '', '_bx_tasks_menu_item_title_mt_submenu_time_common', 'page.php?i=tasks-time-manage', '', '_self', '', '', 2147483647, 1, 0, 3),
-('bx_tasks_manage_tools_submenu', 'bx_tasks', 'tasks-time-administration', '', '_bx_tasks_menu_item_title_mt_submenu_time_administration', 'page.php?i=tasks-time-administration', '', '_self', '', '', 128, 1, 0, 4);
+('bx_tasks_manage_tools_submenu', 'bx_tasks', 'tasks-time-administration', '', '_bx_tasks_menu_item_title_mt_submenu_time_administration', 'page.php?i=tasks-time-administration', '', '_self', '', '', 128, 1, 0, 4),
+('bx_tasks_manage_tools_submenu', 'bx_tasks', 'tasks-timers', '', '_bx_tasks_menu_item_title_mt_submenu_timers', 'page.php?i=tasks-timers', '', '_self', '', '', 2147483647, 1, 0, 5);
 
 -- MENU: manage tools: item submenu
 INSERT INTO `sys_objects_menu`(`object`, `title`, `set_name`, `module`, `template_id`, `deletable`, `active`, `override_class_name`, `override_class_file`) VALUES 

+ 11 - 0
modules/boonex/tasks/install/sql/install.sql

@@ -316,6 +316,17 @@ CREATE TABLE IF NOT EXISTS `bx_tasks_time_track` (
   PRIMARY KEY (`id`)
 );
 
+-- TABLE: timers
+CREATE TABLE IF NOT EXISTS `bx_tasks_timers` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `content_id` int(11) NOT NULL default '0',
+  `profile_id` int(11) NOT NULL default '0',
+  `started` int(11) NOT NULL default '0',
+  `duration` int(11) NOT NULL default '0',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `timer` (`content_id`, `profile_id`)
+);
+
 -- TABLE: favorites
 CREATE TABLE `bx_tasks_favorites_track` (
   `id` int(11) NOT NULL AUTO_INCREMENT,

+ 1 - 1
modules/boonex/tasks/install/sql/uninstall.sql

@@ -1,6 +1,6 @@
 
 -- TABLES
-DROP TABLE IF EXISTS `bx_tasks_tasks`, `bx_tasks_lists`, `bx_tasks_assignments`, `bx_tasks_covers`, `bx_tasks_files`, `bx_tasks_photos`, `bx_tasks_photos_resized`, `bx_tasks_videos`, `bx_tasks_videos_resized`, `bx_tasks_cmts`, `bx_tasks_cmts_notes`, `bx_tasks_votes`, `bx_tasks_votes_track`, `bx_tasks_reactions`, `bx_tasks_reactions_track`, `bx_tasks_views_track`, `bx_tasks_meta_keywords`, `bx_tasks_meta_locations`, `bx_tasks_meta_mentions`, `bx_tasks_reports`, `bx_tasks_reports_track`, `bx_tasks_time`, `bx_tasks_time_track`, `bx_tasks_favorites_track`, `bx_tasks_scores`, `bx_tasks_scores_track`;
+DROP TABLE IF EXISTS `bx_tasks_tasks`, `bx_tasks_lists`, `bx_tasks_assignments`, `bx_tasks_covers`, `bx_tasks_files`, `bx_tasks_photos`, `bx_tasks_photos_resized`, `bx_tasks_videos`, `bx_tasks_videos_resized`, `bx_tasks_cmts`, `bx_tasks_cmts_notes`, `bx_tasks_votes`, `bx_tasks_votes_track`, `bx_tasks_reactions`, `bx_tasks_reactions_track`, `bx_tasks_views_track`, `bx_tasks_meta_keywords`, `bx_tasks_meta_locations`, `bx_tasks_meta_mentions`, `bx_tasks_reports`, `bx_tasks_reports_track`, `bx_tasks_time`, `bx_tasks_time_track`, `bx_tasks_timers`, `bx_tasks_favorites_track`, `bx_tasks_scores`, `bx_tasks_scores_track`;
 
 -- STORAGES & TRANSCODERS
 DELETE FROM `sys_objects_storage` WHERE `object` LIKE 'bx_tasks_%';

+ 206 - 0
modules/boonex/tasks/js/timer.js

@@ -0,0 +1,206 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT 
+ * @defgroup    Tasks Tasks
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+function BxTasksTimer(oOptions) {
+    this._sActionsUrl = oOptions.sActionUrl;
+    this._sObjName = oOptions.sObjName == undefined ? 'oBxTasksTimer' : oOptions.sObjName;
+
+    this._sAnimationEffect = oOptions.sAnimationEffect == undefined ? 'fade' : oOptions.sAnimationEffect;
+    this._iAnimationSpeed = oOptions.iAnimationSpeed == undefined ? 'slow' : oOptions.iAnimationSpeed;
+
+    this._aHtmlIds = oOptions.aHtmlIds == undefined ? {} : oOptions.aHtmlIds;
+    this._oRequestParams = oOptions.oRequestParams == undefined ? {} : oOptions.oRequestParams;
+
+    this._bMulti = oOptions.bMulti == undefined ? false : oOptions.bMulti;
+
+    this._aTimers = [];
+}
+
+BxTasksTimer.prototype.init = function (iContentId, iProfileId, bStarted) {
+    var $this = this;
+
+    $(document).ready(function () {
+        if(bStarted)
+            $this._setTimer(iContentId, iProfileId);
+    });
+};
+
+BxTasksTimer.prototype.start = function (oSource, iContentId, iProfileId) {
+    if(this._bMulti)
+        this.stopAll();
+
+    this._performAction(oSource, 'start', iContentId, iProfileId);
+};
+
+BxTasksTimer.prototype.stop = function (oSource, iContentId, iProfileId) {
+    this._clearTimer(iContentId, iProfileId);
+
+    this._performAction(oSource, 'stop', iContentId, iProfileId);
+};
+
+BxTasksTimer.prototype.stopAll = function () {
+    if(!this._aTimers)
+        return;
+
+    for(var sKey in this._aTimers) {
+        var aKey = sKey.split('-');
+        if(!aKey || aKey.length != 2)
+            continue;
+        
+        var oTimer = $('#' + this._aHtmlIds['timer'] + aKey[0] + '-' + aKey[1]);
+        if(!oTimer || !oTimer.length)
+            continue;
+
+        this.stop(oTimer.get(0), aKey[0],aKey[1]);
+    }
+};
+
+BxTasksTimer.prototype.resume = function (oSource, iContentId, iProfileId) {
+    if(this._bMulti)
+        this.stopAll();
+
+    this._performAction(oSource, 'resume', iContentId, iProfileId);
+};
+
+BxTasksTimer.prototype.log = function (oSource, iContentId, iProfileId) {
+    this._clearTimer(iContentId, iProfileId);
+
+    this._performAction(oSource, 'log', iContentId, iProfileId);
+};
+
+BxTasksTimer.prototype.clear = function (oSource, iContentId, iProfileId) {
+    var $this = this;
+
+    bx_confirm('', function() {
+        $this._clearTimer(iContentId, iProfileId);
+
+        $this._performAction(oSource, 'clear', iContentId, iProfileId);
+    });
+};
+
+BxTasksTimer.prototype.onPerformActionStart = function(oData) {
+    this._onPerformActionAndSet(oData);
+};
+BxTasksTimer.prototype.onPerformActionStop = function(oData) {
+    this._onPerformAction(oData);
+};
+BxTasksTimer.prototype.onPerformActionResume = function(oData) {
+    this._onPerformActionAndSet(oData);
+};
+BxTasksTimer.prototype.onPerformActionLog = function(oData) {
+    this._onPerformAction(oData);
+};
+BxTasksTimer.prototype.onPerformActionClear = function(oData) {
+    this._onPerformAction(oData);
+};
+
+BxTasksTimer.prototype.loadingInButton = function(e, bShow) {
+    if($(e).length)
+        bx_loading_btn($(e), bShow);
+    else
+        bx_loading($('body'), bShow);
+};
+
+BxTasksTimer.prototype.loadingInTimer = function(e, bShow) {
+    var oParent = $(e).length ? $(e).parents('.bx-tasks-timer:first') : $('body'); 
+    bx_loading(oParent, bShow);
+};
+
+BxTasksTimer.prototype.loadingInBlock = function(e, bShow) {
+    var oParent = $(e).length ? $(e).parents('.bx-db-container:first') : $('body'); 
+    bx_loading(oParent, bShow);
+};
+
+BxTasksTimer.prototype._performAction = function (oSource, sAction, iContentId, iProfileId) {
+    var $this = this;
+    var oDate = new Date();
+
+    this.loadingInTimer(oSource, true);
+
+    $.get(
+        this._sActionsUrl + 'process_timer/' + sAction + '/' + iContentId + '/' + iProfileId, 
+        {
+            _t: oDate.getTime()
+        },
+        function(oData) {
+            $this.loadingInTimer(oSource, false);
+
+            processJsonData(oData);
+        },
+        'json'
+    );
+
+    return false;
+};
+
+BxTasksTimer.prototype._onPerformActionAndSet = function(oData) {
+    if(oData && oData.content != undefined) {
+        var sTimer = '#' + this._aHtmlIds['timer'] + oData.content_id + '-' + oData.profile_id;
+        $(sTimer).replaceWith(oData.content);
+
+        this._setTimer(oData.content_id, oData.profile_id, $(sTimer));        
+    }
+};
+
+BxTasksTimer.prototype._onPerformAction = function(oData) {
+    if(oData && oData.content != undefined)
+        $('#' + this._aHtmlIds['timer'] + oData.content_id + '-' + oData.profile_id).replaceWith(oData.content);
+};
+
+BxTasksTimer.prototype._setTimer = function(iContentId, iProfileId, oTimer) {
+    var $this = this;
+
+    this._clearTimer(iContentId, iProfileId);
+
+    if(!oTimer)
+        oTimer = $('#' + this._aHtmlIds['timer'] + iContentId + '-' + iProfileId);
+
+    this._aTimers[iContentId + '-' + iProfileId] = setInterval(function() {
+        var oH = oTimer.find('.bx-tasks-th');
+        var oM = oTimer.find('.bx-tasks-tm');
+        var oS = oTimer.find('.bx-tasks-ts');
+
+        var iS = parseInt(oS.html()) + 1;
+        if(iS == 60) {
+            iS = 0;
+
+            var iM = parseInt(oM.html()) + 1;
+            if(iM == 60) {
+                iM = 0;
+
+                oH.html($this._padDigits(parseInt(oH.html()) + 1, 2));
+            }
+            oM.html($this._padDigits(iM, 2));
+        }
+        oS.html($this._padDigits(iS, 2));
+    }, 1000);
+};
+
+BxTasksTimer.prototype._clearTimer = function(iContentId, iProfileId) {
+    var sTimer = iContentId + '-' + iProfileId;
+    if(!this._aTimers[sTimer]) 
+        return;
+
+    clearInterval(this._aTimers[sTimer]);
+    this._aTimers = this._aTimers.splice(sTimer, 1);
+};
+
+BxTasksTimer.prototype._clearTimers = function() {
+    if(!this._aTimers)
+        return;
+
+    for(var sKey in this._aTimers)
+        clearInterval(this._aTimers[sKey]);
+
+    this._aTimers = [];
+};
+
+BxTasksTimer.prototype._padDigits = function(iNumber, iDigits) {
+    return Array(Math.max(iDigits - String(iNumber).length + 1, 0)).join(0) + iNumber;
+};

+ 28 - 0
modules/boonex/tasks/template/browse_timers.html

@@ -0,0 +1,28 @@
+__js_code__
+<div class="bx-tasks-timers-sections">
+    <div class="bx-tasks-ts-cnt space-y-4">
+        <bx_repeat:sections>
+            <div class="bx-tasks-section">
+                <div class="bx-tasks-section-ttl mb-2 text-xl">__section_title__</div>
+                <div class="bx-tasks-section-cnt flex flex-wrap -m-2">
+                    <bx_repeat:timers>
+                        <div class="bx-tasks-timer-wrp w-1/4">
+                            <div class="bx-tasks-timer bx-def-box bx-def-box-round-corners m-2">
+                                <div class="bx-tasks-timer-cnt p-4">
+                                    <div class="bx-tasks-t-task flex">
+                                        <bx_if:show_context>
+                                            <a class="bx-tasks-t-context truncate" href="__url__">__title__</a><span class="pr-1">:</span>
+                                        </bx_if:show_context>
+                                        <a class="bx-tasks-t-content truncate" href="__content_url__">__content_title__</a>
+                                    </div>
+                                    <div class="bx-tasks-t-value mt-4">__timer__</div>
+                                </div>
+                            </div>
+                            <script>__js_code_timer__</script>
+                        </div>
+                    </bx_repeat:timers>
+                </div>
+            </div>
+        </bx_repeat:sections>
+    </div>
+</div>

+ 10 - 0
modules/boonex/tasks/template/entry-timer.html

@@ -0,0 +1,10 @@
+<div id="__html_id__" class="bx-tasks-timer">
+    <div class="bx-tasks-timer-cnt">
+        <div class="bx-tasks-content flex flex-row justify-center text-2xl">
+            <div class="bx-tasks-th">__hours__</div><div>:</div>
+            <div class="bx-tasks-tm">__minutes__</div><div>:</div>
+            <div class="bx-tasks-ts">__seconds__</div>
+        </div>
+        <div class="bx-tasks-actions mt-2">__actions__</div>
+    </div>
+</div>