📜 ⬆️ ⬇️

Task management in project organization

Required the chief engineer to automate the task management process. He gives a lot of orders to his subordinates, but it is simply unrealistic to manually control the process of their execution. It is all the more impossible to remember all orders. Therefore, immediately insisted on the use of suitable automation.

Non-trivial tasks required a non-trivial approach. Detailed description with pictures and source code under the cut.

Project organization, numbering about 200 people. In the top management there are 4 people, in direct submission of the chief engineer 4 main project engineers (CIP) and as many of their assistants, plus all the department heads.

The chief engineer per day can assign from 5 to 15 tasks to his direct subordinates, who, in turn, can delegate tasks to several department heads, and those to their subordinates. Classical hierarchical scheme. Thus, the number of active tasks per unit of time can reach 600-800! To keep them all in your head is simply unrealistic, and in the conditions of a lame performing discipline the issue of control becomes vital.

In the organization, at that moment, MS Project was used for several years, however, for project management in general, and not for short-term assignments. The idea of ​​using it to manage short-term orders is shallow after a brief discussion.
Given the experience of using the easla.com system for managing correspondence, we decided to try using it for task management. Moreover, the tasks involved close integration with correspondence.
')

Task


I mean, not a task, but a statement of the problem. Initially, all the same, it was planned to make the tasks simple: the topic, description, author, performer, planned and actual dates. Therefore, the requirements were simple:

A little later, already during the trial operation, it was necessary to expand the functionality and the requirements became more:

In general, the tasks were not as simple as it seemed at first glance.

Decision


First of all, the questions caused tasks to several performers. If you create one task for all, i.e. not personalized, the probability of its execution will decrease to almost zero. Each performer will hope for another. I do not know, like the others, but we have exactly that. Therefore, all tasks should be individualized, so that we had to implement the task cloning mechanism for each executor.
Then it was necessary to determine the importance of the task. Three types of importance were introduced and each was assigned a maximum due date:

Labor is also an interesting topic. Some employees find it difficult to specify the time spent. I do not even know how to explain this, but they are either afraid to put too little, or they are afraid to indicate too much, therefore they have entered an approximate scale with sufficient accuracy for analysis:

Having decided on the tool and principles of the process, I started the implementation.

Implementation


In easla.com created a new process "Tasks". In it created the object "Task". The object has the following attributes.

Attributes


room

Normal counter for sequential numbering.

Designation

String attribute The value is calculated after the creation of the task and cannot be changed by the user. In the read-only mode, the attribute is translated in the “At Initialization” script:
cobjectref()->attributeref('tsk_task_code')->readonly = true; 


Author

User, i.e. employee of the organization. The author can be any employee of the organization. A complete list of employees is formed in the "At Initialization" script:
 $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; 

The attribute is assigned the value of the active user and the “read only” mode so that it is not possible to create a task from another employee.

Executor

Employee of the organization entrusted with the task. Multiple attribute, since The same task can be performed by different specialists, and, say, the ISU will bring everything together into one solution. The list of employees is formed in the "At initialization" script:
 $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; 


Contract

Link to the object "Contract". The attribute is initialized in the “Task” object script.

Theme

Normal string attribute. At first, the contract number was entered into it, but they quickly abandoned this practice and entered a separate attribute for the contract number.

Description

Multiline string attribute for a detailed description of the task. The description can be changed only by the author of the task, therefore in the “On Initialization” and “On Change” scripts it is written:
 cattributeref()->readonly = cuser()->id != cobjectref()->attributeref('tsk_task_author')->value; 


Notify about execution

An integer attribute, which is actually a flag, which defines the possibility of notifying the author of the task of its completion. The list of valid values ​​and the initial value is defined in the "At Initialization" script:
 cattributeref()->values = array('',''); if (empty(cattributeref()->value)) cattributeref()->value = 0; 


Task Category

Classifier that defines the category of the task. Now there are only three:

The list of valid values ​​is formed in the "At Initialization" script:
 $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); } 

When you change the category, the “necessity” of the “ Reason for closing ” attribute changes.
 $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; } 


Importance

Classifier The list of valid values ​​and the initial value are also defined in the “At Initialization” script:
 $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)['']; } 

