Управление задачами в проектной организации

Потребовал главный инженер автоматизировать процесс управления задачами. Поручений своим подчиненным он дает много, но вручную контролировать процесс их исполнения просто нереально. Помнить все поручения тем более невозможно. Поэтому сразу настоял на использовании подходящих средств автоматизации.
079320553ba84d3fa3faf4868514cb9d.png
Нетривиальные задачи потребовали нетривиального подхода. Подробное описание с картинками и исходным кодом под катом.
Проектная организация, численностью порядка 200 человек. В высшем руководстве 4 человека, в прямом подчинении главного инженера 4 главных инженера проекта (ГИП) и столько же их помощников, плюс все начальники отделов.

Главный инженер в день может поручать от 5 до 15 задач своим прямым подчиненным, которые, в свою очередь, могут делегировать выполнение задач нескольким начальникам отделов, а те своим подчиненным. Классическая иерархическая схема. Таким образом, количество активных задач в единицу времени может достигать 600–800! Удержать их все в голове просто нереально, а в условиях хромающей исполнительской дисциплины вопрос контроля становится жизненно важным.

В организации в тот момент уже несколько лет использовался MS Project, правда, для управления проектами в целом, а не краткосрочными поручениями. Идею использовать его для управления краткосрочными поручениями отмели после краткого обсуждения.
Учитывая наличие опыта использования системы easla.com для управления корреспонденцией решили попробовать использовать ее же для управления задачами. Тем более, задачи предполагали тесную интеграцию с перепиской.

Задача


В смысле, не задача, а постановка задачи. Изначально все-таки планировалось сделать задачи простыми: тема, описание, автор, исполнитель, плановые и фактические даты. Поэтому и требования были простыми:

  • Регистрировать задачу в системе
  • Автоматически определять автора и позволять выбирать любого исполнителя
  • Автоматически вычислять плановые даты
  • При смене статусов фиксировать фактические даты.


Немного позже, уже во время пробной эксплуатации, потребовалось расширить функционал и требований стало больше:

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


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

Решение


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

  • Высокая (8 рабочих часов, т.е. рабочий день)
  • Обычная (40 рабочих часов, т.е. рабочая неделя)
  • Низкая (80 рабочих часов).


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

  • Нисколько
  • Секунды
  • Несколько минут
  • 15 минут
  • Полчаса
  • 45 минут
  • Целый час
  • Больше часа и т.д.


Определившись с инструментом и принципами работы процесса, приступил к реализации.

Реализация


В easla.com создал новый процесс «Задачи». В нем создал объект «Задача». Объект наделил следующими атрибутами.

Атрибуты


Номер
Обычный счетчик для последовательной нумерации.Обозначение
Строковый атрибут. Значение вычисляется после создания задачи и не может быть изменено пользователем. В режим «только для чтения» атрибут переводится в скрипте «При инициализации»:

cobjectref()->attributeref('tsk_task_code')->readonly = true;

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

$src_users = corganization()->users();
$end_users = array();
foreach ($src_users as $u)
  $end_users += array($u->id => $u->description);
cobjectref()->attributeref('tsk_task_author')->values = $end_users; 
cobjectref()->attributeref('tsk_task_author')->value = cuser()->id;
cobjectref()->attributeref('tsk_task_author')->readonly = true;


Атрибуту присваивается значение активного пользователя и режим «только для чтения», чтобы не было возможности создать задачу от другого сотрудника.Исполнитель
Сотрудник организации, которому поручено выполнение задачи. Множественный атрибут, т.к. одна и та же задача может выполняться разными специалистами, а, скажем, ГИП соберет все вместе в одно решение. Список сотрудников формируется в скрипте «При инициализации»:

$src_users = corganization()->group('group_all')->users();
$end_users = array();
foreach ($src_users as $u)
    if ($u['islocked'] == 0)
        $end_users += array($u->id => $u->description);
asort($end_users);
cobjectref()->attributeref('tsk_task_executor')->values = $end_users; 
cattributeref()->size = 6;

Договор
Ссылка на объект «Договор». Инициализация атрибута происходит скрипте объекта «Задача».Тема
Обычный строковый атрибут. По началу в него вписывали номер договора, но быстро отказались от такой практики и для номера договора ввели отдельный атрибут.Описание
Многострочный строковый атрибут для подробного описания поставленной задачи. Описание может менять только автор задачи, поэтому в скриптах «При инициализации» и «При изменении» прописано:

cattributeref()->readonly = cuser()->id != cobjectref()->attributeref('tsk_task_author')->value;

Уведомить об исполнении
Целочисленный атрибут, фактически являющийся флагом, определяющий возможность уведомления автора задачи о ее завершении. Список допустимых значений и начальное значение определяется в скрипте «При инициализации»:

cattributeref()->values = array('Нет','Да');
if (empty(cattributeref()->value))
    cattributeref()->value = 0;

Категория задачи
Классификатор, определяющий категорию задачи. Сейчас их всего три:

  • Подготовка ответа на письмо
  • Решение планерки
  • Прочее.


Список допустимых значений формируется в скрипте «При инициализации»:

$src_classificators = classificatorChilds('task_category');
$end_classificators = array();
foreach($src_classificators as $c)
  $end_classificators += array($c['id']=>$c['name']);
if (count($end_classificators) > 0)
{
  cobjectref()->attributeref('tsk_task_category')->values = $end_classificators;
  cobjectref()->attributeref('tsk_task_category')->value = key($end_classificators);
}


При изменении категории меняется «необходимость» атрибута «Основание для закрытия».

$src_classificators = classificatorChilds('task_category');
foreach($src_classificators as $c)
    if ($c['id'] == cattributeref()->value)
        break;
        
if (empty($c))
    return;
    
