[ Index ] |
PHP Cross Reference of Eventum |
[Summary view] [Print] [Text view]
1 <?php 2 /* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */ 3 // +----------------------------------------------------------------------+ 4 // | Eventum - Issue Tracking System | 5 // +----------------------------------------------------------------------+ 6 // | Copyright (c) 2003, 2004, 2005, 2006, 2007 MySQL AB | 7 // | | 8 // | This program is free software; you can redistribute it and/or modify | 9 // | it under the terms of the GNU General Public License as published by | 10 // | the Free Software Foundation; either version 2 of the License, or | 11 // | (at your option) any later version. | 12 // | | 13 // | This program is distributed in the hope that it will be useful, | 14 // | but WITHOUT ANY WARRANTY; without even the implied warranty of | 15 // | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 16 // | GNU General Public License for more details. | 17 // | | 18 // | You should have received a copy of the GNU General Public License | 19 // | along with this program; if not, write to: | 20 // | | 21 // | Free Software Foundation, Inc. | 22 // | 59 Temple Place - Suite 330 | 23 // | Boston, MA 02111-1307, USA. | 24 // +----------------------------------------------------------------------+ 25 // | Authors: Bryan Alsdorf <bryan@mysql.com> | 26 // +----------------------------------------------------------------------+ 27 // 28 // @(#) $Id: class.customer_stats_report.php 3246 2007-02-09 09:10:12Z glen $ 29 // 30 31 require_once (APP_INC_PATH . "class.error_handler.php"); 32 require_once (APP_INC_PATH . "class.time_tracking.php"); 33 require_once(APP_PEAR_PATH . "Math/Stats.php"); 34 35 /** 36 * The Customer Stats report will be too complex to group with the rest of 37 * the reports so I am seperating it into a seperate class. 38 * 39 * @version 1.0 40 * @author Bryan Alsdorf <bryan@mysql.com> 41 */ 42 43 class Customer_Stats_Report 44 { 45 /** 46 * The ID of the project this report is for. 47 * @var integer 48 */ 49 var $prj_id; 50 51 /** 52 * Support Levels to show 53 * @var array 54 */ 55 var $levels; 56 57 /** 58 * Customers to display stats for 59 * @var array 60 */ 61 var $customers; 62 63 /** 64 * Start date of the report 65 * @var string 66 */ 67 var $start_date; 68 69 /** 70 * End date of the report 71 * @var string 72 */ 73 var $end_date; 74 75 /** 76 * The current customer restriction 77 * @var array 78 */ 79 var $current_customers; 80 81 /** 82 * If expired contracts should be excluded. 83 * @var boolean 84 */ 85 var $exclude_expired_contracts; 86 87 /** 88 * An array listing the union of time tracking categories that have data. 89 * @var array 90 */ 91 var $time_tracking_categories = array(); 92 93 /** 94 * Class Constructor. Accepts the support level, customer, 95 * start date and end date to be used in this report. If a customer is 96 * specified the support level is ignored. If the date is left off or invalid all dates are included. 97 * 98 * @access public 99 * @param integer $prj_id The id of the project this report is for. 100 * @param array $levels The support levels that should be shown in this report. 101 * @param array $customers The customers this report should be for. 102 * @param string $start_date The start date of this report. 103 * @param string $end_date The end date of this report. 104 */ 105 function Customer_Stats_Report($prj_id, $levels, $customers, $start_date, $end_date) 106 { 107 $this->prj_id = $prj_id; 108 $this->levels = $levels; 109 $this->customers = $customers; 110 $this->start_date = $start_date; 111 $this->end_date = $end_date; 112 } 113 114 115 /** 116 * Returns all data for this report. 117 * 118 * @access public 119 * @return array 120 */ 121 function getData() 122 { 123 $data = array(); 124 125 // determine if this should be customer based or support level based. 126 if ($this->isCustomerBased()) { 127 // customer based 128 129 // get "all" row of data 130 $data[] = $this->getAllRow(); 131 132 foreach ($this->customers as $customer_id) { 133 $details = Customer::getDetails($this->prj_id, $customer_id); 134 $data[] = $this->getDataRow($details["customer_name"], array($customer_id)); 135 } 136 } else { 137 // support level based 138 if (count($this->levels) > 0) { 139 $grouped_levels = Customer::getGroupedSupportLevels($this->prj_id); 140 foreach ($this->levels as $level_name) { 141 if ($level_name == "Aggregate") { 142 // get "all" row of data 143 $data[] = $this->getAllRow(); 144 continue; 145 } 146 147 $support_options = array(); 148 if ($this->exclude_expired_contracts) { 149 $support_options[] = CUSTOMER_EXCLUDE_EXPIRED; 150 } 151 $customers = Customer::getListBySupportLevel($this->prj_id, $grouped_levels[$level_name], $support_options); 152 $data[] = $this->getDataRow($level_name, $customers); 153 } 154 } 155 } 156 157 return $data; 158 } 159 160 161 /** 162 * Returns data row for specified name and customers. 163 * 164 * @param string $name Name of data row. 165 * @param string $customers Customers to include in this row. 166 * @return array An array of data. 167 */ 168 function getDataRow($name, $customers) 169 { 170 $this->current_customers = $customers; 171 return array( 172 "title" => $name, 173 "customer_counts" => $this->getCustomerCounts($name), 174 "issue_counts" => $this->getIssueCounts($name), 175 "email_counts" => $this->getEmailCounts(), 176 "time_tracking" => $this->getTimeTracking(), 177 "time_stats" => $this->getTimeStats() 178 ); 179 } 180 181 182 /** 183 * Returns the "all" row, that is the row that always appears at the top of the report 184 * and covers all support levels and customers regardless of what is selected. 185 * 186 * @access private 187 * @return array The array of data for this row. 188 */ 189 function getAllRow() 190 { 191 $row = array( 192 "title" => ev_gettext("Aggregate") 193 ); 194 195 // get complete list of customers. 196 $all_levels = array(); 197 $levels = Customer::getSupportLevelAssocList($this->prj_id); 198 foreach ($levels as $level_id => $level_name) { 199 $all_levels[] = $level_id; 200 } 201 if ($this->exclude_expired_contracts) { 202 $support_option = CUSTOMER_EXCLUDE_EXPIRED; 203 } else { 204 $support_option = array(); 205 } 206 $this->current_customers = Customer::getListBySupportLevel($this->prj_id, $all_levels, $support_option); 207 208 // get customers 209 $row["customer_counts"] = $this->getCustomerCounts("All"); 210 211 // get total # of issues, avg issues per customer, median issues per customer 212 $row['issue_counts'] = $this->getIssueCounts("All"); 213 214 // get actions counts such as # of customer actions per issue, avg customer actions per issue, 215 // median customer actions per issue. 216 $row['email_counts'] = $this->getEmailCounts(); 217 218 // get time tracking information 219 $row['time_tracking'] = $this->getTimeTracking(); 220 221 // get other time related stats such as avg and median time between issues and avg and median time to close. 222 $row['time_stats'] = $this->getTimeStats(); 223 224 return $row; 225 } 226 227 228 /** 229 * Returns various customer statistics. 230 * 231 * @access private 232 * @param string $name The name of this data row. 233 * @return array Array of statistics 234 */ 235 function getCustomerCounts($name) 236 { 237 $customer_count = count($this->current_customers); 238 239 // split by low/medium/high 240 $issue_counts = $this->getIssueCountsByCustomer($name); 241 $activity = array( 242 'low' => 0, 243 'medium' => 0, 244 'high' => 0 245 ); 246 if ((is_array($issue_counts)) && (count($issue_counts) > 0)) { 247 foreach ($issue_counts as $count) { 248 if ($count <= 2) { 249 $activity['low']++; 250 } elseif ($count > 2 && $count <= 8) { 251 $activity['medium']++; 252 } elseif ($count > 8) { 253 $activity['high']++; 254 } 255 } 256 } 257 if ($customer_count > 0) { 258 foreach ($activity as $key => $value) { 259 $activity[$key] = ($value * 100) / $customer_count; 260 } 261 $inactive_count = ((($customer_count - count($issue_counts)) * 100) / $customer_count); 262 } else { 263 $inactive_count = 0; 264 } 265 266 return array( 267 "customer_count" => $customer_count, 268 "activity" => $activity, 269 "active" => count($issue_counts), 270 "inactive" => $inactive_count 271 ); 272 } 273 274 275 /** 276 * Returns the counts relating to number of issues. 277 * - total: total number of issues for the support level. 278 * - avg: Average number of issues opened by customers for support level. 279 * - median: Median number of issues opened by customers for support level. 280 * 281 * @access private 282 * @param string $name The name of this data row. 283 * @return array Array of counts. 284 */ 285 function getIssueCounts($name) 286 { 287 $issue_counts = $this->getIssueCountsByCustomer($name); 288 if ((is_array($issue_counts)) && (count($issue_counts) > 0)) { 289 $stats = new Math_Stats(); 290 $stats->setData($issue_counts); 291 292 return array( 293 "total" => $stats->sum(), 294 "avg" => $stats->mean(), 295 "median" => $stats->median(), 296 "max" => $stats->max() 297 ); 298 } else { 299 return array( 300 "total" => 0, 301 "avg" => 0, 302 "median" => 0, 303 "max" => 0 304 ); 305 } 306 } 307 308 /** 309 * Returns an array of issue counts for customers. 310 * 311 * @access private 312 * @param string $name The name of this data row. 313 */ 314 function getIssueCountsByCustomer($name) 315 { 316 static $issue_counts; 317 318 // poor man's caching system... 319 if (!empty($issue_counts[$name])) { 320 return $issue_counts[$name]; 321 } 322 323 $stmt = "SELECT 324 count(*) 325 FROM 326 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 327 WHERE 328 " . $this->getWhereClause("iss_customer_id", "iss_created_date") . " 329 GROUP BY 330 iss_customer_id"; 331 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 332 if (PEAR::isError($res)) { 333 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 334 return ""; 335 } 336 $issue_counts[$name] = $res; 337 return $res; 338 } 339 340 341 /** 342 * Returns the counts relating to # of customer and developer emails. 343 * 344 * @access public 345 * @return array Array of counts. 346 */ 347 function getEmailCounts() 348 { 349 $counts = array( 350 "customer" => array(), 351 "developer" => array() 352 ); 353 $stmt = "SELECT 354 count(*) 355 FROM 356 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email, 357 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "email_account, 358 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 359 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_user 360 WHERE 361 sup_ema_id = ema_id AND 362 sup_iss_id = iss_id AND 363 sup_usr_id = pru_usr_id AND 364 ema_prj_id = pru_prj_id AND 365 pru_role = " . User::getRoleID('Customer') . " AND 366 " . $this->getWhereClause("iss_customer_id", "sup_date") . " 367 GROUP BY 368 sup_iss_id"; 369 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 370 if (PEAR::isError($res)) { 371 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 372 return array(); 373 } 374 375 if (count($res) > 0) { 376 $stats = new Math_Stats(); 377 $stats->setData($res); 378 379 $counts["customer"]["total"] = $stats->sum(); 380 $counts["customer"]["avg"] = $stats->mean(); 381 $counts["customer"]["median"] = $stats->median(); 382 } else { 383 $counts["customer"]["total"] = 0; 384 $counts["customer"]["avg"] = 0; 385 $counts["customer"]["median"] = 0; 386 } 387 388 $stmt = "SELECT 389 count(*) 390 FROM 391 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email, 392 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "email_account, 393 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 394 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_user 395 WHERE 396 sup_ema_id = ema_id AND 397 sup_iss_id = iss_id AND 398 sup_usr_id = pru_usr_id AND 399 ema_prj_id = pru_prj_id AND 400 pru_role != " . User::getRoleID('Customer') . " AND 401 " . $this->getWhereClause("iss_customer_id", "sup_date") . " 402 GROUP BY 403 sup_iss_id"; 404 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 405 if (PEAR::isError($res)) { 406 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 407 return array(); 408 } 409 if (count($res) > 0) { 410 $stats = new Math_Stats(); 411 $stats->setData($res); 412 413 $counts["developer"]["total"] = $stats->sum(); 414 $counts["developer"]["avg"] = $stats->mean(); 415 $counts["developer"]["median"] = $stats->median(); 416 } else { 417 $counts["developer"]["total"] = 0; 418 $counts["developer"]["avg"] = 0; 419 $counts["developer"]["median"] = 0; 420 } 421 422 return $counts; 423 } 424 425 426 /** 427 * Returns information from time tracking module, split by category 428 * 429 * @access private 430 * @return array Array of counts. 431 */ 432 function getTimeTracking() 433 { 434 $time = array(); 435 436 // get total stats 437 $time[0] = $this->getIndividualTimeTracking(); 438 $time[0]["name"] = "Total"; 439 $this->time_tracking_categories[0] = "Total"; 440 441 // get categories 442 $categories = Time_Tracking::getAssocCategories(); 443 foreach ($categories as $ttc_id => $category) { 444 $individual = $this->getIndividualTimeTracking($ttc_id); 445 if (count($individual) > 0) { 446 $time[$ttc_id] = $individual; 447 $time[$ttc_id]["name"] = $category; 448 449 $this->time_tracking_categories[$ttc_id] = $category; 450 } 451 } 452 453 return $time; 454 } 455 456 457 /** 458 * Returns time tracking information for a certain category, or all categories if no category is passed. 459 * 460 * @access public 461 * @param $ttc_id The id of the time tracking category. Default false 462 * @return array Array of time tracking information 463 */ 464 function getIndividualTimeTracking($ttc_id = false) 465 { 466 $stmt = "SELECT 467 ttr_time_spent 468 FROM 469 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "time_tracking, 470 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 471 WHERE 472 ttr_iss_id = iss_id"; 473 if ($ttc_id != false) { 474 $stmt .= "\n AND ttr_ttc_id = $ttc_id"; 475 } 476 $stmt .= "\nAND " . $this->getWhereClause("iss_customer_id", "ttr_created_date"); 477 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 478 if (PEAR::isError($res)) { 479 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 480 return array(); 481 } 482 if (count($res) > 0) { 483 $stats = new Math_Stats(); 484 $stats->setData($res); 485 $total = $stats->sum(); 486 $avg = $stats->mean(); 487 $median = $stats->median(); 488 return array( 489 "total" => $total, 490 "total_formatted" => Misc::getFormattedTime($total, true), 491 "avg" => $avg, 492 "avg_formatted" => Misc::getFormattedTime($avg), 493 "median"=> $median, 494 "median_formatted" => Misc::getFormattedTime($median), 495 ); 496 } else { 497 return array(); 498 } 499 } 500 501 502 /** 503 * Returns information about time to close and time to first response. 504 * 505 * @access private 506 * @return array Array of counts. 507 */ 508 function getTimeStats() 509 { 510 // time to close 511 $stmt = "SELECT 512 round(((unix_timestamp(iss_closed_date) - unix_timestamp(iss_created_date)) / 60)) 513 FROM 514 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 515 WHERE 516 iss_closed_date IS NOT NULL AND 517 " . $this->getWhereClause("iss_customer_id", array("iss_created_date", "iss_closed_date")); 518 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 519 if (PEAR::isError($res)) { 520 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 521 return array(); 522 } 523 if (count($res) > 0) { 524 $stats = new Math_Stats(); 525 $stats->setData($res); 526 527 $time_to_close = array( 528 "avg" => $stats->mean(), 529 "avg_formatted" => Misc::getFormattedTime($stats->mean()), 530 "median"=> $stats->median(), 531 "median_formatted" => Misc::getFormattedTime($stats->median()), 532 "max" => $stats->max(), 533 "max_formatted" => Misc::getFormattedTime($stats->max()), 534 "min" => $stats->min(), 535 "min_formatted" => Misc::getFormattedTime($stats->min()) 536 ); 537 } else { 538 $time_to_close = array( 539 "avg" => 0, 540 "avg_formatted" => Misc::getFormattedTime(0), 541 "median"=> 0, 542 "median_formatted" => Misc::getFormattedTime(0), 543 "max" => 0, 544 "max_formatted" => Misc::getFormattedTime(0), 545 "min" => 0, 546 "min_formatted" => Misc::getFormattedTime(0) 547 ); 548 } 549 550 // time to first response 551 $stmt = "SELECT 552 round(((unix_timestamp(iss_first_response_date) - unix_timestamp(iss_created_date)) / 60)) 553 FROM 554 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 555 WHERE 556 iss_first_response_date IS NOT NULL AND 557 " . $this->getWhereClause("iss_customer_id", array("iss_created_date", "iss_closed_date")); 558 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 559 if (PEAR::isError($res)) { 560 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 561 return array(); 562 } 563 if (count($res) > 0) { 564 $stats = new Math_Stats(); 565 $stats->setData($res); 566 567 $time_to_first_response = array( 568 "avg" => $stats->mean(), 569 "avg_formatted" => Misc::getFormattedTime($stats->mean()), 570 "median"=> $stats->median(), 571 "median_formatted" => Misc::getFormattedTime($stats->median()), 572 "max" => $stats->max(), 573 "max_formatted" => Misc::getFormattedTime($stats->max()), 574 "min" => $stats->min(), 575 "min_formatted" => Misc::getFormattedTime($stats->min()) 576 ); 577 } else { 578 $time_to_first_response = array( 579 "avg" => 0, 580 "avg_formatted" => Misc::getFormattedTime(0), 581 "median"=> 0, 582 "median_formatted" => Misc::getFormattedTime(0), 583 "max" => 0, 584 "max_formatted" => Misc::getFormattedTime(0), 585 "min" => 0, 586 "min_formatted" => Misc::getFormattedTime(0) 587 ); 588 } 589 590 return array( 591 "time_to_close" => $time_to_close, 592 "time_to_first_response" => $time_to_first_response 593 ); 594 } 595 596 597 /** 598 * Returns if this report is customer based 599 * 600 * @return boolean 601 */ 602 function isCustomerBased() 603 { 604 return ((is_array($this->customers)) && (count($this->customers) > 0) && (!in_array("", $this->customers))); 605 } 606 607 608 /** 609 * Sets if expired contracts should be exclude 610 * 611 * @access public 612 * @param boolean $split If expired contracts should be excluded 613 */ 614 function excludeExpired($exclude) 615 { 616 $this->exclude_expired_contracts = $exclude; 617 } 618 619 620 /** 621 * Returns where clause based on what the current support level/customer is set to, and date range currently set. 622 * If $date_field is an array, the fields will be ORed together. 623 * 624 * @param string $customer_field The name of customer_id field 625 * @param mixed $date_field The name of the date field 626 * @return string A string with the SQL limiting the resultset 627 */ 628 function getWhereClause($customer_field, $date_field) 629 { 630 $where = ''; 631 if (!empty($customer_field)) { 632 if (count($this->current_customers) > 0) { 633 $where .= $customer_field . " IN(" . join(",",$this->current_customers) . ")"; 634 } else { 635 // XXX: this is a dirty hack to handle support levels that don't have customers, but I can't think of anything better right now. 636 $where .= "1 = 2"; 637 } 638 } 639 640 641 if ((!empty($this->start_date)) && (!empty($this->end_date))) { 642 if (!empty($customer_field)) { 643 $where .= " AND\n"; 644 } 645 if (is_array($date_field)) { 646 $date_conditions = array(); 647 foreach ($date_field as $field) { 648 $date_conditions[] = "($field BETWEEN '" . $this->start_date . "' AND '" . $this->end_date . "')"; 649 } 650 $where .= "(" . join(" OR ", $date_conditions) . ")"; 651 } else { 652 $where .= "($date_field BETWEEN '" . $this->start_date . "' AND '" . $this->end_date . "')"; 653 } 654 } 655 return $where; 656 } 657 658 659 /** 660 * Returns the text for the row label. Will be "Support Level" if viewing support levels and "Customer" if viewing a specific customer. 661 * 662 * @access public 663 * @return string The text for the row label. 664 */ 665 function getRowLabel() 666 { 667 if ($this->isCustomerBased()) { 668 return ev_gettext("Customer"); 669 } else { 670 return ev_gettext("Support Level"); 671 } 672 } 673 674 675 /** 676 * Returns an array of graph types 677 * 678 * @access public 679 * @return array An array of graph types 680 */ 681 function getGraphTypes() 682 { 683 return array( 684 1 => array( 685 "title" => ev_gettext("Total Workload by Support Level"), 686 "desc" => ev_gettext("Includes issue count, Developer email Count, Customer Email Count, Customers count by Support Level"), 687 "size" => array( 688 "x" => 800, 689 "y" => 350 690 ) 691 ), 692 2 => array( 693 "title" => ev_gettext("Avg Workload per Customer by Support Level"), 694 "desc" => ev_gettext("Displays average number of issues, developer emails and customer emails per issue by support level"), 695 "size" => array( 696 "x" => 800, 697 "y" => 350 698 ), 699 "value_format" => "%.1f" 700 ), 701 3 => array( 702 "title" => ev_gettext("Avg and Median Time to Close by Support Level"), 703 "desc" => ev_gettext("Displays time stats"), 704 "size" => array( 705 "x" => 600, 706 "y" => 350 707 ), 708 "y_label" => ev_gettext("Days") 709 ), 710 4 => array( 711 "title" => ev_gettext("Avg and Median Time to First Response by Support Level"), 712 "desc" => ev_gettext("Displays time stats"), 713 "size" => array( 714 "x" => 600, 715 "y" => 350 716 ), 717 "y_label" => ev_gettext("Hours") 718 ) 719 ); 720 } 721 722 723 /** 724 * Returns the list of sections that can be displayed. 725 * 726 * @access public 727 * @return array An array of sections. 728 */ 729 function getDisplaySections() 730 { 731 return array( 732 "customer_counts" => ev_gettext("Customer Counts"), 733 "issue_counts" => ev_gettext("Issue Counts"), 734 "email_counts" => ev_gettext("Email Counts"), 735 "time_stats" => ev_gettext("Time Statistics"), 736 "time_tracking" => ev_gettext("Time Tracking") 737 ); 738 } 739 740 /** 741 * Returns the list of time tracking categories that have data. 742 * 743 * @access public 744 * @return array An array of time tracking categories 745 */ 746 function getTimeTrackingCategories() 747 { 748 return $this->time_tracking_categories; 749 } 750 } 751 752 // benchmarking the included file (aka setup time) 753 if (APP_BENCHMARK) { 754 $GLOBALS['bench']->setMarker('Included Customer_Stats_Report Class'); 755 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Dec 19 21:21:33 2007 | Cross-referenced by PHPXref 0.7 |