“When changing”, the planned closing date of the task is recalculated:
 if (empty(cattributeref()->value)) return; cobjectref()->calcPlanEndDate(cattributeref()->value); 

The calcPlanEndDate function is described in the object itself.

Basis for opening

A reference to an object, in particular, an incoming or outgoing document, which became the basis for the appearance of the task. The list of incoming and outgoing documents is formed in the “At Initialization” script:
 cattributeref()->readonly = cobjectref()->attributeref('tsk_task_description')->readonly; $base = cobjectref()->prepareIncomings(); $base += cobjectref()->prepareOutgoings(); $base = array_reverse($base, true); cattributeref()->values = $base; 


Applications

File attribute It is rarely used, but it is needed if the accompanying documents must be attached to the task.

Start date (plan)

The planned date and time of the start of the task. We agreed that it will be assigned with an offset of +1 hour to the current time, which is written in the “At Initialization” script:
 cattributeref()->readonly = cobjectref()->attributeref('tsk_task_description')->readonly; cattributeref()->value = calendarDateAdd(currentDateTime(), 3600); 

Only the author can change. Separate attention to the calendarDateAdd function; it calculates the planned start date in accordance with the production calendar !

End Date (Plan)

The planned date and time of the end of the task. Depends on the importance and the planned start date. The initial value is calculated "During initialization":
 cattributeref()->readonly = cobjectref()->attributeref('tsk_task_description')->readonly; if (empty(cattributeref()->value) && !empty(cobjectref()->tsk_task_importance)) { cobjectref()->calcPlanEndDate(cobjectref()->tsk_task_importance); } 


Start date (fact) and End date (fact)

The actual start and end dates that are entered only when the status of the task changes.

Reason for closing

A reference to the object, namely, the outgoing document, which became the basis for closing the task. Moreover, if the category of the task “Preparing an Incoming Response” is assigned to the task, the task cannot be closed until the reason for closing is filled. The list of available outgoing documents is formed in the "At Initialization" script:
 $base = cobjectref()->prepareIncomings(); $base += cobjectref()->prepareOutgoings(); $base = array_reverse($base, true); cattributeref()->values = $base; 


Labor costs

An integer attribute containing the number of seconds the executor spent on the task. Since the tasks are assumed to be short-term, the list of acceptable values ​​is plentiful. It is formed during attribute initialization:
 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; 


Comments

Multi-line text attribute for commenting the task. The attribute saves history, so you can track who wrote what and when.
That's all the attributes!

An object


The “Task” object has uneasy behavior and validation, which are described in its scripts. Helper functions for initializing attributes and calculating dates and times are described in the script:
Before initialization of the object
 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; 


I will separately pay attention to the calcPlanEndDate function, which uses the calendarDateAdd function to calculate a planned date and time. With its help, it is possible to calculate the time in working hours, taking into account the organization’s production calendar .

Additional initialization of attributes and the calculation of the state of the planned start and end dates is carried out in the script:
After initializing the object
 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; 


The states of the attributes Start date (plan) and End date (plan) are calculated based on the current time. As soon as the date is missed, the attribute is painted in red, but before that, first in orange and yellow, in such a simple way reminding the performer of the importance of completing the task on time.

Before saving an object, it is important to validate all entered values ​​and refuse to save it if something is wrong.
In addition to identifying errors, there is a check for the existence of a similar task on three grounds: the topic, the basis for the discovery and the performer. If exactly the same task is found, then the assignment of a new one is denied. Very, very useful feature!
In addition, as mentioned above, the task should be assigned to only one performer, and all the others in the list should receive copies of it, so the task is saved with only one performer, and the rest are stored in the object's arguments.
By the way, the list of performers is analyzed for the presence of a GUI in it. And if it is found, then the task of the GUI becomes the main one, and all the rest are created as subtasks to it. This sorting of tasks is very convenient for the GUI.
Before saving the object
 $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; 


The temporary refuge for the list of performers who will be assigned copies of the tasks is:
 $this->arguments['executor'] 

You can create as many “arguments” in the object. In my case, one was enough.

After the object is saved, clones of tasks are created, if necessary, and sending notifications.
After saving the object
 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', )); 


