Создаем Time Tracker приложение с Webix

Время — один из самых ценных ресурсов. Если вы научитесь правильно управлять этим ресурсом, то работа вашей команды станет более эффективной, а это, в свою очередь, положительно скажется на результатах вашей компании.

И в этой статье я хочу рассказать о том, как создать небольшое приложение на основе компонентов Webix UI, которое поможет правильно организовать тайм-менеджмент ваших сотрудников и эффективно управлять их рабочим временем.

С кодом готового приложения и живой демкой можно ознакомиться здесь.

Webix Time Tracker

Давайте представим, что вы управляете IT отделом, в котором работа ведется в трех направлениях:

  • разработка
  • тестирование
  • документирование.

И вам, как руководителю, необходимо знать, насколько эффективно трудятся ваши подчиненные в течении рабочего дня, и сколько времени они уделяют каждой из задач.

Для этого, в нашем приложении мы предусмотрим специальную таблицу, где работники смогут просматривать назначенные им задачи по дням недели, а также сводную статистику за всю неделю.

Напротив каждой задачи текущего дня мы разместим трекер для учета времени, потраченного на ее выполнение. Работник должен будет запустить трекер перед началом выполнения задачи и остановить его после завершения. А задачи всех остальных дней будут доступны только для просмотра.

Навигацию по дням недели мы реализуем через специальные вкладки, а отображать расписание задач «по дням» и «за неделю» можно будет с помощью специального контрола на тулбаре.

Учитывая все вышесказанное, наше приложение будет состоять из 3 частей:

  • Тулбар с контролами
  • Панель навигации по дням недели
  • Панель расписания, на которой работники смогут просматривать задачи в разных режимах (по дням и за неделю) и управлять учетом времени тех задач, которые назначены им на текущий день.

В браузере приложение будет выглядеть так:

Режим “Week Days”

Week Days

Режим “Week Totals”

Week Totals

Мы определились с функционалом и интерфейсом приложения. Теперь давайте перейдем к практике и посмотрим, как реализовать все это с помощью компонентов библиотеки Webix.

Подготавливаем почву

Для работы с Webix нам необходимо подключить его ресурсы. В нашем приложении мы будем использовать стандартную GPL версию библиотеки, которую можно скачать здесь. Но вы также можете подключить необходимые файлы данной версии через CDN по следующим ссылкам:

 <script type="text/javascript" src="http://cdn.webix.com/edge/webix.js"></script>
 <link rel="stylesheet" type="text/css" href="http://cdn.webix.com/edge/webix.css">

А теперь переходим к лейауту приложения. И здесь стоит рассказать о том, что вебикс использует декларативный подход в построении интерфейса. Это значит, что каждый компонент представляет собой json объект, в котором мы описываем все его основные характеристики.

Вкладывая такие объекты друг в друга, мы формируем интерфейс приложения, который инициализируется внутри конструктора webix.ui(). Ему мы и передаем json объект с уже готовым лейаутом.

А поскольку держать один длинный json не совсем удобно, мы разобьем его на соответствующие части (здесь это ряды), каждую из которых будем хранить в переменной. Здесь стоит уточнить, что эти части мы будем разрабатывать в отдельных файлах, которые также нужно подключить в index.html.

 <script type="text/javascript" src="js/toolbar.js"></script>
 <script type="text/javascript" src="js/subbar.js"></script>
 <script type="text/javascript" src="js/datatable.js"></script>

В конечном итоге, наш лейаут будет состоять из трех рядов и выглядеть следующим образом:

webix.ui({
  rows:[
    toolbar,
    subbar,
    datatable
  ]
});

А для полной уверенности в том, что код приложения начнет выполняться после полной загрузки HTML страницы, нам необходимо обернуть конструкцию лейаута методом webix.ready(function(){}). Выглядит это следующим образом:

webix.ready(function(){
  webix.ui({
    rows:[
      ...
    ]
  });
});

Мы подключили необходимые ресурсы и определились с лейаутом приложения. А сейчас самое время приступить к разработке соответствующих частей интерфейса и его функционала.

Создаем панель расписания