if ($c['code'] == 'task_category_answer') {
    cobjectref()->attributeref('tsk_task_base_open')->isRequired = true;
}

Важность
Классификатор. Список допустимых значений и начальное значение также определяется в скрипте «При инициализации»:

$src_classificators = classificatorChilds('tsk_importance');
$end_classificators = array();
foreach($src_classificators as $c)
  $end_classificators += array($c['id']=>$c['name']);
if (count($end_classificators) > 0)
{
  cobjectref()->attributeref('tsk_task_importance')->values=$end_classificators;
  cobjectref()->attributeref('tsk_task_importance')->value = array_flip($end_classificators)['Обычная'];
}


«При изменении» происходит пересчет плановой даты закрытия задачи:

if (empty(cattributeref()->value))
    return;
cobjectref()->calcPlanEndDate(cattributeref()->value);


Функция calcPlanEndDate описана в самом объекте.Основание для открытия
Ссылка на объект, в частности, входящий или исходящий документ, который и стал основанием для появления задачи. Список входящих и исходящих документов формируется в скрипте «При инициализации»:

cattributeref()->readonly = cobjectref()->attributeref('tsk_task_description')->readonly;
$base = cobjectref()->prepareIncomings();
$base += cobjectref()->prepareOutgoings();
$base = array_reverse($base, true);
cattributeref()->values = $base;

Приложения
Файловый атрибут. Используется редко, но он нужен, если к задаче необходимо приложить сопроводительные документы.Дата начала (план)
Плановые дата и время начала выполнения задачи. Договорились о том, что она будет назначаться со смещением +1 час к текущему времени, что и прописано в скрипте «При инициализации»:

cattributeref()->readonly = cobjectref()->attributeref('tsk_task_description')->readonly;
cattributeref()->value = calendarDateAdd(currentDateTime(), 3600);


Изменить может только автор. Отдельное внимание на функцию calendarDateAdd, она вычисляет плановую дату начала в соответствии с производственным календарем! Дата окончания (план)
Плановые дата и время окончания выполнения задачи. Зависит от важности и плановой даты начала. Начальное значение вычисляется «При инициализации»:

cattributeref()->readonly = cobjectref()->attributeref('tsk_task_description')->readonly;
if (empty(cattributeref()->value) && !empty(cobjectref()->tsk_task_importance)) {
    cobjectref()->calcPlanEndDate(cobjectref()->tsk_task_importance);
}

Дата начала (факт) и Дата окончания (факт)
Фактические даты начала и окончания, которые проставляются только при изменении статуса задачи.Основание для закрытия
Ссылка на объект, а именно, исходящий документ, который стал основанием для закрытия задачи. Более того, если категория задачи «Подготовка ответа на входящее», то задача не может быть закрыта, пока не будет заполнено основание для закрытия. Список доступных исходящих документов формируется в скрипте «При инициализации»:

$base = cobjectref()->prepareIncomings();
$base += cobjectref()->prepareOutgoings();
$base = array_reverse($base, true);
cattributeref()->values = $base;

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

cattributeref()->values = array(
0=>'Нисколько',
1=>'Секунды',
5=>'Несколько минут',
15=>'15 минут',
30=>'Полчаса',
45=>'45 минут',
60=>'Целый час',
75=>'Больше часа',
90=>'Полтора часа',
105=>'Почти два часа',
120=>'2 часа',
150=>'2 часа 30 минут',
360=>'3 часа',
240=>'Полдня',
480=>'Целый день',
960=>'2 дня',
1440=>'3 дня',
1920=>'4 дня',
2400=>'Рабочая неделя',
4800=>'Две недели',
7200=>'Три недели',
9600=>'Целый месяц',
19200=>'Два месяца',
19200=>'Два месяца',
28800=>'Три месяца',
);
cattributeref()->value = 0;

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

Объект


Объект «Задача» обладает непростым поведением и валидацией, которые описаны в его скриптах. Вспомогательные функции для инициализация атрибутов и вычисления даты и времени описаны в скрипте:

До инициализации объекта
function calcTskCode($num)
{
  return 'ЗАДАЧА-'.sprintf('%06d', $num);
}
function prepareContracts()
{
    $src_contracts = selectAll(
      'agr_management',
      'agr_management_contract'
    );
        
    $end_contracts = array();
    foreach ($src_contracts as $s)
      $end_contracts += array($s['id'] => $s['description']);
    asort($end_contracts);
    return $end_contracts;
}
function prepareIncomings()
{
    $src_documents = selectAll(
      'crs_management',
      'crs_management_incoming',
      array('crs_management_incoming_contragent_regnum')
    );
        
    $end_documents = array();
    foreach ($src_documents as $d)
      $end_documents += array($d['id'] => $d['description'].' ['.$d['crs_management_incoming_contragent_regnum'].']');
    //asort($end_documents);
    return $end_documents;
}
function prepareOutgoings()
{
    $src_documents = selectAll(
      'crs_management',
      'crs_management_outgoing'
    );
        
    $end_documents = array();
    foreach ($src_documents as $d)
      $end_documents += array($d['id'] => $d['description']);
    //asort($end_documents);
    return $end_documents;
}
function calcPlanEndDate($importance)
{
    if (empty($importance))
        return;
        
    $c = classificator($importance);
    if (empty($c))
        return;
    
    $delta = 0;
    switch ($c['code']) {
        case 'tsk_importance_01':
            $delta = 28800;
            break;
        case 'tsk_importance_02':
            $delta = 144000;
            break;
        case 'tsk_importance_03':
            $delta = 288000;
            break;
    }
    cobjectref()->attributeref('tsk_task_plan_enddate')->value = calendarDateAdd(currentDateTime(), $delta);
}
if (cobjectref()->hasAttributeref('tsk_task_contract'))
    cobjectref()->attributeref('tsk_task_contract')->values = prepareContracts();
