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

Зажадав головний інженер автоматизувати процес управління завданнями. Доручень своїм підлеглим він дає багато, але вручну контролювати процес їхнього виконання, просто нереально. Пам'ятати всі доручення тим більше неможливо. Тому відразу наполіг на використанні відповідних засобів автоматизації.

Нетривіальні завдання зажадали нетривіального підходу. Детальний опис з картинками і вихідним кодом під катом.

Проектна організація, чисельністю близько 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',
));


Зрештою форма об'єкта стала виглядати якось так:


Статуси

Мудрувати зі статусами не став: Створена, Прийнята, Виконана, Відхилена. Прийнятою вважається завдання, з якої працівник ознайомився і прийняв до виконання. Інші статуси зрозумілі з назви.


Дії

Невелике число статусів веде до кращого розумію процесу і зменшення числа дій. Дій і прям вийшло небагато.

Прийняти
Призначення, власне, випливає з назви. Переводить задачу в статус «Прийнята» і встановлює фактичну дату початку роботи.
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;

<h5>Додати підзадачу</h5>
Призначене для створення підзадачі до відкритого завдання. Використовується переважно ГИПами та начальниками відділів.
$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,
));




Призначені мною
Зрозуміло з назви, що вигляд відображає перелік завдань, автором яких є активний користувач. Також є «інтелектуальним», т. к. аналізує активного користувача, а у разі, якщо він є ГИПом, додає дві категорії: група Гіпа та ПІБ Гіп. Категорії потрібні для того, щоб ГІП міг бачити як свої персональні завдання окремо, так і завдання доручені його помічникові. ГІП і його помічник працюють над одним пулом завдань.
Призначені мною
$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,
));




Мої завершені
Простий вигляд. Відображає перелік завершених завдань, виконавцем яких є активний користувач.
Мої завершені
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_startdate'=>array('enable'=> 'true'),
'tsk_task_plan_enddate'=>array('default'=>'asc', 'enable'=>true)
),
'conditions'=>array(
'status'=>array('tsk_task_initiated','tsk_task_created','tsk_task_processed')
),
'sorting'=>true,
'pagination'=>array('pagesize'=>10),
'showcreate'=>true,
));