Для создания панели с расписанием лучше всего подойдет виджет DataTable. В таблице нам нужно будет визуализировать следующую информацию:

  • задача, над которой будет работать сотрудник
  • проект, к которому относится задача
  • направление, к которому относится задача (разработка, тестирование или документация)
  • дата назначения задачи
  • время, потраченное на выполнение задачи
  • трекер, который будет запускать и останавливать учет времени, потраченного на выполнение задачи.

И того, в нашей таблице будет 6 столбцов. Давайте посмотрим как это реализовать в коде.

В файле datatable.js мы создаем таблицу и сохраняем ее в переменную datatable. Базовая конфигурация виджета будет выглядеть так:

const datatable = {
  view:"datatable",
  id:"table",
  url:"./data/data.json",
  columns:[...]
}

Сам компонент нужно объявлять с помощью выражения view: «datatable». После этого, следует указать уникальный id, который позволит получать доступ к таблице (например, для вызова ее методов).

Чтобы загрузить данные о расписании, которые находятся на удаленном сервере, следует указать свойство url и присвоить ему необходимый путь. Стоит учитывать, что вместо этого пути мы также можем указать скрипт, который должен вернуть соответствующие данные в формате json. А если бы данные уже находились на клиенте, то мы бы использовали свойство data вместо url.

Данные о расписании представляют собой массив задач, о каждой из которых доступна следующая информация:

{
  "id":"1",
  "task":"Create a toolbar with controls",
  "project":"Book Store App",
  "department":"Development",
  "date":"08/23/2021",
  "time":5000000
}

А теперь давайте распределим эти данные по соответствующим столбцам нашей таблицы.

Настраиваем столбцы таблицы

Для настройки столбцов нам нужно задать соответствующие параметры в массиве свойства columns.
Чтобы отобразить в столбце нужные нам данные, в его настройках необходимо указать свойство id и присвоить ему специальный ключ, под которым хранятся эти данные.

columns:[
  { id:"task", ... },
   ...
]

Для каждого из столбцов мы задаем название хедера через его свойство header, а также фиксированную ширину через свойство width для всех столбцов кроме первого. Дело в том, что названия задач, которые будут находится в этом столбце, скорее всего будут занимать больше места чем остальные данные. Исходя из этого, мы дадим ему максимум возможного пространства, позволив занять всё оставшееся место. И для этого, в настройках первого столбца мы укажем свойство fillspace в значении true.

columns:[
  { id:"task", header:"Task", fillspace:true },
  { id:"project", header:"Project", width:140 },
  ...
]

Форматируем даты

А сейчас давайте перейдем к столбцу, в котором будет отображаться дата назначения каждой задачи. И здесь у нас возникает небольшая загвоздка. Дело в том, что даты приходят от сервера в виде строк типа «08/23/2021». А поскольку работать со строковыми датами в JS не самое приятное занятие, то лучше будет сразу перевести их в соответствующие JS Date объекты. Тем более, что нам придется форматировать даты и использовать их для фильтрации таблицы.
Чтобы это реализовать, в конструкторе виджета мы определяем свойство scheme. В объекте этого свойства нам необходимо указать специальный ключ $init и присвоить ему коллбэк, который выполнится после загрузки данных, но перед их отрисовкой. Внутри коллбэка мы переопределяем строковое значение входящей даты в соответствующий объект Date с помощью метода dateFormatDate() объекта webix.i18n.

{
  view:"datatable",
  ...
  scheme:{
    $init:function(obj){
      obj.date = webix.i18n.dateFormatDate(obj.date);
    }
  }, ...
}

Теперь все даты будут попадать в столбец в виде соответствующих объектов вместо строк. Но нам нужно отобразить их в формате Mon 23 08 2021.
А для этого, в настройках столбца «Date» нам следует указать специальное свойство format и присвоить ему метод dateToStr() объекта webix.Date. Метод принимает необходимый формат даты в виде набора ключей «%D %d %m %Y» и возвращает дату в виде соответствующей строки.

{ id:"date", header:"Date", format:webix.Date.dateToStr("%D %d %m %Y") }

Вот таким образом в таблицу попадают даты в виде соответствующих JS Date объектов, а мы их отрисовываем в виде строк нужного нам формата.

Создаем шаблоны