cobjectref()->childTabs = array('tsk_task_sub');
cobjectref()->childAll = false;



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

Дополнительная инициализация атрибутов и вычисление состояния плановых дат начала и окончания осуществляется в скрипте:

После инициализации объекта
if (cobjectref()->hasAttributeref('tsk_task_base_open') && empty(cobjectref()->attributeref('tsk_task_base_open')->value) && !empty(cobjectref()->parentrefId))
{
    $parent = select(cobjectref()->parentrefId);
    if (!empty($parent))
        cobjectref()->attributeref('tsk_task_base_open')->value = $parent->attributeref('tsk_task_base_open')->value;
}
cobjectref()->attributeref('tsk_task_code')->value = calcTskCode(cobjectref()->attributeref('tsk_task_num')->value);
if (!cobjectref()->inFinalStatus())
{
    if (!empty(cobjectref()->tsk_task_plan_startdate) && (cobjectref()->status->code == 'tsk_task_initiated' || cobjectref()->status->code == 'tsk_task_created'))
    {
        if (cobjectref()->tsk_task_plan_startdate instanceof DateTime)
            $dts_plan = date_timestamp_get(cobjectref()->tsk_task_plan_startdate);
        else
            $dts_plan = date_timestamp_get(date_create(cobjectref()->tsk_task_plan_startdate));
            
        $dts_now = date_timestamp_get(date_create());
        if ($dts_plan < $dts_now)
            cobjectref()->attributeref('tsk_task_plan_startdate')->state = 1;
        elseif ($dts_plan - $dts_now < 3600)
            cobjectref()->attributeref('tsk_task_plan_startdate')->state = 2;
        elseif ($dts_plan - $dts_now < 28800)
            cobjectref()->attributeref('tsk_task_plan_startdate')->state = 3;
        else
            cobjectref()->attributeref('tsk_task_plan_startdate')->state = 4;
    }
    
    if (!empty(cobjectref()->tsk_task_plan_enddate) && (cobjectref()->status->code == 'tsk_task_initiated' || cobjectref()->status->code == 'tsk_task_created' || cobjectref()->status->code == 'tsk_task_processed'))
    {
        if (cobjectref()->tsk_task_plan_enddate instanceof DateTime)
            $dts_plan = date_timestamp_get(cobjectref()->tsk_task_plan_enddate);
        else
            $dts_plan = date_timestamp_get(date_create(cobjectref()->tsk_task_plan_enddate));
            
        $dts_now = date_timestamp_get(date_create());
        if ($dts_plan < $dts_now)
            cobjectref()->attributeref('tsk_task_plan_enddate')->state = 1;
        elseif ($dts_plan - $dts_now < 3600)
            cobjectref()->attributeref('tsk_task_plan_enddate')->state = 2;
        elseif ($dts_plan - $dts_now < 28800)
            cobjectref()->attributeref('tsk_task_plan_enddate')->state = 3;
        else
            cobjectref()->attributeref('tsk_task_plan_enddate')->state = 4;
    }
}
if (!empty(cobjectref()->tsk_task_author))
{
    $pgroup = array('group_pdg');
    $agroups = corganization()->user(cobjectref()->tsk_task_author)->groups();
    foreach ($agroups as $ag)
        if (in_array($ag['code'], $pgroup))
        {
            cobjectref()->attributeref('tsk_task_author')->readonly = false;
        }
    
}
if (cobjectref()->hasAttributeref('tsk_task_notice_of_execute')) {
    if (empty(cobjectref()->attributeref('tsk_task_notice_of_execute')->value)) {
        cobjectref()->attributeref('tsk_task_notice_of_execute')->value = 0;
    }
    cobjectref()->attributeref('tsk_task_notice_of_execute')->readonly = $cuser_id != cobjectref()->attributeref('tsk_task_author')->value;
}
if (cobjectref()->hasAttributeref('tsk_task_category')) {
    if (empty(cobjectref()->attributeref('tsk_task_category')->value)) {
        $values = cobjectref()->attributeref('tsk_task_category')->values;
        cobjectref()->attributeref('tsk_task_category')->value = key($values);
    }
    $category_id = cobjectref()->attributeref('tsk_task_category')->value;
    $category_classificator = classificator($category_id);
    
    if ($category_classificator->code == 'task_category_plan') {
        $ro = cuser()->id != cobjectref()->attributeref('tsk_task_author')->value;
        cobjectref()->attributeref('tsk_task_category')->readonly = $ro;
        cobjectref()->attributeref('tsk_task_plan_startdate')->readonly = $ro;
        cobjectref()->attributeref('tsk_task_plan_enddate')->readonly = $ro;
    }
}
cobjectref()->attributeref('tsk_task_plan_enddate')->readonly = $cuser_id != cobjectref()->attributeref('tsk_task_author')->value;



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

Перед сохранением объекта важно выполнить валидацию всех введенных значений и отказать в его сохранении, если что-то не так.
Помимо выявления ошибок, происходит проверка на существование похожей задачи по трем признакам: теме, основанию для открытия и исполнителю. Если найдена точно такая же задача, то в назначении новой отказано. Очень и очень полезная фишка!
Кроме этого, как было упомянуто выше, задача должна быть назначена только одному исполнителю, а все остальные в списке должны получить ее копии, поэтому задача сохраняется только с одним исполнителем, а остальные сохраняются в аргументах объекта.
Кстати, список исполнителей анализируется на наличие в нем ГИПа. И если он найден, то задача ГИПа становится основной, а все остальные создаются как подзадачи к ней. Такое упорядочивание задач очень удобно для ГИПа.

