You are here

support_pm.module in Support Ticketing System 6

Same filename and directory in other branches
  1. 7 support_pm/support_pm.module

Support Project Management. @author Jeremy Andrews <jeremy@tag1consulting.com> @package Support

File

support_pm/support_pm.module
View source
<?php

/**
* @file 
* Support Project Management.
* @author Jeremy Andrews <jeremy@tag1consulting.com>
* @package Support
*/

/**
 * TODO:
 *  - JavaScript to calculate support plan totals
 *  - Combined "calendar" view showing all plans on one page
 */

/**
 * Implementation of hook_perm();
 */
function support_pm_perm() {
  return array(
    'create plans',
    'view all plans',
    'administer plans',
    'view support projects',
    'administer support projects',
  );
}

/**
 * Implementation of hook_menu().
 * TODO: Include date in 'view' and 'edit' tabs
 */
function support_pm_menu() {
  $items = array();
  $items['user/%user/support_plan'] = array(
    'title' => 'Support plan',
    'description' => 'Support planning',
    'page callback' => 'support_pm_plan_overview_weekly',
    'page arguments' => array(
      1,
    ),
    'access arguments' => array(
      'create plans',
    ),
    'type' => MENU_LOCAL_TASK,
  );
  $items['user/%user/support_plan/view'] = array(
    'title' => 'Show plan',
    'description' => 'Create support plans',
    'page callback' => 'support_pm_plan_overview_weekly',
    'page arguments' => array(
      1,
    ),
    'access arguments' => array(
      'create plans',
    ),
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['user/%user/support_plan/edit'] = array(
    'title' => 'Edit plan',
    'description' => 'Create support plans',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'support_pm_user_week',
      1,
    ),
    'access arguments' => array(
      'create plans',
    ),
    'weight' => 2,
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/support/plan_report'] = array(
    'title' => 'Plan reports',
    'description' => 'Generate support plan reports',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'support_pm_admin_reports',
    ),
    'access arguments' => array(
      'administer support',
    ),
    'file' => 'support_pm.admin.inc',
  );
  $items['admin/support/plan_settings'] = array(
    'title' => 'Plan report settings',
    'description' => 'Configure support plans',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'support_pm_admin_settings',
    ),
    'access arguments' => array(
      'administer support',
    ),
    'file' => 'support_pm.admin.inc',
  );
  $items['admin/support/project'] = array(
    'title' => 'Projects',
    'description' => 'Configure support projects',
    'page callback' => 'support_pm_admin_project_overview',
    'access arguments' => array(
      'administer support projects',
    ),
    'file' => 'support_pm.admin.inc',
  );
  $items['admin/support/project/list'] = array(
    'title' => 'List',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/support/project/add'] = array(
    'title' => 'Add project',
    'type' => MENU_LOCAL_TASK,
    'weight' => 2,
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'support_pm_admin_project_form',
    ),
    'access arguments' => array(
      'administer support projects',
    ),
    'file' => 'support_pm.admin.inc',
  );
  $items['admin/support/project/%support_pm_project/edit'] = array(
    'title' => 'Edit',
    'type' => MENU_CALLBACK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'support_pm_admin_project_form',
      3,
    ),
    'access arguments' => array(
      'administer support projects',
    ),
    'file' => 'support_pm.admin.inc',
  );
  $items['support_pm/image/%'] = array(
    'page callback' => 'support_pm_display_swatch',
    'page arguments' => array(
      2,
    ),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );

  // Add project sub-menu
  $states = array(
    'all' => 'all',
    'all open' => 'all open',
    'my open' => 'my open',
  ) + _support_states();
  $result = db_query('SELECT sp.projid, sp.project, sp.path, sp.weight, spc.clid FROM {support_project} sp LEFT JOIN {support_project_client} spc ON sp.projid = spc.projid WHERE disabled = 0 ORDER BY spc.clid, sp.weight ASC, sp.project');
  while ($project = db_fetch_object($result)) {
    if ($client = support_client_load($project->clid)) {
      $clients = array(
        $client,
      );
    }
    else {
      $clients = array();
      $result2 = db_query('SELECT clid FROM {support_client} WHERE status = 1');
      while ($client = db_fetch_object($result2)) {
        $clients[] = support_client_load($client->clid);
      }
    }
    foreach ($clients as $client) {
      foreach ($states as $sid => $state) {
        if ($client->parent == 0) {
          $items["support/{$client->path}/{$state}/{$project->path}"] = array(
            'title' => check_plain($project->project),
            'page callback' => 'drupal_get_form',
            'page arguments' => array(
              'support_page_form',
              $client->clid,
              $state,
            ),
            'access callback' => 'support_access_clients',
            'access arguments' => array(
              $client,
            ),
            'weight' => $project->weight,
            'type' => MENU_LOCAL_TASK,
          );
        }
        else {
          $parent = support_client_load($client->parent);
          $items["support/{$parent->path}/{$client->path}/{$state}/{$project->path}"] = array(
            'title' => check_plain($project->project),
            'page callback' => 'drupal_get_form',
            'page arguments' => array(
              'support_page_form',
              $client->clid,
              $state,
            ),
            'access callback' => 'support_access_clients',
            'access arguments' => array(
              $client,
            ),
            'weight' => $project->weight,
            'type' => MENU_LOCAL_TASK,
          );
        }
      }
    }
  }
  return $items;
}

/**
 * Implementation of hook_theme().
 */