Помимо столбца с датами, у нас также есть несколько столбцов, данные которых нужно отобразить не так, как они приходят с сервера. На этот случай у таблицы предусмотрено специальное свойство template. С его помощью мы можем задать определенный шаблон, по которому виджет отобразит данные того или иного столбца.

Этому свойству необходимо присвоить либо строку с определенным шаблоном, либо функцию, которая будет его возвращать.

И такой шаблон нам сперва следует задать для столбца «Time», в котором будет отображаться время, потраченное на выполнение каждой задачи. Данные о времени приходят от сервера в миллисекундах. Нам же нужно отобразить их в виде соответствующей строки «00:00:00». Для этого, в настройках столбца мы задаем следующий шаблон:

{ id:"time", header:"Time", template:function(obj){ return timeToString(obj.time) }

Функция timeToString() внутри шаблона будет принимать время в миллисекундах в качестве параметра. Стоит уточнить, что это время хранится в объекте данных каждого ряда под ключом time. Так вот функция обработает полученное значение, разобьет его на отдельные сегменты (часы, минуты и секунды) и вернет время в виде соответствующей строки. Выглядит это следующим образом:

function timeToString(time){ //time in ms
  if(time){
    const diffInHrs = time / 3600000;
    const hh = Math.floor(diffInHrs);

    const diffInMin = (diffInHrs - hh) * 60;
    const mm = Math.floor(diffInMin);

    const diffInSec = (diffInMin - mm) * 60;
    const ss = Math.floor(diffInSec);

    const formattedHH = webix.Date.toFixed(hh);
    const formattedMM = webix.Date.toFixed(mm);
    const formattedSS = webix.Date.toFixed(ss);

    return `${formattedHH}:${formattedMM}:${formattedSS}`;
  }else{
    return "00:00:00";
  }
}

Здесь нужно учитывать, что время будет обновляться каждый раз, когда значение obj.time конкретного ряда будет меняться трекером. Но об этом мы поговорим чуть позже.

А сейчас мы переходим к столбцу «Department», где будут отображаться названия отделов к которым относится задача. Чтобы облегчить восприятие, давайте добавим иконку тега перед названием отделов и для каждого из них зададим определенный цвет через свойство cssFormat.

{
  id:"department",
  header:"Department",
  template:"<span class='mdi mdi-tag'></span> #department#",
  cssFormat:markDepartment
}

Функция markDepartment, которую мы задали свойству cssFormat, будет проверять названия отделов каждого ряда и выставлять цвет шрифта для текста и тега (поскольку это обычная font-based иконка). Функция будет выглядеть следующим образом:

function markDepartment(value, obj){
  if(obj.department == "Development"){
    return { "color":"green" };
  }else if(obj.department == "QA"){
    return { "color":"orange" };
  }else{
    return { "color":"blue" };
  }
}

И в последнем столбце нам осталось реализовать кнопку, с помощью которой сотрудник будет запускать учет времени, потраченного на конкретную задачу. Для этого мы создадим специальную функцию togglePlayPauseButton() и присвоим ее свойству template в настройках столбца.

{ id:"tracker", header:"Tracker", template:togglePlayPauseButton }

Функция-шаблон будет выглядеть следующим образом:

function togglePlayPauseButton(obj){
  return `<span class="
    webix_button webix_primary webix_icon
    toggleplaypause
    mdi ${obj.$isRunning ? "
mdi-pause-circle" : "mdi-play-circle"}
    "
></span>`;
}

Она будет считывать значение маркера $isRunning той задачи, для которой отображается текущая кнопка. Этот маркер присваивается каждой задаче динамически в ходе работы приложения и будет сигнализировать о том, запущен для нее трекер или нет, а следовательно в каком состоянии надо отобразить кнопку — play или pause. Для удобства мы зададим кнопке с иконкой mdi-pause-circle красный цвет, чтобы визуально выделить ее состояние. В приложении это можно сделать через обычный CSS.

Такой же цвет мы определим и для времени, которое будет отсчитываться для текущей задачи в столбце «Time». А сделать это можно с помощью уже знакомого нам свойства cssFormat.

Для этого мы создадим новую функцию markTime(), в которой будем проверять состояние маркера $isRunning. Если маркер существует (трекер запущен), то время будет красным, если нет — черным. Функция будет выглядеть следующим образом:

function markTime(value, obj){
  if(obj.$isRunning){
    return { "color":"red" };
  }else{
    return { "color":"black" };
  }
}

Подсчитываем общее время в футере

Теперь у нас есть интерфейс для подсчета времени, потраченного на каждую задачу. Согласитесь, что было бы очень удобно подсчитывать также общее количество потраченного времени за день и за неделю. Давайте сделаем такую сводку в футере таблицы.

По умолчанию футера в таблице нет. Чтобы его добавить, в конструкторе виджета нам необходимо указать свойство footer в значении true. После этого, в настройках нужных столбцов мы также указываем свойство footer и задаем ему необходимый контент.

Для первого столбца мы определяем название нашей сводки через конфигурацию footer:{ text:»Total:»}.

Теперь возвращаемся к настройкам столбца «Time», где отображается время, потраченное на каждую задачу, которое нам нужно подсчитать. И здесь стоит рассказать о том, что у Webix есть встроенные элементы для хедеров и футеров таблицы. Мы можем задать для них специальный счетчик, который будет подсчитывать и выводить сумму всех значений столбца. Для этого, в настройках конкретного столбца нужно задать конфигурацию footer:{content:»summColumn» }.

Но такой расклад нас не совсем устраивает, поскольку учет времени ведется в миллисекундах, а их суммарное количество будет выглядеть не очень информативно. Нам же необходимо отформатировать итоговое время и отобразить его в виде соответствующей строки.

Поэтому, давайте создадим собственный счетчик на основе summColumn, который будет подсчитывать все значения столбца и форматировать полученный результат перед его отрисовкой. Выглядит это следующим образом:

webix.ui.datafilter.totalTime = webix.extend({
  refresh:function(master, node, value){
    let result = 0;
    master.mapCells(null, value.columnId, null, 1, function(value){
      value = value*1;
      if(!isNaN(value))
        result+=value;
      return value;
    });
    node.innerHTML = timeToString(result);
  }
}, webix.ui.datafilter.summColumn);

Метод refresh нового счетчика будет высчитывать сумму значений всех полей этого столбца, а затем отформатирует полученный результат с помощью функции timeToString(). Напомню, что эта функция переводит полученное количество миллисекунд в соответствующую строку времени. А происходить это будет при каждом изменении значения в любой из ячеек этого столбца.

И нам остается только добавить этот счетчик в настройки столбца «Time» в виде конфигурации footer:{ content:»totalTime»}.

Мы создали таблицу, загрузили данные о расписании и настроили необходимые контролы. Теперь в браузере мы можем увидеть такой результат:

По сути, интерфейс для работы трекера у нас готов. А теперь давайте настроим его окружение, а именно:

  • добавим возможность просматривать задачи по разным дням недели
  • добавим возможность просматривать сводные данные всей недели.

Создаем панель навигации

Панель навигации будет состоять из вкладок для перехода по дням текущей недели и стрелок для переключения к предыдущей и следующей неделе. А вкладку с текущим днем мы пометим специальной иконкой. Давайте сначала определимся с лейаутом панели, а затем перейдем к разработке ее элементов.

Вкладки для навигации по дням недели мы создадим с помощью компонента tabbar и сохраним его в переменную tabbar. Стрелки для переключения недель мы реализуем с помощью двух элементов icon и разместим их с обеих сторон таббара.

Все вышеперечисленное нам необходимо поместить в отдельный лейаут и сохранить его в переменную subbar в файла subbar.js. Напомню, что эту переменную мы используем для сборки лейаута всего приложения в файле index.html. Код лейаута будет выглядеть следующим образом:

const subbar = {
  id:"navigation",
  cols:[
    { view:"icon", icon:"wxi-angle-left" },
    tabbar,
    { view:"icon", icon:"wxi-angle-right" }
  ]
}

В браузере мы увидим такой результат:

Для этой панели мы задали специальный id, с помощью которого будем прятать и показывать навигацию при смене режимов просмотра. Но всему свое время.

Теперь давайте займемся непосредственно вкладками для переключения дней недели. Как я уже упоминал выше, для их создания мы будем использовать компонент tabbar. Объявляется он с помощью выражения view: «tabbar». Чтобы получить доступ к виджету, мы зададим ему уникальный id, а заодно установим фиксированную высоту через свойство height и уберем границы с помощью свойства borderless.

const tabbar = {
  view:"tabbar",
  id:"tabbar",
  borderless:true,
  height:45
}

А сейчас нужно определиться с опциями для соответствующих вкладок с днями недели. Задавать их нужно через свойство options в виде массива. И здесь нам придется прибегнуть к небольшой хитрости.

Дело в том, что при смене вкладок мы будем фильтровать задачи в таблице по дате их назначения. Исходя из этого, было бы неплохо, чтобы каждая опция таббара содержала дату, по которой мы будем показывать задачи при клике на эту опцию. И лучше всего распределять эти даты динамически. Сейчас расскажу как это реализовать.

По умолчанию наш таббар будет показывать все дни недели. Нам же нужно знать, какой день именно сегодня. Для удобства, давайте сохраним текущую дату в переменной today_date в файле index.html.

<script>
  const today_date = new Date(2021, 7, 24);
</script>

И здесь стоит уточнить, что в реальном проекте, текущая дата будет определяться методом new Date(). Мы же определяем дату статично, поскольку наши данные находятся в диапазоне 1 недели.

Для формирования массива с опциями нам нужно получить индекс текущего дня недели и сохранить его в переменную day_num. Для этого мы используем метод getDay() объекта Date сегодняшней даты.

const day_num = today_date.getDay();

Нужно учитывать, что по умолчанию Webix использует локаль «en-US», где неделя начинается с воскресенья, индекс которой равен 0. Если же у нас установлен вторник, то его индекс будет равен 2.

Для создания опций нам также необходимо получить дату начала недели, к которой относится текущая дата, и сохранить ее в переменную week_start. Сделать это можно с помощью метода weekStart() объекта webix.Date.

const week_start = webix.Date.weekStart(today_date);

Когда все переменные у нас подготовлены, давайте сформируем массив опций для таббара. Объект каждой опции будет содержать следующие поля:

  • id — id опции
  • value — полное название дня
  • icon — иконка (только для сегодняшнего дня)
  • date — дата.

Чтобы создать массив опций с вышеупомянутыми полями для всех недели, мы воспользуемся методом map(), который вызовем для массива dayFull объекта calendar. Этот массив уже содержит полные названия всех дней недели, которая в текущей локали начиная с воскресенья. Выглядит это следующим образом:

const options = webix.i18n.calendar.dayFull.map((day, i) => {
  return {
    id: i,
    value: day,
    icon: i == day_num ? "mdi mdi-calendar-today" : "",
    date: webix.Date.add(week_start, i, "day", true)
  }
});

Как вы видите, иконку мы задаем только для сегодняшнего дня, индекс которого соответствует переменной цикла. А дату для каждого дня мы определяем с помощью метода add() объекта webix.Date. Этот метод позволяет добавить нужное количество дней к исходной дате, которая у нас хранится в переменной week_start (начало недели).

Когда массив с опциями вкладок сформирован, его следует задать свойству options таббара, а заодно определить вкладку по умолчанию с помощью его свойства value. Конструктор виджета будет выглядеть следующим образом:

const tabbar = {
  view:"tabbar",
  id:"tabbar",
  borderless:true,
  height:45,
  value:day_num,
  options:options
}

В таббаре нам осталось реализовать еще одну фичу. Речь идет о том, что при переключении вкладок мы будем фильтровать расписание задач таблицы по дате их назначения. Для этого нам нужно установить обработчик на событие onChange, которое будет срабатывать при переключении вкладок. Сделать это можно через объект свойства on нашего таббара.

{
  view:"tabbar",
    ...
  on:{
    onChange:filterDatatable
  }
}

Функция-обработчик будет выглядеть следующим образом:

function filterDatatable(){
  const tabbar = $$("tabbar");

  const value = tabbar.getValue();
  const current_date = tabbar.getOption(value).date;


  table.filter(function(obj){
    return webix.Date.equal(obj.date, current_date);
  });
}

Сначала нам нужно получить дату активной вкладки, по которой мы будем фильтровать таблицу. Для этого мы получаем id этой вкладки через метод таббара getValue() и передаем его в качестве аргумента методу getOption(), который вернет объект вкладки, где и хранится дата.

После этого, нам необходимо отфильтровать расписание таблицы по дате активной вкладки. И здесь мы воспользуемся методом filter(), который будет возвращать только те задачи, даты которых будут совпадать с датой вкладки.

Теперь пользователи смогут переключаться по дням недели, а таблица будет фильтровать задачи по соответствующей дате.

И здесь было бы логично запретить запуск трекера по всем дням кроме сегодняшнего. А для этого мы можем просто спрятать столбец с кнопками таймера, и показывать его только для вкладки с текущей датой.

Сначала нам нужно сравнить текущую дату и дату активной вкладки с помощью метода equal() объекта webix.Date. Если даты совпадают, то мы проверяем видимость столбца «Tracker» через метод таблицы isColumnVisible() и отображаем его с помощью метода showColumn().

Если даты не совпадают и столбец «Tracker» отображается, то мы его прячем другим методом таблицы hideColumn().

А функция теперь будет выглядеть следующим образом:

function filterDatatable(){
  const tabbar = $$("tabbar");

  const value = tabbar.getValue();
  const current_date = tabbar.getOption(value).date;

  const table = $$("table");
  const isVisible = table.isColumnVisible("tracker");

  if(webix.Date.equal(today_date, current_date)){
    if(!isVisible)
      table.showColumn("tracker");
  }else if(isVisible)
    table.hideColumn("tracker");

  table.filter(function(obj){ // unfilter the table
    return webix.Date.equal(obj.date, current_date);
  });
}

Вот так мы реализовали навигацию по дням недели и настроили фильтрацию расписания по соответствующим датам этих дней. Но и это еще не все. Дело в том, что при первой загрузке таблица будет отображать все задачи сразу, в то время как активная вкладка на таббаре будет соответствовать текущей дате.

Чтобы это исправить, нам нужно отфильтровать таблицу по дате сегодняшнего дня сразу после загрузки в нее данных. Для этого нам следует добавить свойство ready в конструктор виджета datatable и указать функцию filterDatatable() внутри коллбэка этого свойства.

{
  view:"datatable",
    ...
  ready:function(){
    filterDatatable();
  }
}

Теперь данные будут фильтроваться сразу после инициализации компонента и при переключении дней недели на таббаре. А в браузере мы увидим такой результат:

Создаем тулбар

А сейчас давайте реализуем просмотр статистики задач за неделю. Для этого мы предусмотрим специальный контрол на тулбаре. Здесь сразу стоит уточнить, что тулбар нашего приложения будет состоять из трех элементов:

  • лейбл с названием приложения
  • лейбл с текущей датой
  • тогл-кнопка для просмотра задач «по дням» и «за неделю».

В браузере он будет выглядеть следующим образом:

Toolbar

В файле toolbar.js мы создаем тулбар и сохраняем его в переменную toolbar. Сам виджет объявляется с помощью выражения view: «toolbar». Мы можем задать ему встроенную стилизацию через свойство css и фиксированную высоту через свойство height.

А чтобы расположить содержимое виджета в виде столбцов, нам необходимо поместить нужные элементы в массив свойства cols. Конструктор виджета будет выглядеть следующим образом:

const toolbar = {
  view:"toolbar",
  css:"webix_dark",
  height:45,
  cols:[]
}

Внутри массива cols нам нужно отобразить лейбл виджета и сегодняшнюю дату, а также создать тогл-кнопку, которая будет переключать вид таблицы из сводной статистики за неделю на расписание по дням и обратно.

Мы объявляем лейблы с помощью выражения view: «label» и задаем им фиксированную ширину через свойство width. Название каждого лейбла следует указать через его свойство label. Для первого компонента с названием приложения это будет строка «Webix Time Tracker». А для компонента с текущей датой мы применим метод dateToStr(«%l %d %m %Y») объекта webix.Date, а затем передадим ему текущую дату, которую метод отобразит по указанному шаблону.

cols:[
  { view:"label", width:150, label:"Webix Time Tracker"},
  {}, // spacer
  { view:"label", width:190, label:webix.Date.dateToStr("%l %d %m %Y")(today_date) },
  {}, // spacer
  … // toggle button
]

И у нас остается только тогл-кнопка для переключения режимов отображения. Ее конструктор будет выглядеть следующим образом:

{
  view:"toggle",
  width:130,
  offLabel:"Week Totals",
  onLabel:"Week Days"
}

У этого виджета есть 2 состояния: on и off. Название для каждого из них можно задать через соответствующие свойства onLabel и offLabel.

А теперь давайте разберемся с функционалом кнопки. При смене ее состояния, нам нужно будет показывать соответствующий режим панели расписания. Если кнопка включена (установлен режим «Week Totals»), то нам необходимо отобразить все задачи этой недели, предварительно скрыв столбец «Tracker» таблицы и панель навигации. Если кнопка выключена (установлен режим «Week Days»), то мы показываем панель навигации и фильтруем таблицу по дате активной вкладки.

Чтобы все это реализовать, нам следует установить обработчик на событие onChange нашей кнопки. А сделать это можно в объекте ее свойства on.

{
  view:"toggle",
  …,
  on:{
    onChange:toggleHandler
  }
}

}