До сохранения объекта
$executors = cobjectref()->attributeref('tsk_task_executor')->value;
if (count($executors) > 1)
{
    $gips = corganization()->group('group_gip_only')->users();
    $fgip = false;
    foreach ($gips as $gip)
        if (in_array($gip['id'], $executors)) {
            $fgip = true;
            break;
        }
        
    $hgips = corganization()->group('group_gip_helper_only')->users();
    $fhgip = false;
    foreach ($hgips as $hgip)
        if (in_array($hgip['id'], $executors)) {
            $fhgip = true;
            break;
        }
    
    if ($fgip) {
        cobjectref()->attributeref('tsk_task_executor')->value = $gip->id;
        $this->arguments['executor'] = array_diff($executors, array($gip->id));
        $this->arguments['executorIsChild'] = true;
    } elseif ($fhgip) {
        cobjectref()->attributeref('tsk_task_executor')->value = $hgip->id;
        $this->arguments['executor'] = array_diff($executors, array($hgip->id));
        $this->arguments['executorIsChild'] = true;
    } else {
        cobjectref()->attributeref('tsk_task_executor')->value = $executors[0];
        $this->arguments['executor'] = array_slice($executors, 1);
    }
}
if (!empty($executors) &&
    cobjectref()->attributeref('tsk_task_executor')->existValue != cobjectref()->attributeref('tsk_task_executor')->value)
{
    $conditions = array(
        'tsk_task_subj'=>cobjectref()->tsk_task_subj,
        'tsk_task_base_open'=>cobjectref()->tsk_task_base_open,
        'tsk_task_executor'=>$executors[0],
    );
    
    if (!cobjectref()->isNewRecord)
        $conditions['id'] = '<>'.cobjectref()->id;
        
    $exist = selectAll('tsk_management', 'tsk_task', array(), $conditions);
    if (count($exist) > 0) {
        $exs_task_links = array();
        $executor = corganization()->user($executors[0]);
        foreach ($exist as $x) {
            $exs_task = select($x['id']);
            $exs_task_links[] = $exs_task->viewLink().' для '.$executor->viewLink().' Статус: '.$exs_task->status->viewLink();
        }
        throw new Exception('Невозможно назначить задачу, т.к. найдены подобные задачи:'.implode('',$exs_task_links));      
    }
}
if (cobjectref()->status->code == 'tsk_task_initiated')
{
  cobjectref()->status = 'tsk_task_created';
  cobjectref()->flags = 1;
}
elseif (!cobjectref()->isNewRecord && (cobjectref()->status->code == 'tsk_task_created' || cobjectref()->status->code == 'tsk_task_processed'))
{
    $src_user_id = cobjectref()->attributeref('tsk_task_executor')->existValue;
    $trg_user_id = cobjectref()->attributeref('tsk_task_executor')->value;
    $src_user_id = $src_user_id[0];
    $trg_user_id = $trg_user_id[0];
    if ($src_user_id != $trg_user_id) 
    {
        $conditions = array(
            'tsk_task_subj'=>cobjectref()->tsk_task_subj,
            'tsk_task_base_open'=>cobjectref()->tsk_task_base_open,
            'tsk_task_executor'=>$trg_user_id,
        );
        
        $exist = selectAll('tsk_management', 'tsk_task', array(), $conditions);
        
        if (count($exist) > 0) {
            $exs_task_links = array();
            foreach ($exist as $x) {
                $exs_task = select($x['id']);
                $exs_task_links[] = $exs_task->viewLink().' для '.corganization()->user($trg_user_id)->viewLink().' Статус: '.$exs_task->status->viewLink();
                echo 'Задача не переназначена, т.к. найдены подобные задачи для указанного сотрудника:'.implode('',$exs_task_links); 

            }
        } else {
            sendEmail(array(
                'to'=>corganization()->user($trg_user_id),
                'subj'=>cobjectref()->attributeref('tsk_task_code')->value.' переназначена',
                'body'=>'Вам переназначена задача от '.corganization()->user($src_user_id)->description.'!',
                'objects'=>cobjectref(),
                'roles'=>'tsk_executor',
                'files'=>true
            ));
            echo cobjectref()->viewLink().' успешно переназначена сотруднику '.corganization()->user($trg_user_id)->viewLink();
        }
    }
    
    cobjectref()->flags = 0;
}
else
  cobjectref()->flags = 0;
cobjectref()->description = cobjectref()->attributeref('tsk_task_code')->value;



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

$this->arguments['executor']


Таких «аргументов» в объекте можно создать сколько угодно. В моем случае хватило одного.

После сохранение объекта происходит создание клонов задач, если необходимо, и рассылка уведомлений.