function support_pm_theme() {
  return array(
    'support_pm_user_week' => array(
      'arguments' => array(
        'form' => NULL,
      ),
    ),
    'support_pm_pager' => array(
      'arguments' => array(
        'text' => NULL,
        'op' => NULL,
        'parameters' => array(),
        'attributes' => array(),
      ),
    ),
    'support_pm_user_client_hours_details' => array(
      'arguments' => array(
        'day' => array(),
        'scale' => NULL,
      ),
    ),
    'support_pm_user_hours_summary' => array(
      'arguments' => array(
        'totals' => array(),
        'load_callback' => NULL,
        'max' => NULL,
        'message' => 'Mismatch',
      ),
    ),
    'support_pm_plan_diff' => array(
      'arguments' => array(
        'diff' => NULL,
        'plan' => NULL,
      ),
    ),
  );
}

/**
 * Implementation of hook_nodeapi().
 */
function support_pm_nodeapi(&$node, $op, $teaser, $page) {
  if ($node->type == 'support_ticket') {
    switch ($op) {
      case 'view':
        if (user_access('view support projects')) {
          if ($project = support_pm_project_load_nid($node->nid)) {
            $node->content['support-project'] = array(
              '#value' => "<div class='support-priority'>Project: " . check_plain($project->project) . '</div>',
              '#weight' => -1,
            );
          }
        }
        break;
      case 'load':
        $node->project = support_pm_project_load_nid($node->nid);
        break;
      case 'insert':
      case 'update':
        db_query("UPDATE {support_project_ticket} SET projid = %d WHERE nid = %d", $node->project, $node->nid);
        if (!db_affected_rows()) {
          @db_query("INSERT INTO {support_project_ticket} (projid, nid) VALUES(%d, %d)", $node->project, $node->nid);
        }
        break;
      case 'delete':
        db_query("DELETE FROM {support_project_ticket} WHERE nid = %d", $node->nid);
        break;
    }
  }
}
function support_pm_comment(&$comment, $op) {
  if (is_array($comment)) {
    $node = node_load($comment['nid']);
  }
  else {
    $node = node_load($comment->nid);
  }
  if ($node->type == 'support_ticket') {
    switch ($op) {
      case 'view':
        if (user_access('view support projects')) {
          if ($project = support_pm_project_load_nid($node->nid)) {
            $comment->comment = "<div class='support-priority'>Project: " . check_plain($project->project) . '</div>' . $comment->comment;
          }
        }
    }
  }
}
function support_pm_form_alter(&$form, $form_state, $form_id) {
  if ($form_id == 'support_ticket_node_form') {
    $node = $form['#node'];

    // @todo: include disabled project if already set to it
    $options = support_pm_load_projects(_support_current_client());
    $form['support']['project'] = array(
      '#type' => 'select',
      '#title' => t('Project'),
      '#prefix' => '&nbsp;&nbsp;',
      '#options' => $options,
      '#default_value' => isset($node->project) && isset($node->project->projid) ? $node->project->projid : support_pm_default_project($options),
    );
  }
}
function support_pm_db_rewrite_sql($query, $primary_table, $primary_field, $args) {

  // We need to ensure that we don't mistake a state for a project.
  $path = FALSE;
  if (arg(3)) {
    $path = arg(3);
    $states = array(
      'all' => 'all',
      'all open' => 'all open',
      'my open' => 'my open',
    ) + _support_states();
    $states = array_values($states);
    if (in_array($path, $states)) {

      // Hmm, arg(3) was a state, we must be on a subclient.
      $path = arg(4);
    }
  }
  if ($primary_table == 'n' && $primary_field == 'nid' && strpos($query, 'ELECT DISTINCT') && strpos($query, 'support_ticket') && strpos($query, 'client =') && $path) {
    if ($path == preg_replace('/[^0-9a-zA-Z_-]/', '', $path)) {
      return array(
        'join' => ' LEFT JOIN {support_project_ticket} spt ON n.nid = spt.nid LEFT JOIN {support_project} sp ON spt.projid = sp.projid',
        'where' => "sp.path = '{$path}'",
      );
    }
  }
  else {
    if ($primary_table == 'n' && $primary_field == 'nid' && strpos($query, 'support_ticket_timer') && strpos($query, 't.client IN')) {
      $project = isset($_GET['project']) ? $_GET['project'] : '';
      if ($project && $project == preg_replace('/[^0-9a-zA-Z_-]:/', '', $project)) {
        list($project, $client) = explode(':', $project);
        if (!$client) {
          $client = _support_current_client();
        }
        $alter = array(
          'join' => ' LEFT JOIN {support_project_ticket} spt ON n.nid = spt.nid LEFT JOIN {support_project} sp ON spt.projid = sp.projid LEFT JOIN {support_project_client} spc ON sp.projid = spc.projid',
        );
        if (strtolower($project) == 'null') {
          $alter['where'] = "ISNULL(sp.path) AND spc.clid = {$client}";
        }
        else {
          $alter['where'] = "sp.path = '{$project}' AND spc.clid = {$client}";
        }
        return $alter;
      }
    }
    else {
      if ($primary_table == 'c' && $primary_field == 'cid' && strpos($query, 'support_ticket_comment_timer') && strpos($query, 't.client IN')) {
        $project = isset($_GET['project']) ? $_GET['project'] : '';
        if ($project && $project == preg_replace('/[^0-9a-zA-Z_-]:/', '', $project)) {
          list($project, $client) = explode(':', $project);
          if (!$client) {
            $client = _support_current_client();
          }
          $alter = array(
            'join' => ' LEFT JOIN {support_project_ticket} spt ON c.nid = spt.nid LEFT JOIN {support_project} sp ON spt.projid = sp.projid LEFT JOIN {support_project_client} spc ON sp.projid = spc.projid',
          );
          if (strtolower($project) == 'null') {
            $alter['where'] = "ISNULL(sp.path) AND spc.clid = {$client}";
          }
          else {
            $alter['where'] = "sp.path = '{$project}' AND spc.clid = {$client}";
          }
          return $alter;
        }
      }
    }
  }
}