Всі рішення планерки
Дуже важливий перелік завдань, т. к. містить тільки завдання, призначені за рішенням наради і перебувають на контролі. При виконанні скрипта, вид аналізує перелік усіх завдань, вибирає з нього всіх виконавців, групує по відділах і формує список категорій виду з найменуваннями відділів.
Таким чином, кожен співробітник, а як правило, це начальники та головні спеціалісти відділів, можуть легко відфільтрувати з усього списку поставлених завдань тільки свої.
Всі рішення планерки
$src_executors = selectColumnAll('tsk_management','tsk_task','tsk_task_executor',
array(
'status'=>array('tsk_task_initiated','tsk_task_created','tsk_task_processed'),
'tsk_task_category'=>array('task_category_plan')
)
);
$end_executors = array();
foreach ($src_executors as $u) {
$end_executors[] = $u['id'];
}
$users = corganization()->users($end_executors);
$departments = array(0=>'Всі');
foreach ($users as $u) {
foreach ($u->departments as $d) {
$departments[$d['id']] = $d['name'];
}
}
cviewpub()->categories = $departments;
cviewpub()->category = is_null(cviewpub()->category) ? 0 : cviewpub()->category;
$conditions = array(
'status'=>array('tsk_task_initiated','tsk_task_created','tsk_task_processed'),
'tsk_task_category'=>array('task_category_plan')
);
if (cviewpub()->category != '0') {
$department = corganization()->department(cviewpub()->category);

if (!empty($department)) {
$users = $department->users();
$c = array('id');
foreach ($users as $u)
$c[] = $u['id'];

$conditions['tsk_task_executor'] = $c;
}
}
cviewpub()->exec(array(
'object'=>objectDef('tsk_management','tsk_task'),
'attributes'=>array(
'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
'tsk_task_contract'=>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',
'tsk_task_startdate',
'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,
));




Авторський нагляд
Дуже важливий і непростий вид, який використовують ГИПы для підготовки кошторисів з авторського нагляду. Складність виду полягає в тому, що він насправді відображає інформацію не тільки про завдання, але і про їх підставах на відкриття і закриття, тобто про листи. Таким чином, у вигляді відображається інформація про трьох об'єктах відразу! Таку можливість надає easla.com при використанні атрибутів типу «Об'єкт» та зазначення в описі виду атрибутів вкладених об'єктів через крапку.
Авторський нагляд
cviewpub()->exec(array(
'object'=>objectDef('tsk_management','tsk_task'),
'attributes'=>array(
'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
'tsk_task_contract'=>array('link'=>'value'),
'tsk_task_contract.agr_management_contract_title'=>array('header'=>'Найменування договору','limit'=>'30'),
'tsk_task_contract.agr_management_contract_contragent'=>array('header'=>'Контрагент'),
'tsk_task_contract.agr_management_contract_project_manager'=>array('header'=>'ГІП'),
'tsk_task_subj'=>array('limit'=>160),
'tsk_task_executor'=>array('link'=>'value','options'=>array('style'=>'width: 10%;')),
'tsk_task_executor.email',
'tsk_task_base_open'=>array('link'=>'value','export'=>array('id','crs_management_incoming_contragent_regnum')),
'tsk_task_base_open.crs_management_incoming_receive_date'=>array('header'=>'Дата отримання вхідного'),
'tsk_task_base_close'=>array('link'=>'value','export'=>array('id','crs_management_outgoing_regnum')),
'tsk_task_base_close.crs_management_outgoing_sentdate',
'tsk_task_efforts',
'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'=>array(
'tsk_task_base_close'=>array('crs_management_outgoing_content'=>684)
),
'sorting'=>true,
'pagination'=>array('pagesize'=>10),
'showcreate'=>true,
));


До речі, зверну увагу на опцію export використовувану в налаштуванні виду. Якщо налаштовувати вигляд так, щоб у ньому відображалось всі необхідну кількість колонок, він буде такий великий в ширину, що не поміститься навіть на широкоформатний екран. Але при експорті виду в Excel, потрібно багато колонок. Вирішується з допомогою опції export. У ній зазначено, які саме колонки експортувати, замість відображається у вигляді. Круто!


Окрім загальних видів доступних користувачеві через головне меню були створені дод. види для об'єктів, у термінології easla.com – у контексті об'єкта. З їх допомогою вдалося на формі вхідного і вихідного листа відображати список залежних від нього завдань. Дуже зручно, коли треба проаналізувати, які завдання були призначені на підставі вибраного, скажімо, вхідного документа і в якому стані вони знаходяться на даний момент.
Завдання по документу
cviewpub()->exec(array(
'object'=>objectDef('tsk_management','tsk_task'),
'attributes'=>array(
'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
'tsk_task_subj'=>array('limit'=>160),
//'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_plan_enddate'=>array('default'=>'asc', 'enable'=>true)
),
'sorting'=>true,
'pagination'=>array('pagesize'=>10),
));




Крім цього, підзадачі треба було відображати детальніше, ніж система пропонує це за замовчуванням, тому був створений додатковий вид.
Підзадачі

$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;'))
);

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_subj'=>array('enable'=> 'true'),
'tsk_task_plan_startdate'=>array('enable'=> 'true'),
'tsk_task_plan_enddate'=>array('default'=>'asc', 'enable'=>true)
),
'sorting'=>true,
'pagination'=>array('pagesize'=>20),
'showcreate'=>true,
));




Ролі

Ролей всього три: менеджер, учасник, спостерігач. Менеджер, зрозуміло, може все. Учасник не може тільки видаляти завдання. Спостерігач може тільки переглядати завдання.

Звичайно, при такому поділі прав доступу кожен співробітник має право змінювати чужі завдання. Але, аналізуючи всі озвучені вимоги до процесу і внутрішні відносини в колективі компанії прийшов до висновку, що розрулювати права так, щоб ніхто і нічого не міг змінити без вагомої на те санкції виявиться лише «палицею в колесах». Убезпечив завдання від свавілля шляхом використання «історичних» атрибутів. Інакше кажучи, якщо хтось і щось змінить завдання, залишаться сліди і буде видно, як було, як було, і хто змінив.

Підсумки

Особисто для мене завжди основним критерієм успішності проекту автоматизації є не підписаний акт або технічне завдання з грифом «Виконано», а факт експлуатації запущеної системи.
Зараз процес «Завдання» активної використовується всіма учасниками процесу. Велика заслуга в цьому головного інженера, який дисциплінує всіх інших особистим прикладом. До речі, він же є одним з основних постачальників дод. вимог до процесу.
Дуже приємною несподіванкою стала поява виду «Авторський нагляд», які використовується для вивантаження даних і формування кошторису в Microsoft Excel. Виявилося, що така кошторис приносить чималий прибуток організації.
До речі, крім об'єкта «Завдання» в рамках описаного процесу був реалізований об'єкт «Ознайомлення», що дозволяє відправляти будь-які документи на ознайомлення з контролем виконання. Але про це окремо.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.