После сохранения объекта
if (cobjectref()->hasAttributeref('tsk_task_base_open') && !empty(cobjectref()->attributeref('tsk_task_base_open')->value))
{
    $base = select(cobjectref()->attributeref('tsk_task_base_open')->value);
    
    if (!empty($base) && ($base->status->code == 'crs_management_incoming_handed' || $base->status->code == 'crs_management_incoming_created'))
    {
        $base->status = 'crs_management_incoming_exec';
        $base->save();
    }
}
if ((cobjectref()->status->code == 'tsk_task_created') && (cobjectref()->flags == 1))
{
    $to = cobjectref()->attributeref('tsk_task_executor')->value;
    $to = corganization()->user(is_array($to) ? $to[0] : $to);
    sendEmail(array(
        'to'=>$to,
        'subj'=>cobjectref()->attributeref('tsk_task_code')->value.' назначена',
        'body'=>'Вам назначена новая задача!',
        'objects'=>cobjectref(),
        'roles'=>'tsk_executor',
        'files'=>true
    ));
    
    echo cobjectref()->viewLink().' успешно назначена сотруднику '.$to->description;
    if (!empty(cobjectref()->attributeref('tsk_task_base_open')->value))
    {
        $base = select(cobjectref()->attributeref('tsk_task_base_open')->value);
        
        if (is_null($base))
            throw new Exception('Не найден документ указанный в основании для открытия задачи');
            
        if ($base->code == 'crs_management_incoming')
        {
            if ($base->status->code != 'crs_management_incoming_ok')
            {
                $base->status = 'crs_management_incoming_ok';
                $base->save();
            }
        }
    }
}
if (isset($this->arguments['executor'])) {
    $executors = $this->arguments['executor'];
    $ischild = isset($this->arguments['executorIsChild']) ? $this->arguments['executorIsChild'] : false;
    if (count($executors) > 0)
    {
        $new_task_links = array();
        $exs_task_links = array();
        foreach ($executors as $e) {
            $conditions = array(
                'tsk_task_subj'=>cobjectref()->tsk_task_subj,
                'tsk_task_base_open'=>cobjectref()->tsk_task_base_open,
                'tsk_task_executor'=>$e,
            );
            $exist = selectAll('tsk_management', 'tsk_task', array(), $conditions);
    
            if (count($exist) > 0) {
                foreach ($exist as $x) {
                    $exs_task = select($x['id']);
                    $exs_task_links[] = $exs_task->viewLink().' для '.corganization()->user($e)->viewLink().' Статус: '.$exs_task->status->viewLink();
                }
            } else {
                $new_task = new Objectref();
                $new_task->prepare(objectDef('tsk_management','tsk_task'));
                $new_task->attributeref('tsk_task_author')->value = cobjectref()->tsk_task_author;
                $new_task->attributeref('tsk_task_contract')->value = cobjectref()->tsk_task_contract;
                $new_task->attributeref('tsk_task_subj')->value = cobjectref()->tsk_task_subj;
                $new_task->attributeref('tsk_task_description')->value = cobjectref()->tsk_task_description;
                $new_task->attributeref('tsk_task_category')->value = cobjectref()->tsk_task_category;
                $new_task->attributeref('tsk_task_importance')->value = cobjectref()->tsk_task_importance;
                $new_task->attributeref('tsk_task_plan_startdate')->value = cobjectref()->tsk_task_plan_startdate;
                $new_task->attributeref('tsk_task_plan_enddate')->value = cobjectref()->tsk_task_plan_enddate;
                $new_task->attributeref('tsk_task_executor')->value = $e;
                $new_task->attributeref('tsk_task_comment')->value = cobjectref()->tsk_task_comment;
                $new_task->attributeref('tsk_task_base_open')->value = cobjectref()->tsk_task_base_open;
                $new_task->save();
                
                if ($ischild === true)
                    cobjectref()->childAdd($new_task);
                else {
                    $parents = cobjectref()->parents();
                    if (!empty($parents)) {
                        $p = select($parents[0]['id']);
                        $p->childAdd($new_task);
                    }
                }
                    
                $new_task_links[] = $new_task->viewLink().' для '.corganization()->user($e)->viewLink();
            }
        }
        
        if (count($exs_task_links) > 0)
            echo 'Доп. задачи не назначены, т.к. найдены подобные:'.implode('',$exs_task_links); 

    }
}
if (cobjectref()->hasAttributeref('tsk_task_notice_of_execute')) {
    if (cobjectref()->attributeref('tsk_task_notice_of_execute')->value == 1) { 
        if (cobjectref()->status->code == 'tsk_task_ok')
            sendEmail(array(
                'to'=>corganization()->user(cobjectref()->attributeref('tsk_task_author')->value),
                'subj'=>cobjectref()->attributeref('tsk_task_code')->value.' выполнена',
                'body'=>'Назначенная вами задача выполнена!',
                'objects'=>cobjectref(),
                'roles'=>'tsk_executor',
            ));
    }
}
if (cobjectref()->status->code == 'tsk_task_failed')
    sendEmail(array(
        'to'=>corganization()->user(cobjectref()->attributeref('tsk_task_author')->value),
        'subj'=>cobjectref()->attributeref('tsk_task_code')->value.' отклонена',
        'body'=>'Назначенная вами задача отклонена!',
        'objects'=>cobjectref(),
        'roles'=>'tsk_executor',
    ));



В конечном счете форма объекта стала выглядеть как-то так:
708c14958d294192945fff3af6db7a48.png

Статусы


Мудрить со статусами не стал: Создана, Принята, Выполнена, Отклонена. Принятой считается задача, с которой сотрудник ознакомился и принял к исполнению. Остальные статусы понятны из названия.
c16638c843af49d6b3c73541c251a5d2.png

Действия


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

cobjectref()->status = 'tsk_task_processed';
cobjectref()->attributeref('tsk_task_startdate')->value = currentDateTime();

Выполнить
Успешно закрывает задачу фиксируя фактическую дату окончания работы над ней. Обязательно требует заполнения комментария. Это было одно из требований главного инженера. На «первых парах» он очень возмущался, когда подчиненные закрывали задачи без комментариев. Было совершенно непонятно, что сделано и на каком основании задача закрыта.
Кроме этого, действие проверяет, является ли задача вложенной, и если так, то проверяет, все ли рядом стоящие с ней задачи выполнены. При положительном результате, проверяет статус вышестоящей задачи и при необходимости направляет ее исполнителю по почте уведомление о том, что наверняка задачу можно закрывать, т.к. все вложенные задачи выполнены.

if (cobjectref()->hasAttributeref('tsk_task_efforts')) {
    if (empty(cobjectref()->attributeref('tsk_task_efforts')->value))
        throw new Exception("Не указаны трудозатраты в задаче!");
}
$src_classificators = classificatorChilds('task_category');
foreach($src_classificators as $c)
    if ($c['id'] == cobjectref()->attributeref('tsk_task_category')->value)
        break;
        
if (empty($c))
    throw new Exception("Не найдена категория задачи!");
    