/**
 * Allow projects to be selected.
 */
function support_pm_support_timer_client_report_alter(&$report) {
  $report->filters .= drupal_get_form('support_pm_invoice_ui_form');
}

/**
 * Provide form for selecting projects.
 */
function support_pm_invoice_ui_form() {
  $form = array();
  if (user_access('view support projects')) {
    drupal_add_js('$(document).ready(function() { $("form#support-pm-invoice-ui-form select").change(function() { $("form#support-pm-invoice-ui-form").submit(); }); });', 'inline');
    $projects = array(
      -1 => t('-- no project --'),
      0 => t('-- all projects --'),
    ) + support_pm_load_projects(_support_current_client(), TRUE);
    $project = isset($_GET['project']) ? $_GET['project'] : '';
    if ($project && $project == preg_replace('/[^0-9a-zA-Z_-]:/', '', $project)) {
      if (strtolower($project) == 'null') {
        $selected = -1;
      }
      else {
        list($project, $client) = explode(':', $project);
        $selected = (int) db_result(db_query("SELECT projid FROM {support_project} WHERE path = '%s'", $project));
        if ($client) {
          $selected = "{$selected}:{$client}";
        }
      }
    }
    else {
      $selected = 0;
    }
    $form['pm'] = array(
      '#type' => 'fieldset',
      '#title' => t('Project'),
      '#collapsible' => TRUE,
      '#collapsed' => $selected ? FALSE : TRUE,
    );
    $form['pm']['projects'] = array(
      '#title' => t('Project'),
      '#type' => 'select',
      '#options' => $projects,
      '#default_value' => $selected,
      '#description' => t('Filter report by selected project.'),
    );
    $form['pm']['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Update filter'),
    );
  }
  return $form;
}

/**
 * Add url filter when projects are selected.
 */
function support_pm_invoice_ui_form_submit($form, &$form_state) {
  $project = NULL;
  if (!empty($form_state['values']['projects'])) {
    list($projid, $client) = explode(':', $form_state['values']['projects']);
    if (!$client) {
      $client = _support_current_client();
    }
    $projects = support_pm_load_projects($client);
    if (isset($projects[$projid])) {
      $project = support_pm_project_load($projid);
    }
    else {
      if ($form_state['values']['projects'] == -1) {
        $project = new stdClass();
        $project->path = 'null';
      }
    }
  }
  $path = drupal_get_path_alias(isset($_GET['q']) ? $_GET['q'] : '');
  $query = array();
  foreach ($_GET as $key => $value) {
    if (!in_array($key, array(
      'q',
      'project',
    ))) {
      $query[$key] = $value;
    }
  }
  if (is_object($project)) {
    if ($client != _support_current_client()) {
      $query['project'] = "{$project->path}:{$client}";
    }
    else {
      $query['project'] = $project->path;
    }
  }
  drupal_goto($path, $query);
}

/**
 *
 */