Функция-обработчик этого события будет выглядеть следующим образом:

function toggleHandler(value){
  const table = $$("table");
  const nav = $$("navigation");
  if(value){
    nav.hide();
    table.filter();
    if(table.isColumnVisible("tracker"))
      table.hideColumn("tracker");

  }else{
    nav.show();
    filterDatatable();
  }
}

Здесь стоит уточнить, что панель навигации мы прячем и показываем с помощью соответствующих методов show() и hide(). А метод таблицы filter(), вызванный без аргументов, будет сбрасывать фильтрацию ее данных в исходное состояние.

Но нужно помнить, что в реальном проекте мы бы фильтровали данные недели по датам от понедельника по пятницу. А поскольку в нашей базе присутствуют данные только на 1 неделю (в качестве образца), то мы просто сбрасываем фильтрацию таблицы.

Теперь мы можем переключаться между режимами «Week Days» и «Week Totals» с помощью соответствующего контрола на тулбаре.

Запускаем трекер

И нам осталось реализовать еще одну важную фичу приложения, а именно — настроить трекер для запуска и остановки учета времени. Хочу напомнить, что мы создали соответствующие контролы в столбце «Tracker», с помощью которых и будем управлять трекером. Давайте сделаем так, чтобы при первом клике по кнопке наш трекер запускался, а при повторном — останавливался.