if ($c['code'] == 'task_category_answer') {
    cobjectref()->attributeref('tsk_task_base_open')->isRequired = true;
    if (empty(cobjectref()->attributeref('tsk_task_comment')->value) || empty(cobjectref()->attributeref('tsk_task_base_close')->value)) {
        echo 'Невозможно выполнить задачу категории '.$c->useLink().' при отсутствии комментария и основания для закрытия!';
        caction()->redirect = cobjectref()->updateUrl();
        return;
    } 
} elseif (empty(cobjectref()->attributeref('tsk_task_comment')->value)) {
    echo 'Невозможно выполнить задачу при отсутствии комментария!';
    caction()->redirect = cobjectref()->updateUrl();
    return;
}
if (empty(cobjectref()->attributeref('tsk_task_startdate')->value)) {
    cobjectref()->attributeref('tsk_task_startdate')->value = currentDateTime();
}
cobjectref()->attributeref('tsk_task_enddate')->value = currentDateTime();
cobjectref()->status = 'tsk_task_ok';

$parents = cobjectref()->parents();
if (!empty($parents)) {
    $parentId = $parents[0]['id'];
    $childTasks = selectAll('tsk_management','tsk_task',array(),array(
        'parents'=>$parentId,
        'status'=>array('tsk_task_initiated','tsk_task_created','tsk_task_processed')
        ));
        
    if (!empty($childTasks)) {
        $parentTask = select($parentId);
        if (in_array($parentTask->status->code, array('tsk_task_initiated','tsk_task_created','tsk_task_processed'))) {
            sendEmail(array(
                    'to'=>corganization()->user($parentTask->attributeref('tsk_task_executor')->value[0]),
                    'subj'=>$parentTask->attributeref('tsk_task_code')->value.' может быть закрыта?',
                    'body'=>'Предполагаю, что '.$parentTask->viewLink().' может быть закрыта, т.к. закрыты все вложенные в нее задачи!',
                ));
        }
    }
}

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

if (empty(cobjectref()->attributeref('tsk_task_comment')->value))
    echo 'Невозможно отклонить задачу при отсутствии комментария.';
else
{
    cobjectref()->status = 'tsk_task_failed';
    cobjectref()->attributeref('tsk_task_enddate')->value = currentDateTime();
}

Вернуть
Действие доступное только менеджеру процесса. Позволяет вернуть задачу из конечного статуса в статус «Принята». Необходима на случай, когда задачу закрыли по ошибке. Требуется редко, но все равно необходима.

cobjectref()->status = 'tsk_task_processed';
cobjectref()->attributeref('tsk_task_enddate')->value = null;

Добавить подзадачу
Предназначено для создания подзадачи к открытой задаче. Используется преимущественно ГИПами и начальниками отделов. $task = cobjectref(); $new_task = new Objectref(); $new_task->prepare(objectDef('tsk_management','tsk_task')); $new_task->parentrefId = $task->id; $new_task->attributeref('tsk_task_description')->value = $task->attributeref('tsk_task_description')->value; if ($task->hasAttributeref('tsk_task_contract')) $new_task->attributeref('tsk_task_contract')->value = $task->attributeref('tsk_task_contract')->value; if ($task->hasAttributeref('tsk_task_subj')) $new_task->attributeref('tsk_task_subj')->value = $task->attributeref('tsk_task_subj')->value; if ($task->hasAttributeref('tsk_task_category')) $new_task->attributeref('tsk_task_category')->value = $task->attributeref('tsk_task_category')->value; if ($task->hasAttributeref('tsk_task_base_open')) $new_task->attributeref('tsk_task_base_open')->value = $task->attributeref('tsk_task_base_open')->value; $new_task->attributeref('tsk_task_plan_startdate')->value = $task->attributeref('tsk_task_plan_startdate')->value; $new_task->attributeref('tsk_task_plan_enddate')->value = $task->attributeref('tsk_task_plan_enddate')->value; $new_task->status = 'tsk_task_initiated'; caction()->redirect = urlNewObjectref($new_task);

Команды


Первый процесс, в котором понадобилось создать команды. Команды отличаются от действий тем, что выполняются в контексте процесса, а не объекта. Таким образом, позволяют обрабатывать объекты «пакетно»: все сразу или выбранные пользователем.

Дело в том, что при интеграции процесса «Задачи» с «Перепиской» в исходящих письмах был реализован алгоритм, который при отправке исходящего письма в ответ на указанные входящие, комментирует задачи, созданные на основании соответствующих входящих и прописывает в их основании для закрытия отправляемое исходящее письмо. Уфф… Иными словами, задачи, которые были созданы при появлении входящего получают комментарии и заполненное основание для закрытия. Очень полезная фишка, т.к. часто, ГИП так занят, что закрывать задачу прямо сейчас ему некогда, а когда «руки дошли», ему нужно вспоминать, на каком основании он должен закрыть задачу. Понятно, что все надо делать вовремя, чтобы не забывать и не вспоминать, но уж если так случилось, то надо сократить время, затрачиваемое на восстановление картины в памяти.

В результате такого автоматического заполнения оснований для закрытия у ГИПа скапливаются задачи готовые к закрытию, осталось «только кнопочку нажать» (так было, пока главный инженер не потребовал обязательно указывать трудозатраты в каждой задаче). Когда таких готовых задач много, их закрытие превращалось в сплошное «тыкание мышкой». Поэтому была создана команда «Закрыть готовые».

Закрыть готовые
Команда ищет готовые к закрытию задачи и закрывает первые 10 из них. Не все только потому, чтобы сохранить хоть какой-то контроль над их закрытием. Были случаи, когда, закрывая таким образом задачи ГИП спохватывался, но было уже поздно и задачи приходилось возвращать вручную.