function support_pm_plan_overview_weekly($account) {
  $output = NULL;
  $week = isset($_GET['week']) ? _support_pm_first_day((int) $_GET['week']) : _support_pm_first_day(time());

  // Set the page title (keep it consisten with the editing page)
  drupal_set_title(t('@start - @end', array(
    '@start' => format_date($week, 'medium'),
    '@end' => format_date($week + 86400 * 6, 'medium'),
  )));
  $header = array(
    '',
  );
  $row = array(
    '<strong>' . t('Plan') . '</strong>',
  );
  $row2 = array(
    '<strong>' . t('Actual') . '</strong>',
  );
  $totals = array();
  $hours = array();

  // TODO: Allow for 5 day weeks, etc
  for ($i = 0; $i < 7; $i++) {
    $date = $week + 86400 * $i;
    $header[] = t('!day<br />!date', array(
      '!day' => format_date($date, 'custom', 'l'),
      '!date' => format_date($date, 'custom', 'M d'),
    ));
    $day = support_pm_day_load($date, $account);
    if (is_array($day[$account->uid])) {
      $row[] = theme('support_pm_user_client_hours_details', $day[$account->uid]);
    }
    else {
      $row[] = '';
    }

    // Add up totals
    if (is_array($day[$account->uid])) {
      foreach ($day[$account->uid] as $clid => $data) {
        $totals['plan'][$clid] += $data->hours;
      }
    }
    else {
      if (!is_array($totals['plan'])) {
        $totals['plan'] = array();
      }
    }

    // Integrate with the support_timer module, if enabled
    $hour = array();
    if (module_exists('support_timer')) {

      // The support_timer module uses a slightly different date format
      $convert = strtotime(date('d M Y', $date));
      $result = db_query('SELECT tt.time, t.client FROM {support_ticket_timer} tt LEFT JOIN {support_ticket} t ON tt.nid = t.nid LEFT JOIN {node} n ON t.nid = n.nid WHERE tt.date = %d AND n.uid = %d', $convert, $account->uid);
      while ($timer = db_fetch_object($result)) {
        $hour[$timer->client]->hours += support_pm_timer_to_hours($timer->time);
        $hours[$timer->client]->hours += support_pm_timer_to_hours($timer->time);
      }
      $result = db_query('SELECT tt.time, t.client FROM {support_ticket_comment_timer} tt LEFT JOIN {support_ticket_comment} t ON tt.cid = t.cid LEFT JOIN {comments} c ON t.cid = c.cid WHERE tt.date = %d AND c.uid = %d', $convert, $account->uid);
      while ($timer = db_fetch_object($result)) {
        $hour[$timer->client]->hours += support_pm_timer_to_hours($timer->time);
        $hours[$timer->client]->hours += support_pm_timer_to_hours($timer->time);
      }
      $row2[] = theme('support_pm_user_client_hours_details', $hour);
    }
  }
  $rows = array(
    $row,
  );

  // Only display actual data if support_timer is enabled to collect it
  if (count($row2) > 1) {
    $rows[] = $row2;
    foreach ($hours as $clid => $data) {

      // Add up totals
      $totals['actual'][$clid] = $data->hours;
    }
  }
  $output = theme('support_pm_pager', t('‹ previous'), '<');
  $header2 = array(
    t('Plan'),
  );
  $plan_sum = is_array($totals['plan']) ? array_sum($totals['plan']) : 0;
  $actual_sum = is_array($totals['actual']) ? array_sum($totals['actual']) : 0;
  $max = $plan_sum > $actual_sum ? $plan_sum : $actual_sum;
  $row = array(
    theme('support_pm_user_hours_summary', $totals['plan'], 'support_client_load', $max, t('Not scheduled')),
  );
  if (count($row2) > 1) {
    $header2[] = t('Actual');
    $row[] = theme('support_pm_user_hours_summary', $totals['actual'], 'support_client_load', $max, t('Not worked'));
  }
  $rows2 = array(
    $row,
  );
  $output .= theme('table', $header2, $rows2, array(
    'id' => 'support_pm_summary',
  ));
  $output .= theme('table', $header, $rows, array(
    'id' => 'support_pm_week',
  ));
  $header = array(
    t('Client'),
  );
  if (function_exists('imagecreatetruecolor')) {
    $header[] = t('Color');
  }
  $header[] = t('Planned');
  $header[] = t('Worked');
  $header[] = t('Difference');
  if (isset($totals['plan']) || isset($totals['actual'])) {
    if (isset($totals['plan']) && isset($totals['actual'])) {
      $all_clients = $totals['plan'] + $totals['actual'];
    }
    else {
      $all_clients = isset($totals['plan']) ? $totals['plan'] : $totals['actual'];
    }
  }
  else {
    $all_clients = array();
  }
  $rows = $clients = array();
  foreach ($all_clients as $clid => $data) {
    $client = support_client_load($clid);
    $clients[$clid] = $client->name;
  }
  asort($clients);
  $rows = array();
  foreach ($clients as $clid => $name) {
    $row = array();
    $client = support_client_load($clid);
    $row[] = l($name, "support/{$client->path}");
    $comments = db_result(db_query('SELECT comment FROM {support_plan} WHERE clid = %d AND uid = %d AND day = %d', $clid, $account->uid, $week));
    if (!empty($comments)) {
      $row[0] .= '<br />' . check_plain($comments);
    }
    if (function_exists('imagecreatetruecolor')) {
      $row[] = "<img src='" . url('support_pm/image/') . support_pm_chartapi_color($clid) . "' alt='swatch' height='15' width='15' />";
    }
    $row[] = isset($totals['plan'][$clid]) ? number_format($totals['plan'][$clid], 2) : "0.00";
    $row[] = isset($totals['actual'][$clid]) ? number_format($totals['actual'][$clid], 2) : "0.00";
    if (isset($totals['plan'][$clid])) {
      if (isset($totals['actual'][$clid])) {
        $diff = $totals['actual'][$clid] - $totals['plan'][$clid];
      }
      else {
        $diff = -$totals['plan'][$clid];
      }
    }
    else {
      if (isset($totals['actual'][$clid])) {
        $diff = $totals['actual'][$clid];
      }
      else {
        $diff = 0.0;
      }
    }
    $plan = isset($totals['plan'][$clid]) ? $totals['plan'][$clid] : 0;
    $row[] = theme('support_pm_plan_diff', $diff, $plan);
    $rows[] = $row;
  }
  $row = array(
    '<strong>' . t('Total') . '</strong>',
  );
  if (function_exists('imagecreatetruecolor')) {
    $row[] = "<img src='" . url('support_pm/image/DDDDDD') . "' alt='swatch' height='15' width='15' />";
  }
  $plan = isset($totals['plan']) ? array_sum($totals['plan']) : 0;
  $actual = isset($totals['actual']) ? array_sum($totals['actual']) : 0;
  $row[] = '<strong>' . number_format($plan, 2) . '</strong>';
  $row[] = '<strong>' . number_format($actual, 2) . '</strong>';
  $diff = $actual - $plan;
  $row[] = '<strong>' . theme('support_pm_plan_diff', $diff, $plan);
  $rows[] = $row;
  $output .= theme('table', $header, $rows, array(
    'id' => 'support_pm_clients',
  ));
  $output .= theme('support_pm_pager', t('next ›'), '>');
  return $output;
}
function support_pm_display_swatch($color) {
  $color = preg_replace('/[^0-9a-fA-F]/', '', $color);
  if (strlen($color)) {
    if (strlen($color) == 3) {
      $color = $color . $color;
    }
    if (strlen($color) > 6) {
      $color = substr($color, 0, 6);
    }
    else {
      if (strlen($color) < 6) {
        $color = 'FFFFFF';
      }
    }
  }
  $image = imagecreatetruecolor(15, 15);
  $swatch = imagecolorallocate($image, hexdec(substr($color, 0, 2)), hexdec(substr($color, 2, 2)), hexdec(substr($color, 4, 2)));
  imagefill($image, 0, 0, $swatch);
  header('Content-type: image/png');
  imagepng($image);
  imagedestroy($image);
}