Для этого, нам сначала нужно установить обработчик на событие клика по кнопке с помощью специального свойства onClick таблицы. Здесь нужно уточнить, что при создании кнопки мы указали ей специальный css класс toggleplaypause, который нам теперь пригодится.

Внутри объекта свойства onClick мы указываем этот css класс и присваиваем ему соответствующий обработчик.

{
  view:"datatable",
  …,
  onClick:{
    toggleplaypause:togglePlayPauseHandler
  }
}

Теперь давайте создадим этот обработчик. А выглядеть он будет следующим образом:

let tracker_state = 0;

function togglePlayPauseHandler(e, id){
  if(tracker_state == id.row){
    pauseTimer.call(this);
  }else{
    pauseTimer.call(this);
    playTimer.call(this, id.row);
  }
  return false;
}

Для работы этой функции нам нужно где-то хранить id задачи, на которой запущен трекер. Исходя из этого, мы заранее создаем переменную tracker_state за пределами функции, и будем присваивать ей нужное значение при каждом запуске нового трекера, а также сбрасывать ее при остановке текущего.

При каждом клике по кнопке нам нужно будем сравнивать id ряда, в котором она находится, со значением этой переменной.

Если значения совпадают (трекер запущен), то нам нужно остановить текущий трекер с помощью функции pauseTimer().