Ultimately, the shape of the object began to look something like this:


Statuses


Wise with statuses did not: Created, Accepted, Implemented, Rejected. Accepted is the task with which the employee has reviewed and accepted for execution. The remaining statuses are clear from the title.


Actions


A small number of statuses leads to a better understanding of the process and a decrease in the number of actions. Action and straight turned a little.

To accept

The appointment, in fact, follows from the title. Translates the task into “Accepted” status and sets the actual date of commencement of work
 cobjectref()->status = 'tsk_task_processed'; cobjectref()->attributeref('tsk_task_startdate')->value = currentDateTime(); 


Run

Successfully closes the task fixing the actual end date of work on it. Be sure to require a comment. This was one of the requirements of the chief engineer. On the “first couple” he was very indignant when subordinates closed tasks without comment. It was completely incomprehensible what was done and on what basis the task was closed.
In addition, the action checks whether the task is nested, and if so, it checks whether all tasks adjacent to it are completed. With a positive result, checks the status of the higher-level task and, if necessary, sends it to the contractor by mail with a notification stating that the task can certainly be closed, since All nested tasks are completed.
 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().'   , ..      !', )); } } } 


Reject

It is obvious from the title that the action rejects the task. One condition: the comment must indicate the reason for the rejection.
 if (empty(cobjectref()->attributeref('tsk_task_comment')->value)) echo '     .'; else { cobjectref()->status = 'tsk_task_failed'; cobjectref()->attributeref('tsk_task_enddate')->value = currentDateTime(); } 


Return

The action is available only to the process manager. Allows you to return a task from the final status to the "Accepted" status. Required if the task was closed by mistake. Required rarely, but still needed.
 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); 


Teams


The first process in which it was necessary to create teams. Commands differ from actions in that they are performed in the context of a process, not an object. Thus, they allow processing objects “batch”: all at once or selected by the user.

, «» «» , , , . … , , . , .. , , , « », , . , , , , , .

, « » ( , ). , « ». « ».


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)); } 



Kinds


(), . .


«» , .. , . , , : . , , .
, , , , , . , .
 $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, )); 





It is clear from the name that the view displays a list of tasks authored by an active user. It is also "intellectual", because analyzes the active user, and if he is a GUI, adds two categories: the GUI group and the GUI name. Categories are needed so that the GUI can see both his personal tasks separately and the tasks assigned to his assistant. The GUI and his assistant are working on one task pool.
Appointed by me
 $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, )); 




My completed

Simple view. Displays a list of completed tasks executed by an active user.
My completed
 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) )); 



All tasks

Displays a complete list of all active tasks. It is used, as a rule, to search for other people's tasks.
All tasks
 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, )); 



All decisions

A very important list of tasks, because contains only tasks assigned by the scheduling decision and under control. When executing the script, the view analyzes the list of all tasks, selects all executors from it, groups it into departments, and generates a list of categories of the type with the names of the departments.
Thus, each employee, and as a rule, these are the heads and chief specialists of the departments, can easily filter only their own from the entire list of tasks assigned.
All decisions
 $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, )); 




Author's supervision

A very important and challenging type, which GUIs use to prepare author's supervision estimates. The complexity of the form lies in the fact that it actually displays information not only about the tasks, but also about their grounds for opening and closing, i.e. about letters. Thus, the form displays information about the three objects at once! This possibility is provided by easla.com when using attributes of the “Object” type and specifying the type of attributes of nested objects through a point in the description.
Author's supervision
 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, )); 


By the way, I'll pay attention to the export option used in the view settings. If you adjust the view so that it displays all the required number of columns, it will be so big in width that it does not fit even on a widescreen screen. But when exporting a view to Excel, you need a lot of columns. Solved using the export option . It indicates which columns to export, instead of being displayed as. Cool!


. , 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, )); 




Roles


: , , . , , . . .

, . , , , « ». «» . , - - , , , , .

Results


«», .
«» . , . , . .
« », Microsoft Excel. , .
By the way, besides the “Task” object, the “Acquaintance” object was implemented within the framework of the described process, which allows sending any documents for familiarization with the execution control. But more about that separately.

Source: https://habr.com/ru/post/301944/


All Articles