/**     
 * Page callback.
 */
function support_pm_user_week($form, $user) {
  $form = array();
  $week = isset($_GET['week']) ? (int) $_GET['week'] : time();
  $dates = _support_pm_dates($week);
  drupal_add_js(array(
    'support_pm' => array(
      'unload_warning' => variable_get('support_pm_unload_warning', TRUE),
      'elapsed' => $elapsed,
    ),
  ), 'setting');
  drupal_add_js(drupal_get_path('module', 'support_pm') . '/support_pm.js');
  $clients = _support_available_clients($user);
  $clients['totals'] = '<strong>' . t('Totals') . '</strong>';
  foreach ($clients as $clid => $name) {
    $form['client'][$clid] = array(
      '#value' => $name,
    );
    foreach ($dates as $date => $day) {
      $hours = db_result(db_query("SELECT hours FROM {support_plan} WHERE clid = '%s' AND uid = %d AND day = %d", $clid, $user->uid, $date));
      $form['textfields'][$clid]["{$clid}:{$date}"] = array(
        '#type' => 'textfield',
        '#size' => '2',
        '#default_value' => $hours ? $hours : 0,
        '#disabled' => $clid == 'totals' ? TRUE : FALSE,
      );
    }
    $form['textfields'][$clid]["{$clid}:totals"] = array(
      '#type' => 'textfield',
      '#size' => '2',
      '#disabled' => TRUE,
      '#default_value' => 0,
    );
    if ($clid != 'totals') {
      $comment = db_result(db_query("SELECT comment FROM {support_plan} WHERE clid = '%s' AND uid = %d AND day = %d", $clid, $user->uid, _support_pm_first_day($week)));
      $form['textfields'][$clid]["{$clid}:comment"] = array(
        '#type' => 'textfield',
        '#size' => '20',
        '#disabled' => FALSE,
        '#default_value' => $comment,
      );
    }
  }
  $form['uid'] = array(
    '#type' => 'hidden',
    '#value' => $user->uid,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save plan'),
  );
  return $form;
}

/**
 * Save user's week plan.
 */
function support_pm_user_week_submit($form, &$form_state) {
  $week = isset($_GET['week']) ? _support_pm_first_day((int) $_GET['week']) : _support_pm_first_day(time());
  $dates = _support_pm_dates($week);
  $dates['totals'] = t('Totals');
  $user = user_load(array(
    'uid' => $form_state['values']['uid'],
  ));
  $clients = _support_available_clients($user);
  foreach ($form_state['values'] as $key => $value) {
    $key = explode(':', $key);
    $clid = $key[0];
    $day = $key[1];
    if (!empty($day)) {
      if (isset($clients[$clid]) && $day != 'totals') {
        if ($day == 'comment') {
          db_query("UPDATE {support_plan} SET comment = '%s' WHERE clid = '%s' AND uid = %d AND day = %d", $value, $clid, $user->uid, $week);
        }
        else {
          db_query("UPDATE {support_plan} SET hours = %f WHERE clid = '%s' AND uid = %d AND day = %d", $value, $clid, $user->uid, $day);
        }

        // If we affected 0 rows, we are probably creating a new plan
        if (!db_affected_rows()) {

          // We must create new rows to store the plan.
          if ($day == 'comment') {

            // We may not have changed this row, but it may already exist
            $exists = db_result(db_query("SELECT clid FROM {support_plan} WHERE clid = '%s' AND uid = %d AND day = %d", $clid, $user->uid, $week));
            if (!$exists) {
              db_query("INSERT INTO {support_plan} (comment, clid, uid, day) VALUES ('%s', '%s', %d, %d)", $value, $clid, $user->uid, $week);
            }
          }
          else {

            // We may not have changed this row, but it may already exist
            $exists = db_result(db_query("SELECT clid FROM {support_plan} WHERE clid = '%s' AND uid = %d AND day = %d", $clid, $user->uid, $day));
            if (!$exists) {
              db_query("INSERT INTO {support_plan} (hours, clid, uid, day) VALUES (%f, '%s', %d, %d)", $value, $clid, $user->uid, $day);
            }
          }
        }
      }
    }
  }
  drupal_goto("user/{$user->uid}/support_pm", "week={$week}");
}

/**
 * TODO: Auto-calculate totals whenever a field is updated.
 */