Если же значения не совпадают (трекер не запущен), запускаем новый трекер с помощью функции playTimer() и передаем ей id текущего ряда. После этого, нам следует остановить предыдущий трекер (если он запущен) с помощью функции pauseTimer().

И в завершение, нам необходимо предотвратить вызов других событий, связанных с кликом по кнопке (например селект ряда), вернув из этой функции значение false.

А теперь давайте посмотрим как создать вышеупомянутые функции playTimer() и pauseTimer(), которые будут запускать и останавливать наш трекер.

Функция для запуска трекера

Функция playTimer() будет запускать трекер для учета времени текущего ряда. Ее код будет выглядеть следующим образом:

function playTimer(row_id){
  const item = this.getItem(row_id);
  let time = item.time,
      interval = item.$interval,
      start = Date.now() - time;

  interval = setInterval( () => {
    time = Date.now() - start;
    this.updateItem(row_id, { time:time });
  }, 1000); // update 1 time per second

  this.updateItem(row_id, { $isRunning:true, $interval:interval });
  tracker_state = row_id; // redefine state
}

Для управления трекером нам понадобится еще 1 служебная переменная $interval, которая будет хранить id таймера. Значение этой переменной нам понадобится для остановки уже запущенного таймера. Ее мы будем обновлять при каждом вызове таймера (setInterval()) в нашей функции playTimer().

