JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
ecbf757d1e94ff4328bcbed6621c3c6edf9e4a84
[contractor-progress.git] / tasks.php
1 <?php
2
3 #  Copyright (C) 2008  Jason Woofenden
4 #
5 #  This program is free software: you can redistribute it and/or modify
6 #  it under the terms of the GNU Affero General Public License as published by
7 #  the Free Software Foundation, either version 3 of the License, or
8 #  (at your option) any later version.
9 #
10 #  This program is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #  GNU Affero General Public License for more details.
14 #
15 #  You should have received a copy of the GNU Affero General Public License
16 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 require_once('code/tasks.php');
19
20 $GLOBALS['tasks_form_recipient'] = "fixme@example.com";
21
22
23
24 require_once('code/wfpl/template.php');
25 require_once('code/wfpl/format.php');
26 require_once('code/wfpl/messages.php');
27 require_once('code/wfpl/email.php');
28 require_once('code/db_connect.php');
29
30 function description_has_fixmes($description) {
31         return (strpos($description, 'FIXME') !== false);
32 }
33
34 function tasks_get_fields() {
35         $title = format_oneline($_REQUEST['title']);
36         $url = format_oneline($_REQUEST['url']);
37         $description = format_unix($_REQUEST['description']);
38         $price = format_decimal($_REQUEST['price']);
39
40         tasks_tem_sets($title, $url, $description, $price);
41
42         return array($title, $url, $description, $price);
43 }
44
45 function tasks_tem_sets($title, $url, $description, $price) {
46         tem_set('title', $title);
47         tem_set('url', $url);
48         tem_set('description', $description);
49         tem_set('price', $price);
50 }
51
52 function tasks_main() {
53         if(!logged_in()) {
54                 $GLOBALS['url'] = this_url();
55                 return 'login';
56         }
57
58         if(isset($_REQUEST['tasks_id'])) {
59                 $ret = tasks_display_main();
60                 if($ret) {
61                         return $ret;
62                 }
63                 tem_show('display_body');
64         } else {
65                 $ret = tasks_edit_main();
66                 if($ret) {
67                         return $ret;
68                 }
69                 tem_show('edit_body');
70         }
71 }
72
73 function tasks_display_main() {
74         $task_id = format_int($_REQUEST['tasks_id']);;
75         $client_id = logged_in();
76         if(logged_in_as_contractor()) {
77                 $row = db_get_row('tasks', 'title,url,description,state,price,client_id,paid,finished_at,tested_at', 'where id=%i', $task_id);
78         } else {
79                 $row = db_get_row('tasks', 'title,url,description,state,price,client_id,paid,finished_at,tested_at', 'where id=%i && client_id=%i', $task_id, $client_id);
80         }
81         if($row) {
82                 list($title, $url, $description, $state, $price, $owner_id, $paid, $finished_at, $tested_at) = $row;
83                 tem_set('task_id', $task_id);
84                 tem_set('title', $title);
85                 tem_set('url', $url);
86                 tem_set('description', $description);
87                 tem_set('state', task_state_pretty($state));
88                 tem_set('price', $price);
89                 if($finished_at) {
90                         tem_set('finished_at', $finished_at);
91                         tem_show('finished_at_section');
92                 }
93                 if($tested_at) {
94                         tem_set('tested_at', $tested_at);
95                         tem_show('tested_at_section');
96                 }
97                 if($state == TASK_BUG) {
98                         tem_show('bug_title');
99                 } else {
100                         tem_show('normal_title');
101                 }
102                 if(logged_in_as_contractor()) {
103                         switch($state) {
104                                 case TASK_DRAFT:
105                                 case TASK_NEEDS_CLARIFICATION:
106                                 case TASK_NEEDS_QUOTE:
107                                 case TASK_BUG:
108                                         tem_show('normal_edit_link');
109                                 break;
110                                 case TASK_NEEDS_GO_AHEAD:
111                                         tem_show('approve_price_link');
112                                         tem_show('normal_edit_link');
113                                         tem_show('price_row');
114                                 break;
115                                 case TASK_QUEUED:
116                                         tem_show('normal_edit_link');
117                                         tem_show('working_link');
118                                         tem_show('price_row');
119                                 break;
120                                 case TASK_WORKING:
121                                         tem_show('price_row');
122                                         tem_show('needs_testing_link');
123                                 break;
124                                 case TASK_NEEDS_TESTING:
125                                         if($owner_id == logged_in()) {
126                                                 tem_show('finished_link');
127                                         }
128                                         # FALL THROUGH
129                                 case TASK_FINISHED:
130                                         if($paid) {
131                                                 tem_show('marked_paid');
132                                         } else {
133                                                 tem_show('mark_paid_link');
134                                         }
135                                         tem_show('price_row');
136                                 break;
137                         }
138                 } else {
139                         switch($state) {
140                                 case TASK_DRAFT:
141                                 case TASK_NEEDS_CLARIFICATION:
142                                 case TASK_BUG:
143                                 case TASK_ON_HOLD:
144                                         tem_show('normal_edit_link');
145                                 break;
146                                 case TASK_NEEDS_QUOTE:
147                                         tem_show('hold_link');
148                                         tem_show('normal_edit_link');
149                                 break;
150                                 case TASK_NEEDS_GO_AHEAD:
151                                         tem_show('price_row');
152                                         tem_show('approve_price_link');
153                                         tem_show('normal_edit_link');
154                                 break;
155                                 case TASK_QUEUED:
156                                         tem_show('price_row');
157                                         tem_show('warning_edit_link');
158                                         tem_show('hold_link');
159                                 break;
160                                 case TASK_WORKING:
161                                         tem_show('price_row');
162                                 break;
163                                 case TASK_NEEDS_TESTING:
164                                         tem_show('price_row');
165                                         tem_show('finished_link');
166                                 break;
167                                 case TASK_FINISHED:
168                                         tem_show('price_row');
169                                 break;
170                         }
171                 }
172         } else {
173                 message("Task #$task_id not found");
174                 return './';
175         }
176 }
177
178 define('MAX_PRIORITY', 2000000000);
179 define('MIN_PRIORITY', 0);
180 define('MID_PRIORITY', floor((MAX_PRIORITY - MIN_PRIORITY) / 2));
181
182 function new_lowest_priority($client_id) {
183         $lowest_ord = db_get_value('tasks', 'ord', 'where client_id=%i && state=%i order by ord asc limit 1', $client_id, TASK_QUEUED);
184         if($lowest_ord === false) {
185                 return MID_PRIORITY;
186         }
187         if($lowest_ord == MIN_PRIORITY) {
188                 reprioritize_tasks($client_id); # make room
189                 $lowest_ord = db_get_value('tasks', 'ord', 'where client_id=%i && state=%i order by ord asc limit 1', $client_id, TASK_QUEUED);
190         }
191         return MIN_PRIORITY + floor(($lowest_ord - MIN_PRIORITY) / 2);
192 }
193
194 # keep everything in the same order, but space them out so there's room to squeeze things in anywhere
195 function reprioritize_tasks($client_id) {
196         $ids = db_get_column('tasks', 'id', 'where client_id=%i && state=%i order by ord desc, id desc', $client_id, TASK_QUEUED);
197         $step = floor((MAX_PRIORITY - MIN_PRIORITY) / (count($ids) + 1));
198         $cur = MAX_PRIORITY;
199         foreach($ids as $id) {
200                 $cur -= $step;
201                 db_update('tasks', 'ord', $cur, 'where id=%i', $id);
202         }
203 }
204
205
206 # pass the task id and one of (up,down,top,bottom)
207 function prioritize_task($id, $change) {
208         $row = db_get_row('tasks', 'client_id,ord', 'where id=%i', $id);
209         if(!$row) {
210                 message('Database error #2242');
211                 return;
212         }
213         list($client_id, $ord) = $row;
214         switch($change) {
215                 case 'top':
216                         list($highest_id, $highest_ord) = db_get_row('tasks', 'id,ord', 'where client_id=%i && state=%i order by ord desc limit 1', $client_id, TASK_QUEUED);
217                         if($highest_id == $id) {
218                                 message('Already highest priority');
219                                 return;
220                         }
221
222                         if($highest_ord == MAX_PRIORITY) {
223                                 reprioritize_tasks($client_id); # make room
224                                 $highest_ord = db_get_value('tasks', 'ord', 'where client_id=%i && state=%i order by ord desc limit 1', $client_id, TASK_QUEUED);
225                         }
226
227                         $new_ord = MAX_PRIORITY - floor((MAX_PRIORITY - $highest_ord) / 2);
228                         db_update('tasks', 'ord', $new_ord, 'where id=%i', $id);
229                         return;
230                 case 'bottom':
231                         list($lowest_id, $lowest_ord) = db_get_row('tasks', 'id,ord', 'where client_id=%i && state=%i order by ord asc limit 1', $client_id, TASK_QUEUED);
232                         if($lowest_id == $id) {
233                                 message('Already lowest priority');
234                                 return $lowest_ord;
235                         }
236
237                         if($lowest_ord == MIN_PRIORITY) {
238                                 reprioritize_tasks($client_id); # make room
239                                 $lowest_ord = db_get_value('tasks', 'ord', 'where client_id=%i && state=%i order by ord asc limit 1', $client_id, TASK_QUEUED);
240                         }
241
242                         $new_ord = MIN_PRIORITY + floor(($lowest_ord - MIN_PRIORITY) / 2);
243                         db_update('tasks', 'ord', $new_ord, 'where id=%i', $id);
244                         return;
245                 case 'up':
246                 case 'down':
247                         if($change == 'up') {
248                                 $rows = db_get_rows('tasks', 'id,ord', 'where client_id=%i && state=%i order by ord desc, id desc', $client_id, TASK_QUEUED);
249                                 if($rows[0][0] == $id) {
250                                         message('Already highest priority');
251                                         return;
252                                 }
253                                 if($rows[1][0] == $id) {
254                                         prioritize_task($id, 'top');
255                                         return;
256                                 }
257                         } else {
258                                 $rows = db_get_rows('tasks', 'id,ord', 'where client_id=%i && state=%i order by ord asc, id asc', $client_id, TASK_QUEUED);
259                                 if($rows[0][0] == $id) {
260                                         message('Already lowest priority');
261                                         return;
262                                 }
263                                 if($rows[1][0] == $id) {
264                                         prioritize_task($id, 'bottom');
265                                         return;
266                                 }
267                         }
268                         # find the one we're moving
269                         $cur_index = 0;
270                         $done = count($rows);
271                         for($i = 2; $i < $done ; ++$i) {
272                                 if($rows[$i][0] == $id) {
273                                         $cur_index = $i;
274                                         break;
275                                 }
276                         }
277                         $before_ord = $rows[$cur_index - 1][1];
278                         $before_before_ord = $rows[$cur_index - 2][1];
279                         if(abs($before_before_ord - $before_ord) < 2) {
280                                 reprioritize_tasks($client_id);
281                                 $before_ord = db_get_value('tasks', 'ord', 'where id=%i', $rows[$cur_index - 1][0]);
282                                 $before_before_ord = db_get_value('tasks', 'ord', 'where id=%i', $rows[$cur_index - 2][0]);
283                                 if($before_before_ord == $before_ord) {
284                                         message('Programmer error #8592');
285                                         return;
286                                 }
287                         }
288                         $new_ord = $before_ord + floor(($before_before_ord - $before_ord) / 2);
289                         db_update('tasks', 'ord', $new_ord, 'where id=%i', $id);
290                         return;
291                 default:
292                         message('invalid change');
293                         return;
294         }
295
296 }
297
298 function tasks_edit_main() {
299         $state = TASK_DRAFT; # will be overwritten
300         $client_id = logged_in(); # fixed shortly if we're contractor, unless it's a new task by the contractor
301         $edit_id = format_int($_REQUEST['tasks_edit_id']);
302         unset($_REQUEST['tasks_edit_id']);
303         if($edit_id) {
304                 $owner = db_get_value('tasks', 'client_id', 'where id=%i', $edit_id);
305                 if(logged_in_as_contractor()) {
306                         $client_id = $owner;
307                 } elseif($owner != $client_id) {
308                         message('Sorry, that task was entered by/for another client.');
309                         return './';
310                 }
311
312                 # add hidden field for database id of row we're editing
313                 tem_set('tasks_edit_id', $edit_id);
314                 tem_show('editing');
315
316                 $state = db_get_value('tasks', 'state', 'where id=%i', $edit_id);
317         }
318
319         if(isset($_REQUEST['bump'])) {
320                 switch($_REQUEST['bump']) {
321                         case 'up':
322                         case 'down':
323                         case 'top':
324                         case 'bottom':
325                                 prioritize_task($edit_id, $_REQUEST['bump']);
326                                 return './';
327                 }
328         }
329
330         if(isset($_REQUEST['tasks_new_bug'])) {
331                 $state = TASK_BUG;
332         }
333
334         if(isset($_REQUEST['tasks_hold_id'])) {
335                 $id = $_REQUEST['tasks_hold_id'];
336                 db_update('tasks', 'state', TASK_ON_HOLD, 'where id=%i', $id);
337                 message("Task removed from Jason's to-do list.");
338                 return './';
339         }
340
341         if(isset($_REQUEST['tasks_mark_paid_id'])) {
342                 if(!logged_in_as_contractor()) {
343                         message("Error: only Jason can mark tasks as paid.");
344                         return './';
345                 }
346                 $id = $_REQUEST['tasks_mark_paid_id'];
347                 db_update('tasks', 'paid', 1, 'where id=%i', $id);
348                 message('Marked as paid.');
349                 return './';
350         }
351
352         if(isset($_REQUEST['tasks_approve_price_id'])) {
353                 $id = $_REQUEST['tasks_approve_price_id'];
354                 $owner = db_get_value('tasks', 'client_id', 'where id=%i', $id);
355                 if(logged_in() != $owner) {
356                         message("Error: can't approve a task entered by/for another client.");
357                         return './';
358                 }
359                 $ord = new_lowest_priority($owner);
360                 db_update('tasks', 'state,ord', TASK_QUEUED, $ord, 'where id=%i', $id);
361                 message('Price approved.');
362                 return './';
363         }
364
365         if(isset($_REQUEST['tasks_working_id'])) {
366                 $id = $_REQUEST['tasks_working_id'];
367                 if(!logged_in_as_contractor()) {
368                         message("Error: only Jason can say what he's working on.");
369                         return './';
370                 }
371                 db_update('tasks', 'state', TASK_WORKING, 'where id=%i', $id);
372                 message('Task marked as "in progress".');
373                 return './tasks?tasks_id=' . $id;
374         }
375
376         if(isset($_REQUEST['tasks_needs_testing_id'])) {
377                 $id = $_REQUEST['tasks_needs_testing_id'];
378                 if(!logged_in_as_contractor()) {
379                         message("Error: only Jason can say when he's done.");
380                         return './';
381                 }
382                 db_update('tasks', 'state,finished_at', TASK_NEEDS_TESTING, date('Y-m-d'), 'where id=%i', $id);
383                 message('Task awaits testing.');
384                 return './';
385         }
386
387         if(isset($_REQUEST['tasks_finished_id'])) {
388                 $id = $_REQUEST['tasks_finished_id'];
389                 $owner = db_get_value('tasks', 'client_id', 'where id=%i', $id);;
390                 if(logged_in() != $owner) {
391                         message("Error: can't test a task entered by/for another client.");
392                         return './';
393                 }
394                 db_update('tasks', 'state,tested_at', TASK_FINISHED, date('Y-m-d'), 'where id=%i', $id);
395                 message('Task marked as finished.');
396                 # FIXME also mark it as paid if client's balance can cover it
397                 return './';
398         }
399
400         $delete_id = format_int($_REQUEST['tasks_delete_id']);
401         unset($_REQUEST['tasks_delete_id']);
402         if($delete_id) {
403                 db_delete('tasks', 'where id=%i', $delete_id);
404                 message('Task deleted.');
405
406                 return './tasks.html';
407         }
408
409         if(isset($_REQUEST['title'])) {
410                 list($title, $url, $description, $price) = tasks_get_fields();
411                 $queuing = false;
412                 if(logged_in_as_contractor() && $_REQUEST['client_id']) {
413                         $client_id = format_int($_REQUEST['client_id']);
414                 }
415
416                 # FIXME
417                 if(isset($_REQUEST['save_draft'])) {
418                         $state = TASK_DRAFT;
419                 } elseif(isset($_REQUEST['save_bug'])) {
420                         $state = TASK_BUG;
421                 } elseif(isset($_REQUEST['save_price_no_tiny']) && logged_in_as_contractor()) {
422                         $state = TASK_NEEDS_GO_AHEAD;
423                 } elseif(isset($_REQUEST['save_price']) && logged_in_as_contractor()) {
424                         $tiny_agreement = db_get_value('people', 'tiny_agreement', 'where id=%i', $client_id);
425                         if($price <= $tiny_agreement) {
426                                 $state = TASK_QUEUED;
427                                 $queuing = true;
428                         } else {
429                                 $state = TASK_NEEDS_GO_AHEAD;
430                         }
431                 } elseif(isset($_REQUEST['needs_clarification'])) {
432                         $state = TASK_NEEDS_CLARIFICATION;
433                 } else { # better be "request_price"
434                         if(description_has_fixmes($description)) {
435                                 $state = TASK_NEEDS_CLARIFICATION;
436                                 message("Error: Not requesting price. To get this task priced, you'll need to edit the description so it no longer contains \"FIXME\".");
437                         } else {
438                                 $state = TASK_NEEDS_QUOTE;
439                         }
440                 }
441
442                 if(!logged_in_as_contractor() || $edit_id || $_REQUEST['client_id']) {
443                         if($edit_id) {
444                                 $tables = 'title,url,description,state';
445                                 $values = array($title, $url, $description, $state);
446                                 if(isset($_REQUEST['price']) && logged_in_as_contractor()) {
447                                         $tables .= ',price';
448                                         array_push($values, $price);
449                                 }
450                                 if($queuing) {
451                                         $client_id = db_get_value('tasks', 'client_id', 'where id=%i', $edit_id);
452                                         $tables .= ',ord';
453                                         array_push($values, new_lowest_priority($client_id));
454                                 }
455                                 db_update('tasks', $tables, $values, 'where id=%i', $edit_id);
456                                 message('Changes saved.');
457                         } else {
458                                 # new task
459                                 $paid = 0;
460                                 $client_id = logged_in();
461                                 if(logged_in_as_contractor() && $_REQUEST['client_id']) {
462                                         $client_id = format_int($_REQUEST['client_id']);
463                                         $client_name = db_get_value('people', 'name', 'where id=%i', $client_id);
464                                 } else {
465                                         # if client entered the task, no price is set
466                                         $price = 0;
467                                 }
468                                 if($state == TASK_QUEUED) {
469                                         $ord = new_lowest_priority($client_id);
470                                 } else {
471                                         $ord = 0;
472                                 }
473                                 db_insert('tasks', 'client_id,title,url,description,state,paid,price,ord', $client_id, $title, $url, $description, $state, $paid, $price, $ord);
474                                 if(logged_in_as_contractor()) {
475                                         message("Task saved for $client_name.");
476                                 } else {
477                                         message('Task saved.');
478                                 }
479                         }
480                         if($GLOBALS['tasks_form_recipient'] != "fixme@example.com") {
481                                 $to = $GLOBALS['tasks_form_recipient'];
482                                 $from = $to;
483                                 $reply_to = '';
484                                 if(isset($_REQUEST['email']) and valid_email($_REQUEST['email'])) {
485                                         $reply_to = $_REQUEST['email'];
486                                         if($_REQUEST['name'] and ereg('^[a-zA-Z0-9_\' -]*$', $_REQUEST['name']) !== false) {
487                                                 $reply_to = "$_REQUEST[name] <$reply_to>";
488                                         }
489                                 }
490                                 $subject = 'tasks form submitted';
491                                 $message = tem_run('tasks.email.txt');
492                                 $cc = '';
493                                 $bcc = '';
494                                 if(email($from, $to, $subject, $message, $reply_to, $cc, $bcc)) {
495                                         message('Due to an internal error, your message could not be sent. Please try again later.');
496                                         $error = true;
497                                 }
498                         }
499                         if($error !== true) {
500                                 return './';
501                         }
502                 } else {
503                         message('Error: you must select a client for the task');
504                 }
505                 # otherwise, we display the form again. tasks_get_fields() has
506                 # already put the posted values back into the template engine, so they will
507                 # show up in the form fields. You should add some message asking people to
508                 # fix their entry in whatever way you require.
509         } elseif($edit_id) {
510                 # we've recieved an edit id, but no data. So we grab the values to be edited from the database
511                 list($title, $url, $description, $state, $price) = db_get_row('tasks', 'title,url,description,state,price', 'where id=%i', $edit_id);
512                 tasks_tem_sets($title, $url, $description, $price);
513         } else {
514                 # form not submitted, you can set default values like so:
515                 #tem_set('client_id', 'Yes');
516         }
517
518         # display header
519         if($edit_id) {
520                 tem_show('edit_msg');
521         } elseif($state == TASK_BUG) {
522                 tem_show('bug_msg');
523         } else {
524                 tem_show('new_msg');
525                 if(logged_in_as_contractor()) {
526                         pulldown('client_id', db_get_rows('people', 'id,name', 'where id > 1 order by name'), PULLDOWN_2D);
527                         tem_set('client_id', format_int($_REQUEST['client_id']));
528                         tem_show('client_row');
529                 }
530         }
531
532         # display instructions
533         if($state == TASK_BUG) {
534                 tem_show('bug_instructions');
535                 if(logged_in_as_contractor()) {
536                         tem_show('price_field');
537                         tem_show('contractor_submits');
538                 } else {
539                         tem_show('bug_submit');
540                 }
541         } elseif($state == TASK_NEEDS_QUOTE && logged_in_as_contractor()) {
542                 tem_show('set_price_instructions');
543                 tem_show('price_field');
544                 tem_show('contractor_submits');
545         } else {
546                 if(description_has_fixmes($description)) {
547                         tem_show('fixme_instructions');
548                 } else {
549                         tem_show('normal_instructions');
550                 }
551                 if(logged_in_as_contractor()) {
552                         tem_show('contractor_submits');
553                         switch($state) {
554                                 case TASK_DRAFT:
555                                 case TASK_NEEDS_CLARIFICATION:
556                                 case TASK_NEEDS_QUOTE:
557                                 case TASK_NEEDS_GO_AHEAD:
558                                 case TASK_QUEUED:
559                                 case TASK_BUG:
560                                 tem_show('price_field');
561                         }
562                 } else {
563                         tem_show('normal_submits');
564                 }
565         }
566 }
567
568 ?>