function theme_support_pm_user_week($form) {
  $week = isset($_GET['week']) ? _support_pm_first_day((int) $_GET['week']) : _support_pm_first_day(time());
  drupal_set_title(t('@start - @end', array(
    '@start' => format_date($week, 'medium'),
    '@end' => format_date($week + 86400 * 6, 'medium'),
  )));
  $dates = _support_pm_dates($week);
  $dates['totals'] = t('Totals');
  $dates['comment'] = t('Comments');
  foreach (element_children($form['client']) as $key) {

    // Don't take form control structures
    if (is_array($form['client'][$key])) {
      $row = array();
      $row[] = array(
        'data' => drupal_render($form['client'][$key]),
        'class' => 'support-client',
        'id' => 'client-' . $form['client'][$key]['#value'],
      );
      if (is_array($form['textfields'][$key])) {
        foreach ($dates as $date => $name) {
          $row[] = array(
            'data' => drupal_render($form['textfields'][$key]["{$key}:{$date}"]),
            'class' => 'textfield',
            'title' => $name,
          );
        }
      }
    }
    $rows[] = $row;
  }
  $header[] = t('Client');
  foreach ($dates as $date => $name) {
    if ($name == format_date($date, 'custom', 'l')) {
      $header[] = t('!day<br />!date', array(
        '!day' => format_date($date, 'custom', 'l'),
        '!date' => format_date($date, 'custom', 'M d'),
      ));
    }
    else {
      $header[] = $name;
    }
  }
  $output = theme('support_pm_pager', t('‹ previous'), '<');
  $output .= theme('table', $header, $rows, array(
    'id' => 'permissions',
  ));
  $output .= drupal_render($form);
  $output .= theme('support_pm_pager', t('next ›'), '>');
  return $output;
}

/**
 * Bar graph details.
 */
function theme_support_pm_user_client_hours_details($day, $scale = 12) {
  static $comment;
  $clients = array();
  $chart = array();
  $hours = array();
  $colors = array();
  foreach ($day as $clid => $data) {
    $hours[$clid] = $data->hours;
    $colors[$clid] = support_pm_chartapi_color($clid);
  }
  if (empty($hours)) {
    $hours[] = 0;
    $colors[] = support_pm_chartapi_color(0);
  }

  // TODO: Allow attribute overrides
  $attributes = array(
    'class' => 'chart',
  );
  $output = support_pm_chartapi_render(array(
    'cht' => 'bvs',
    'chd' => 't:' . implode('|', $hours),
    'chco' => implode(',', $colors),
    'chs' => '45x70',
    'chl' => format_plural(array_sum($hours), '1 hr', '@count hrs'),
    'chma' => '7,0,7',
    'chds' => "0,{$scale}",
  ), $attributes);
  return $output;
}

/**
 * Pie chart summary.
 */
function theme_support_pm_user_hours_summary($totals, $load_callback = 'support_client_load', $max = NULL, $message = 'Mismatch') {
  if (!is_array($totals)) {
    return;
  }
  arsort($totals);
  $data = array();
  foreach ($totals as $id => $total) {
    if ($id) {
      $data['totals'][$id] += $total;
      $data['labels'][$id] = format_plural($total, '1 hr', '@count hrs');
      $data['colors'][$id] = support_pm_chartapi_color($id, 'user');
      $details = $load_callback($id);
      $data['legend'][$id] = check_plain($details->name);
    }
  }
  if (is_array($data['totals'])) {
    foreach ($data['totals'] as $id => $total) {
      if (!$total) {
        unset($data['totals'][$id]);
        unset($data['labels'][$id]);
        unset($data['colors'][$id]);
        unset($data['legend'][$id]);
      }
    }
  }
  if (!empty($data)) {
    $total = array_sum($data['totals']);
    if ($max && $total < $max) {
      $data['totals']['filler'] += $max - $total;
      $data['labels']['filler'] = format_plural($max - $total, '1 hr', '@count hrs');
      $data['colors']['filler'] = 'DDDDDD';
      $data['legend']['filler'] = $message;
    }

    // TODO: Allow attribute overrides
    $attributes = array(
      'class' => 'chart',
    );
    $output = support_pm_chartapi_render(array(
      'cht' => 'p',
      'chs' => '450x245',
      'chtt' => format_plural($total, '1 hr', '@count hrs'),
      'chd' => 't:' . implode(',', $data['totals']),
      'chdl' => implode('|', $data['legend']),
      'chco' => implode(',', $data['colors']),
      'chl' => implode('|', $data['labels']),
      'chma' => '25,0,15,15',
    ), $attributes);
  }
  return $output;
}

/**
 * Helper function to show previous and next weeks.
 */
function theme_support_pm_pager($text, $op, $parameters = array(), $attributes = array()) {
  $first_day = isset($_GET['week']) ? _support_pm_first_day((int) $_GET['week']) : _support_pm_first_day(time());
  $week = $first_day;
  switch ($op) {
    case '<':
      $week -= 86400 * 7;
      $prepend = '‹ ';
      break;
    case '>':
      $week += 86400 * 7;
      $append = ' ›';
      break;
    default:
      $append = '';
      $prepend = '';
      break;
  }
  $dates = t('@start - @end', array(
    '@start' => format_date($week, 'medium'),
    '@end' => format_date($week + 86400 * 6, 'medium'),
  ));
  $text = t('@prepend@dates@append', array(
    '@prepend' => $prepend,
    '@dates' => $dates,
    '@append' => $append,
  ));
  $query = array();
  if (!isset($parameters['week'])) {
    $parameters['week'] = $week;
  }
  if (count($parameters)) {
    $query[] = drupal_query_string_encode($parameters, array());
  }
  if (!isset($attributes['title'])) {
    $attributes['title'] = t('View plan for week of @week', array(
      '@week' => $dates,
    ));
  }
  return l($text, $_GET['q'], array(
    'attributes' => $attributes,
    'query' => count($query) ? implode('&', $query) : NULL,
  ));
}
function theme_support_pm_plan_diff($diff, $plan) {
  $diff = number_format($diff, 2);
  $percent = $plan ? number_format($diff / $plan * 100, 2) : 0;
  if ($diff < 0) {
    return "<font color='red'>{$diff} ({$percent}%)</font>";
  }
  else {
    if ($diff > 0) {
      if ($plan) {
        return "<font color='green'>{$diff} (+{$percent}%)</font>";
      }
      else {
        return "<font color='brown'>{$diff}</font>";
      }
    }
  }
  return $diff;
}