Закрыть готовые
$readyTasks = selectAll(
    'tsk_management',
    'tsk_task',
    array(),
    array(
        'tsk_task_executor'=>array('id',cuser()->id),
        'tsk_task_base_close'=>array('not like','is not null'),
        'status'=>array('and','<>tsk_task_ok','<>tsk_task_failed')
    )
);
// debugMode(true);
// debug($readyTasks);
$success = array();
$failed = array();
$max = 10;
$q = 1;
foreach ($readyTasks as $task) {
    $obj = select($task['id']);
    progress($q/$max * 100, $task['description']);
    if (!empty($obj)) {
        $obj->attributeref('tsk_task_efforts')->value = 1;
        $obj->attributeref('tsk_task_enddate')->value = currentDateTime();
        $obj->status = 'tsk_task_ok';
        try {
            $obj->save();
            $success[] = $obj->viewLink();
        } catch (Exception $e) {
            $failed[] = $obj->viewLink();
        }
    } else {
        $failed[] = $task['description'];
    }
    $q++;
    if ($q > $max) break;
}
if (count($success) > 0) {
    echo("Успешно закрыты следующие задачи:".implode(", ",$success)."Всего: ".count($success)); 

}
if (count($failed) > 0) {
    warning("Закрыть не удалось:".implode(", ",$failed)."Всего: ".count($failed)); 
}



Команда доступна на виде снизу (см. скриншоты ниже).Закрыть выбранные
Точно такая же команда, но закрывает только выбранные пользователем задачи.

Закрыть выбранные
$objectrefIds = ccommand()->objectrefIds;
$success = array();
$failed = array();
$cnt = count($objectrefIds);
$q = 1;
if ($cnt == 0)
    throw new Exception('Ничего не выбрано!');
    
foreach ($objectrefIds as $objectrefId) {
    $obj = select($objectrefId);
    progress($q/$cnt * 100, $obj['description']);
    if (!empty($obj)) {
        $obj->attributeref('tsk_task_enddate')->value = currentDateTime();
        $obj->status = 'tsk_task_ok';
        try {
            $obj->save();
            $success[] = $obj->viewLink();
        } catch (Exception $e) {
            $failed[] = $obj->viewLink();
        }
    } else {
        $failed[] = $objectrefId;
    }
    $q++;
    if ($q > $cnt) break;
}
if (count($success) > 0) {
    echo("Успешно закрыты следующие задачи: ".implode(", ",$success)."Всего: ".count($success));  
}
if (count($failed) > 0) {
    warning("Закрыть не удалось: ".implode(", ",$failed)."Всего: ".count($failed));  
}


Виды


Виды (выборки), важная составляющая навигацию по объектам. Позволяют гибко настроить отображение списка задач в соответствии с требованиями пользователей.Мои задачи
«Интеллектуальный» вид, т.к. анализирует, в какой должности находится активный пользователь открывший его. В том случае, если его открыл начальник отдела, добавляет категории: наименование отдела и ФИО начальника. Таким образом, начальник отдела может видеть не только свои задачи, но и задачи порученные всем его подчиненным.
Кстати, существование такого вида было одним из важных требований, выдвинутых пользователями, в частности, главным инженером, ГИПами и начальниками отделов. Не всегда такое возможно реализовать в других системах, заточенных под управление индивидуальными задачами.

Мои задачи
$groups = cuser()->groups();
$isgip = false;
$ishead = false;
foreach($groups as $group)
    if (strncmp($group['data_one'],'09.',3) == 0) {
        $isgip = true;
        break;
    } elseif (strcmp($group['code'],'group_head_and_deputy') == 0) {
        $ishead = true;
        break;
    }
    
$attributes = array(
    'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
    'tsk_task_base_open'=>array('link'=>'value'),
    'tsk_task_contract'=>array('link'=>'value'),
    'tsk_task_subj'=>array('limit'=>160)
);
    
if ($isgip) 
{
    $categories = array($group->name, cuser()->description);
    $us = array('id');
    foreach ($group->users() as $u)
        $us[] = $u['id'];
    cviewpub()->categories = $categories;
    cviewpub()->category = is_null(cviewpub()->category) ? 0 : cviewpub()->category;
    switch(cviewpub()->category)
    {
        case 0:
            $attributes += array('tsk_task_executor'=>array('link'=>'value','inplaceEdit'=>true,'options'=>array('style'=>'width: 30%;')));
            $conditions = array('tsk_task_executor'=>$us);
            break;
        case 1:
            $conditions = array('tsk_task_executor'=>array('id',cuser()->id));
            break;
    }
} elseif ($ishead) {
    foreach($groups as $group)
        if (is_numeric($group['data_one']))
            break;
    $us = array('id');
    foreach ($group->users() as $u)
        $us[] = $u['id'];
            
    $categories = array($group->name, cuser()->description);
    cviewpub()->categories = $categories;
    cviewpub()->category = is_null(cviewpub()->category) ? 0 : cviewpub()->category;
    switch(cviewpub()->category)
    {
        case 0:
            $attributes += array('tsk_task_executor'=>array('link'=>'value','inplaceEdit'=>true,'options'=>array('style'=>'width: 30%;')));
            $conditions = array('tsk_task_executor'=>$us);
            break;
        case 1:
            $conditions = array('tsk_task_executor'=>array('id',cuser()->id));
            break;
    }
} else {
    $categories = array(cuser()->description);
    $conditions = array('tsk_task_executor'=>array('id',cuser()->id));
}
$attributes += array(
    'tsk_task_plan_startdate',
    'tsk_task_plan_enddate',
    'tsk_task_base_close'=>array('link'=>'value'),
    'tsk_task_comment',
    'status'=>array('link'=>'value', 'actions'=>array(),'options'=>array('style'=>'width: 100px;'))
);
$conditions['status'] = array('tsk_task_initiated','tsk_task_created','tsk_task_processed');
cviewpub()->exec(array(
  'object'=>objectDef('tsk_management','tsk_task'),
  'attributes'=>$attributes,
  'sort'=>array(
    'tsk_task_code'=>array('enable'=>true),
    'tsk_task_base_open'=>array('enable'=>true),
    'tsk_task_contract'=>array('enable'=>true),
    'tsk_task_subj'=>array('enable'=>true),
    'tsk_task_plan_startdate'=>array('enable'=>true),
    'tsk_task_plan_enddate'=>array('default'=>'asc', 'enable'=>true)
  ),
  'conditions'=>$conditions,
  'sorting'=>true,
  'pagination'=>array('pagesize'=>10),
  'showcreate'=>true,
));