А теперь возвращаемся обратно к функции. С помощью метода getItem() мы получаем объект нужной задачи по ее id. В этом объекте находятся необходимые нам значения obj.time и obj.$interval. Дальше нам следует объявить несколько локальных переменных, которые понадобятся для управления таймером, а именно:

  • time (количество прошедшего времени в миллисекундах, которое хранится в объекте ряда под ключом time)
  • interval (id таймера, которое также хранится в объекте ряда под ключом $interval)
  • start (время в миллисекундах, с которого нужно начинать отсчет таймера). Значение этой переменной определяется разницей между количеством миллисекунд, прошедших с 1970 года и временем работы текущего таймера.

После этого, мы запускаем таймер setInterval() с задержкой в 1 секунду (1000 миллисекунд) и внутри метода рассчитываем время для переменной time. Ее значение будет определяться разницей между текущим временем (Date.now()) и временем из переменной start.

Чтобы отобразить полученный результат в соответствующем ряду таблицы, мы сохраняем время таймера в объект этого ряда и обновляем его с помощью метода таблицы updateItem().

После запуска самого таймера мы обновляем значения переменной $isRunning, чтобы обновить статус копки трекера. а заодно сохраняем id таймера в переменную $interval. Делаем мы это с помощью метода updateItem() нашей таблицы.