/**
 * TODO: Proper timezone support
 */
function _support_pm_first_day($time = 0) {

  // Use Drupal core's configurable first day of the week.
  $date_first_day = variable_get('date_first_day', 0);
  if (!$time) {
    $time = time();
  }

  // Determine what day of the week $time is
  $day = date('w', $time);

  // Now calculate a timestamp for the first day of this week,
  // respecting the configured first day of the week.
  if ($day >= $date_first_day) {
    $days = $day - $date_first_day;
  }
  else {
    $days = $day + 7 - $date_first_day;
  }
  return strtotime(date('M d, Y 12:00', $time - 86400 * $days));
}

/**
 * Return array of days, $timestamp => $day where $timestamp is the first
 * second of the named $day.
 */
function _support_pm_dates($start = 0) {

  // TODO: Make configurable (ie, disable weekends)
  $day = array(
    t('Sunday'),
    t('Monday'),
    t('Tuesday'),
    t('Wednesday'),
    t('Thursday'),
    t('Friday'),
    t('Saturday'),
  );
  $date = _support_pm_first_day($start);
  $day_of_week = variable_get('date_first_day', 0);

  // Build an array of $timestamp => $day pairs
  for ($i = 0; $i < 7; $i++) {
    $dates[$date] = $day[$day_of_week];
    $date += 86400;
    if (++$day_of_week > 6) {
      $day_of_week = 0;
    }
  }
  return $dates;
}
function support_pm_day_load($date = NULL, $user = NULL, $client = NULL) {
  if (is_null($date)) {

    // default to this week
    $date = _support_pm_first_day();
  }
  $query = 'SELECT pid, clid, uid, day, hours, comment FROM {support_plan} WHERE day = %d AND hours > 0';
  $args = array(
    $date,
  );
  if (is_object($user) && isset($user->uid)) {
    $query .= ' AND uid = %d';
    $args[] = $user->uid;
  }
  if (is_object($client) && isset($client->clid)) {
    $query .= ' AND clid = %d';
    $args[] = $client->clid;
  }
  $day = array();
  $result = db_query($query, $args);
  while ($row = db_fetch_object($result)) {
    $day[$row->uid][$row->clid] = $row;
  }
  return $day;
}

/**
 * Google ChartAPI calls.
 *  TODO: Move into a helper module? (chartapi module seems abandoned)
 **/

/**
 * In order to optimize displaying multiple charts on one page,
 * we generate unique urls for the charts.
 * TODO: Why does this break?
 */
function support_pm_chartapi_uri() {
  return 'http://chart.apis.google.com/chart';
  static $i = 0;
  $uri = "http://{$i}.chart.apis.google.com/chart";
  if (++$i > 9) {
    $i = 0;
  }
  return $uri;
}
function support_pm_chartapi_color($id, $type = 'client') {
  static $color = 0;
  static $values = NULL;
  $update = FALSE;
  if (!isset($values)) {
    $values = variable_get('support_pm_color_values', array());
    $color = variable_get('support_pm_color', 0);
  }
  $colors = array(
    '66FF99',
    '6699CC',
    'FFCCFF',
    'FFFF99',
    'FFFF00',
    '6633CC',
    '666600',
    'FFCC00',
    '666666',
    '66FF00',
    '66CC66',
    '66FFFF',
    '669933',
    'FF6600',
    '6666FF',
    'FF3300',
    '66CCFF',
    '663333',
    'FF0000',
  );
  if (!isset($values[$type][$id])) {
    $values[$type][$id] = $colors[$color++];
    $update = TRUE;
  }
  if ($color > count($colors)) {
    $color = 0;
  }
  if ($update) {
    variable_set('support_pm_color_values', $values);
    variable_set('support_pm_color', $color);
  }
  return $values[$type][$id];
}
function support_pm_chartapi_render($chart, $attributes = array()) {
  return '<img src="' . support_pm_chartapi_uri() . '?' . drupal_query_string_encode($chart) . '"' . drupal_attributes($attributes) . " />\n";
}

/**
 * Convert from timer time format to decimal.
 */
function support_pm_timer_to_hours($time) {
  $time = explode(':', $time);
  if (!isset($time[1])) {
    $time[1] = 0;
  }
  $hours = (int) $time[0];
  $minutes = round((int) $time[1] / 60, 2);
  return $hours + $minutes;
}

/**
 * Load project from database.
 */