2c646024a61b4fd7a642b5f00be2a840.pngНазначенные мной
Понятно из названия, что вид отображает перечень задач, автором которых является активный пользователь. Также является «интеллектуальным», т.к. анализирует активного пользователя, а в случае, если он является ГИПом, добавляет две категории: группа ГИПа и ФИО ГИПа. Категории нужны для того, чтобы ГИП мог видеть как свои персональные задачи отдельно, так и задачи порученные его помощнику. ГИП и его помощник работают над одним пулом задач.

Назначенные мной
$groups = cuser()->groups();
$isgip = false;
foreach($groups as $group)
    if (strncmp($group['data_one'],'09.',3) == 0)
    {
        $isgip = true;
        break;
    }
    
if ($isgip) 
{
    $categories = array($group->name, cuser()->description);
    $us = array();
    foreach ($group->users() as $u)
        $us[] = $u['id'];
    cviewpub()->categories = $categories;
    cviewpub()->category = is_null(cviewpub()->category) ? 0 : cviewpub()->category;
    switch(cviewpub()->category)
    {
        case 0:
            $conditions = array('tsk_task_author'=>$us);
            break;
        case 1:
            $conditions = array('tsk_task_author'=>cuser()->id);
            break;
    }
} 
else 
{
    $categories = array(cuser()->description);
    $conditions = array('tsk_task_author'=>cuser()->id);
}
$conditions['status'] = array('tsk_task_initiated','tsk_task_created','tsk_task_processed');
cviewpub()->exec(array(
  'object'=>objectDef('tsk_management','tsk_task'),
  'attributes'=>array(
    'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
    'tsk_task_base_open'=>array('link'=>'value'),
    'tsk_task_contract'=>array('link'=>'value'),
    'tsk_task_subj'=>array('limit'=>160,'inplaceEdit'=>true),
    //'tsk_task_description'=>array('limit'=>160),
    'tsk_task_executor'=>array('link'=>'value','options'=>array('style'=>'width: 10%;')),
    'tsk_task_plan_startdate',
    'tsk_task_plan_enddate',
    'status'=>array('link'=>'value', 'actions'=>array(),'options'=>array('style'=>'width: 100px;'))
  ),
  'sort'=>array(
    'tsk_task_code'=>array('enable'=>true),
    'tsk_task_base_open'=>array('enable'=>true),
    'tsk_task_subj'=>array('enable'=>true),
    'tsk_task_executor'=>array('enable'=>true),
    'tsk_task_plan_startdate'=>array('enable'=>true),
    'tsk_task_plan_enddate'=>array('default'=>'asc', 'enable'=>true)
  ),
  'conditions'=>$conditions,
  'sorting'=>true,
  'pagination'=>array('pagesize'=>10),
  'showcreate'=>true,
));



1e4ffda557464692be495dcf909433b7.pngМои завершенные
Простой вид. Отображает перечень завершенных задач, исполнителем которых является активный пользователь.

Мои завершенные
cviewpub()->exec(array(
  'object'=>objectDef('tsk_management','tsk_task'),
  'attributes'=>array(
    'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
    'tsk_task_base_open'=>array('link'=>'value'),
    'tsk_task_subj'=>array('limit'=>160),
    'tsk_task_plan_startdate',
    'tsk_task_plan_enddate',
    'tsk_task_startdate',
    'tsk_task_enddate'
  ),
  'sort'=>array(
    'tsk_task_code'=>array('enable'=>true),
    'tsk_task_base_open'=>array('enable'=>true),
    'tsk_task_subj'=>array('enable'=>true),
    'tsk_task_plan_startdate'=>array('enable'=>true),
    'tsk_task_plan_enddate'=>array('default'=>'desc', 'enable'=>true)
  ),
  'conditions'=>array(
    'tsk_task_executor'=>cuser()->id,
    'status'=>'tsk_task_ok'
  ),
  'sorting'=>true,
  'pagination'=>array('pagesize'=>20)
));


Все задачи
Отображает полный перечень всех активных задач. Используется, как правило, для поиска чужих задач.

Все задачи
cviewpub()->exec(array(
  'object'=>objectDef('tsk_management','tsk_task'),
  'attributes'=>array(
    'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
    'tsk_task_base_open'=>array('link'=>'value'),
    'tsk_task_subj'=>array('limit'=>160),
    'tsk_task_executor'=>array('link'=>'value','options'=>array('style'=>'width: 10%;')),
    'tsk_task_plan_startdate',
    'tsk_task_plan_enddate',
    'status'=>array('link'=>'value', 'actions'=>array(),'options'=>array('style'=>'width: 100px;'))
  ),
  'sort'=>array(
    'tsk_task_code'=>array('enable'=>true),
    'tsk_task_base_open'=>array('enable'=>true),
    'tsk_task_subj'=>array('enable'=>true),
    'tsk_task_executor'=>array('enable'=>true),
    'tsk_task_plan_st
    
            

© Habrahabr.ru