И в конце функции мы сохраняем id текущего ряда в переменную tracker_state, чтобы оставить его при повторном клике или запуске нового трекера.

С запуском таймера мы разобрались. Теперь давайте займемся функцией, которая будет его останавливать.

Функция для остановки трекера

Функция pauseTimer() будет останавливать трекер ряда, id которого хранится в переменной tracker_state. Ее код будет выглядеть следующим образом:

function pauseTimer(){
  if(tracker_state){
    const interval = this.getItem(tracker_state).$interval;        
    clearInterval(interval);
    this.updateItem(tracker_state, { $isRunning:false });
    tracker_state = 0;
  }
}

В самом начале функции мы проверяем существует ли переменная tracker_state. Если да, то один из трекеров уже запущен и значение этой переменной соответствует id ряда, в котором находится интересующий нас трекер.

Для остановки этого трекера нам сначала нужно получить значение переменной $interval, которая хранится в объекте текущего ряда. Сделать это можно с помощью метода таблицы getItem(), который принимает id задачи, хранящийся в переменной tracker_state.

После этого, мы останавливаем трекер с помощью метода clearInterval(), которому передаем полученное значение свойства $interval.

А уже после остановки трекера, мы сбрасываем состояние его контрола, переопределяя свойство $isRunning объекта текущей задачи с помощью метода updateItem() таблицы. Метод перерисует ряд, и это свойство будет учтено темплейтом столбца, который мы задали через функцию-шаблон togglePlayPauseButton().

function togglePlayPauseButton(obj){
  return `<span class="webix_button
    webix_primary toggleplaypause
    webix_icon mdi ${obj.$isRunning ? "
mdi-pause-circle" : "mdi-play-circle"}
    "
></span>`;
}

И в конце функции мы сбрасываем переменную tracker_state, чтобы этот трекер можно было запустить повторно.

Вот и все. Теперь ваши сотрудники могут запускать и останавливать трекер любой задачи, назначенной им на сегодня.

Заключение

С кодом готового приложения и живой демкой можно ознакомиться здесь.

В это статье вы научились создавать небольшое приложение для отслеживания времени на основе компонентов библиотеки Webix. Узнать больше о возможностях работы с виджетами Webix вы можете в обширной документации библиотеки.