function support_pm_project_load($projid) {
  static $projects = array();
  if (!isset($projects[$projid])) {
    $projects[$projid] = db_fetch_object(db_query('SELECT * FROM {support_project} WHERE projid = %d', $projid));
    $projects[$projid]->clids = array();
    $result = db_query('SELECT spc.clid, sc.parent FROM {support_project_client} spc LEFT JOIN {support_client} sc ON spc.clid = sc.clid WHERE spc.projid = %d', $projid);
    while ($client = db_fetch_object($result)) {
      $projects[$projid]->clids[] = $client->clid;
      if ($client->parent) {
        $projects[$projid]->parent[$client->clid] = $client->parent;
      }
    }
    drupal_alter('support_pm_project_load', $projects[$projid]);
  }
  return $projects[$projid];
}
function support_pm_project_load_nid($nid) {
  static $projects = array();
  if (!isset($projects[$nid])) {
    $projects[$nid] = db_fetch_object(db_query('SELECT * FROM {support_project_ticket} spt LEFT JOIN {support_project} sp ON spt.projid = sp.projid WHERE spt.nid = %d', $nid));
    drupal_alter('support_pm_project_load_nid', $projects[$nid]);
  }
  return $projects[$nid];
}

/**
 * Load projects assigned to a given client.
 */
function support_pm_load_projects($clid, $recursive = FALSE) {
  $projects = array();
  $result = db_query('SELECT sp.projid, sp.project FROM {support_project} sp LEFT JOIN {support_project_client} spc ON sp.projid = spc.projid WHERE (spc.clid = %d OR ISNULL(spc.clid)) AND disabled = 0', $clid);
  while ($project = db_fetch_object($result)) {
    $projects[$project->projid] = $project->project;
  }
  if ($recursive) {
    $result = db_query("SELECT clid, name FROM {support_client} WHERE parent = %d", $clid);
    while ($subclient = db_fetch_object($result)) {
      $result2 = db_query('SELECT sp.projid, sp.project FROM {support_project} sp LEFT JOIN {support_project_client} spc ON sp.projid = spc.projid WHERE (spc.clid = %d OR ISNULL(spc.clid)) AND disabled = 0', $subclient->clid);
      while ($subproject = db_fetch_object($result2)) {
        $projects["{$subproject->projid}:{$subclient->clid}"] = " -> {$subclient->name}: {$subproject->project}";
      }
    }
  }
  return $projects;
}

/**
 * Determine default for a list of projects.
 */
function support_pm_default_project($projects) {
  $projids = array();
  foreach ($projects as $projid => $project) {
    $projids[] = $projid;
  }
  if (empty($projids)) {
    return 0;
  }
  else {
    return db_result(db_query_range('SELECT projid FROM {support_project} WHERE projid IN (%s) AND disabled = 0 ORDER BY weight ASC', implode(',', $projids), 0, 1));
  }
}

/**
 * Implementation of hook_apachesolr_update_index().
 */
function support_pm_apachesolr_update_index(&$document, $node) {
  if (!isset($node->project->projid)) {
    $document->is_support_project = 0;
  }
  else {
    $document->is_support_project = (int) $node->project->projid;
  }
}

/**
 * Implementation of hook_support_solr_info().
 */
function support_pm_support_solr_info() {
  return array(
    'support_pm_project' => array(
      'facet' => array(
        'info' => t('Support: Filter by project'),
        'facet_field' => 'is_support_project',
      ),
      'filter_by' => t('Filter by Support Project'),
      'facet_callback' => '_support_pm_project_name',
      'block' => array(
        'info' => t('Support: Project'),
        'cache' => BLOCK_CACHE_PER_PAGE,
      ),
    ),
  );
}
function _support_pm_project_name($projid) {
  if (!$projid) {
    return t('None');
  }
  return check_plain(db_result(db_query('SELECT project FROM {support_project} WHERE projid = %d', $projid)));
}

Functions

Namesort descending Description
support_pm_apachesolr_update_index Implementation of hook_apachesolr_update_index().
support_pm_chartapi_color
support_pm_chartapi_render
support_pm_chartapi_uri In order to optimize displaying multiple charts on one page, we generate unique urls for the charts. TODO: Why does this break?
support_pm_comment
support_pm_day_load
support_pm_db_rewrite_sql
support_pm_default_project Determine default for a list of projects.
support_pm_display_swatch
support_pm_form_alter
support_pm_invoice_ui_form Provide form for selecting projects.
support_pm_invoice_ui_form_submit Add url filter when projects are selected.
support_pm_load_projects Load projects assigned to a given client.
support_pm_menu Implementation of hook_menu(). TODO: Include date in 'view' and 'edit' tabs
support_pm_nodeapi Implementation of hook_nodeapi().
support_pm_perm Implementation of hook_perm();
support_pm_plan_overview_weekly
support_pm_project_load Load project from database.
support_pm_project_load_nid
support_pm_support_solr_info Implementation of hook_support_solr_info().
support_pm_support_timer_client_report_alter Allow projects to be selected.
support_pm_theme Implementation of hook_theme().
support_pm_timer_to_hours Convert from timer time format to decimal.
support_pm_user_week Page callback.
support_pm_user_week_submit Save user's week plan.
theme_support_pm_pager Helper function to show previous and next weeks.
theme_support_pm_plan_diff
theme_support_pm_user_client_hours_details Bar graph details.
theme_support_pm_user_hours_summary Pie chart summary.
theme_support_pm_user_week TODO: Auto-calculate totals whenever a field is updated.
_support_pm_dates Return array of days, $timestamp => $day where $timestamp is the first second of the named $day.
_support_pm_first_day TODO: Proper timezone support
_support_pm_project_name