webgui/lib/WebGUI/Asset/Wobject/ProjectManager.pm
Drake 6516062d4d Overall refactoring of projectDisplay.js from the Project Management asset.
Hopefully this will make it much easier to deal with later, and hopefully this
will help to flush out the remaining duration-calculation-related bugs by
avoiding repetition of the calculations.  Various minor fixes in the process.

I'm not convinced this is completely bug-free yet, of course, but it does
seem at least to be slightly less broken than before, so.
2006-10-05 23:07:13 +00:00

1684 lines
62 KiB
Perl

package WebGUI::Asset::Wobject::ProjectManager;
$VERSION = "1.0.0";
#-------------------------------------------------------------------
# WebGUI is Copyright 2001-2006 Plain Black Corporation.
#-------------------------------------------------------------------
# Please read the legal notices (docs/legal.txt) and the license
# (docs/license.txt) that came with this distribution before using
# this software.
#-------------------------------------------------------------------
# http://www.plainblack.com info@plainblack.com
#-------------------------------------------------------------------
use strict;
use DateTime;
use Tie::IxHash;
use WebGUI::International;
use WebGUI::Utility;
use WebGUI::HTML;
use POSIX qw(ceil floor);
use base 'WebGUI::Asset::Wobject';
#-------------------------------------------------------------------
sub _addDaysForMonth {
my $self = shift;
my $dt = $self->session->datetime;
my $eh = $self->session->errorHandler;
my $days_loop = $_[0];
my $hash = $_[1];
my $month = $_[2];
my ($monthStart,$monthEnd) = $dt->monthStartEnd($month);
my $dayOfWeek = $dt->getDayOfWeek($monthStart);
my $mondayAdjust = (7 - ($dayOfWeek-1)) % 7;
my $firstMonday = $dt->addToDateTime($monthStart,0,0,$mondayAdjust,1);
#This line of code just makes things easier to read
my $colCount = 0;
my $monday = $firstMonday;
while ($monday < $monthEnd) {
my $hash = {};
$hash->{'day.number'} = $dt->epochToHuman($monday,"%d");
#$eh->warn($hash->{'day.number'});
$monday += 604800; # Add one week to the first monday of the month
push(@{$days_loop},$hash);
$colCount++;
}
$hash->{'month.colspan'} = $colCount;
#$eh->warn($dt->epochToHuman($firstMonday));
}
#-------------------------------------------------------------------
sub _getDurationUnitHash {
my $self = shift;
my ($session,$privilege,$form,$db,$dt,$i18n,$user) = $self->setSessionVars;
tie my %hash, "Tie::IxHash";
%hash = ( "hours"=>$i18n->get("hours label"), "days"=>$i18n->get("days label") );
return \%hash;
}
#-------------------------------------------------------------------
sub _getDurationUnitHashAbbrev {
my $self = shift;
my ($session,$privilege,$form,$db,$dt,$i18n,$user) = $self->setSessionVars;
tie my %hash, "Tie::IxHash";
%hash = ( "hours"=>$i18n->get("hours label abbrev"), "days"=>$i18n->get("days label abbrev") );
return \%hash;
}
#-------------------------------------------------------------------
sub definition {
my $class = shift;
my $session = shift;
my $definition = shift;
my $i18n = WebGUI::International->new($session,'Asset_ProjectManager');
my $db = $session->db;
my %properties;
tie %properties, 'Tie::IxHash';
%properties = (
projectDashboardTemplateId =>{
fieldType=>"template",
defaultValue=>'ProjectManagerTMPL0001',
tab=>"display",
namespace=>"ProjectManager_dashboard",
hoverHelp=>$i18n->get('projectDashboardTemplate hoverhelp'),
label=>$i18n->get('projectDashboardTemplate label')
},
projectDisplayTemplateId => {
fieldType=>"template",
defaultValue=>'ProjectManagerTMPL0002',
tab=>"display",
namespace=>"ProjectManager_project",
hoverHelp=>$i18n->get('projectDisplayTemplate hoverhelp'),
label=>$i18n->get('projectDisplayTemplate label')
},
ganttChartTemplateId => {
fieldType=>"template",
defaultValue=>'ProjectManagerTMPL0003',
tab=>"display",
namespace=>"ProjectManager_gantt",
hoverHelp=>$i18n->get('ganttChartTemplate hoverhelp'),
label=>$i18n->get('ganttChartTemplate label')
},
editTaskTemplateId =>{
fieldType=>"template",
defaultValue=>'ProjectManagerTMPL0004',
tab=>"display",
namespace=>"ProjectManager_editTask",
hoverHelp=>$i18n->get('editTaskTemplate hoverhelp'),
label=>$i18n->get('editTaskTemplate label')
},
resourcePopupTemplateId =>{
fieldType=>"template",
defaultValue=>'ProjectManagerTMPL0005',
tab=>"display",
namespace=>"ProjectManager_resourcePopup",
hoverHelp=>$i18n->get('resourcePopupTemplate hoverhelp'),
label=>$i18n->get('resourcePopupTemplate label')
},
resourceListTemplateId =>{
fieldType=>"template",
defaultValue=>'ProjectManagerTMPL0006',
tab=>"display",
namespace=>"ProjectManager_resourceList",
hoverHelp=>$i18n->get('resourceListTemplate hoverhelp'),
label=>$i18n->get('resourceListTemplate label')
},
groupToAdd => {
fieldType=>"group",
defaultValue=>3,
tab=>"security",
hoverHelp=>$i18n->get('groupToAdd hoverhelp'),
label=>$i18n->get('groupToAdd label')
}
);
push(@{$definition}, {
assetName=>$i18n->get('assetName'),
icon=>'projManagement.gif',
autoGenerateForms=>1,
tableName=>'PM_wobject',
className=>'WebGUI::Asset::Wobject::ProjectManager',
properties=>\%properties
});
return $class->SUPER::definition($session, $definition);
}
#-------------------------------------------------------------------
#API method called by Time Tracker to return the instance of the PM wobject which this project blongs
sub getProjectInstance {
my $class = shift;
my $session = shift;
my $db = $session->db;
my $projectId = $_[0];
return undef unless $projectId;
my ($assetId) = $db->quickArray("select assetId from PM_project where projectId=?",[$projectId]);
if($assetId) {
return WebGUI::Asset->newByDynamicClass($session,$assetId);
}
return undef;
}
#-------------------------------------------------------------------
#API method called by Time Tracker to return all projects in all assets for which the user passed in has tasks assigned
sub getProjectList {
my $self = shift;
my $db = $self->session->db;
my $userId = $_[0];
my @groupIds = @{WebGUI::User->new($self->session, $userId)->getGroups};
my $groupIdQuery = @groupIds?
('PM_taskResource.resourceId IN ('.join(',', ('?') x @groupIds).')') : '0';
$self->session->db->buildHashRef(<<"SQL", [$userId, @groupIds]);
SELECT DISTINCT PM_project.projectId, PM_project.name
FROM PM_project
INNER JOIN PM_task ON PM_project.projectId = PM_task.projectId
INNER JOIN PM_taskResource ON PM_task.taskId = PM_taskResource.taskId
WHERE (PM_taskResource.resourceKind = 'user' AND PM_taskResource.resourceId = ?)
OR (PM_taskResource.resourceKind = 'group' AND $groupIdQuery)
SQL
}
#-------------------------------------------------------------------
#API method called by Time Tracker to return all tasks for the projectId passed in
sub getTaskList {
my $self = shift;
my $db = $self->session->db;
my $projectId = $_[0];
my $userId = $_[1];
my @groupIds = @{WebGUI::User->new($self->session, $userId)->getGroups};
my $groupIdQuery = @groupIds?
('PM_taskResource.resourceId IN ('.join(',', ('?') x @groupIds).')') : '0';
$self->session->db->buildHashRef(<<"SQL", [$projectId, $userId, @groupIds]);
SELECT DISTINCT PM_task.taskId, PM_task.taskName
FROM PM_task
INNER JOIN PM_taskResource ON PM_task.taskId = PM_taskResource.taskId
WHERE PM_task.projectId = ?
AND ((PM_taskResource.resourceKind = 'user' AND PM_taskResource.resourceId = ?)
OR (PM_taskResource.resourceKind = 'group' AND $groupIdQuery))
SQL
}
#-------------------------------------------------------------------
# API method called by Time Tracker to set percent complete field in the task and update the project cache
sub updateProjectTask {
my $self = shift;
my $db = $self->session->db;
my $eh = $self->session->errorHandler;
my $taskId = $_[0];
my $projectId = $_[1];
my $deltaHours = $_[2];
return 0 unless ($taskId && $projectId && $deltaHours);
my $task = $self->getCollateral('PM_task', 'taskId', $taskId);
my ($units,$hoursPerDay) = $db->quickArray("select durationUnits, hoursPerDay from PM_project where projectId=?",[$projectId]);
return 0 unless ($task->{taskId});
if ($task->{taskType} eq 'milestone') {
return 0;
} elsif ($task->{taskType} eq 'progressive') {
my $deltaDuration = ($units eq 'days')? ($deltaHours / $hoursPerDay) : $deltaHours;
$task->{duration} += $deltaDuration;
$task->{endDate} += $deltaDuration * 3600;
$task->{duration} = 0 if $task->{duration} < 0;
$task->{endDate} = $task->{startDate} if $task->{endDate} < $task->{startDate};
# Don't need to consider dependants here because nothing is allowed
# to depend on a progressive task.
} else {
my $durationHours = ($units eq 'days')? ($task->{duration} * $hoursPerDay) : $task->{duration};
$task->{percentComplete} += ($deltaHours / $durationHours) * 100;
$task->{percentComplete} = 0 if $task->{percentComplete} < 0;
$task->{percentComplete} = 100 if $task->{percentComplete} > 100;
}
$self->setCollateral("PM_task","taskId", { taskId=>$taskId, duration=>$task->{duration}, percentComplete=>$task->{percentComplete} });
$self->_updateProject($projectId);
return 1;
}
#-------------------------------------------------------------------
sub _userCanManageProject {
my $self = shift;
my $user = shift;
my $projectId = shift;
my ($managerGroup) = $self->session->db->quickArray("select projectManager from PM_project where projectId = ?", [$projectId]);
return $self->canView($user->userId) && ($user->isInGroup($managerGroup) || $user->isInGroup($self->get('groupToAdd')));
}
sub _userCanObserveProject {
my $self = shift;
my $user = shift;
my $projectId = shift;
my ($managerGroup, $observerGroup) = $self->session->db->quickArray("select projectManager, projectObserver from PM_project where projectId = ?", [$projectId]);
return $self->canView($user->userId) && ($user->isInGroup($managerGroup) || $user->isInGroup($observerGroup) || $user->isInGroup($self->get('groupToAdd')));
}
sub _userCanManageProjectList {
my $self = shift;
my $user = shift;
return $self->canView($user->userId) && $user->isInGroup($self->get('groupToAdd'));
}
#-------------------------------------------------------------------
sub _updateProject {
my $self = shift;
my ($session,$privilege,$form,$db,$dt,$i18n,$user) = $self->setSessionVars;
my $projectId= $_[0];
my ($minStart, $maxEnd) = $db->quickArray("select min(startDate), max(endDate) from PM_task where projectId=?", [$projectId]);
my ($projectTotal, $complete) = 0;
my $tasks = $db->buildArrayRefOfHashRefs("select * from PM_task where projectId=? and taskType <> 'timed' order by sequenceNumber asc", [$projectId]);
foreach my $task (@{$tasks}) {
$projectTotal += $task->{duration};
$complete += ($task->{duration} * ($task->{percentComplete}/100));
}
my $projectComplete = ($projectTotal == 0)? 0 : (($complete / $projectTotal) * 100);
$db->write("update PM_project set startDate=?, endDate=?, percentComplete=? where projectId=?",[$minStart,$maxEnd,$projectComplete,$projectId]);
}
#-------------------------------------------------------------------
sub prepareView {
my $self = shift;
$self->SUPER::prepareView();
my $template = WebGUI::Asset::Template->new($self->session, $self->get("projectDashboardTemplateId"));
$template->prepare;
$self->{_viewTemplate} = $template;
}
#-------------------------------------------------------------------
sub processErrors {
my $self = shift;
my $errors = "";
if($_[0]) {
$errors = "<ul>";
foreach (@{$_[0]}) {
$errors .= "<li>$_</li>";
}
$errors .= "</ul>";
}
return $errors;
}
#-------------------------------------------------------------------
sub purge {
my $self = shift;
#purge your wobject-specific data here. This does not include fields
# you create for your NewWobject asset/wobject table.
return $self->SUPER::purge;
}
#-------------------------------------------------------------------
sub setSessionVars {
my $self = shift;
my $session = $self->session;
my $i18n = WebGUI::International->new($session,'Asset_ProjectManager');
return ($session,$session->privilege,$session->form,$session->db,$session->datetime,$i18n,$session->user);
}
#-------------------------------------------------------------------
sub view {
my $self = shift;
my $var = $self->get;
my ($session,$privilege,$form,$db,$datetime,$i18n,$user) = $self->setSessionVars;
my $config = $session->config;
my $eh = $session->errorHandler;
$var->{'extras'} = $config->get("extrasURL")."/wobject/ProjectManager";
$var->{'project.create'} = $self->getUrl("func=editProject;projectId=new");
$var->{'project.create.label'} = $i18n->get("project new label");
#Project Table Headers
$var->{'project.name.label'} = $i18n->get("project name label");
$var->{'project.startDate.label'} = $i18n->get("project start date label");
$var->{'project.endDate.label'} = $i18n->get("project end date label");
$var->{'project.cost.label'} = $i18n->get("project cost label");
$var->{'project.complete.label'} = $i18n->get("project complete label");
$var->{'project.actions.label'} = $i18n->get("project action label");
$var->{'empty.colspan'} = 5;
if($self->_userCanManageProjectList($user)) {
$var->{'canEditProjects'} = "true";
$var->{'empty.colspan'} = 6;
}
#Project Data
my @projects = ();
my $sth = $db->read("select * from PM_project where assetId=".$db->quote($self->get("assetId")));
while (my $project = $sth->hashRef) {
my $hash = {};
my $projectId = $project->{projectId};
# Drop projects that the current user can't view.
next unless ($self->_userCanObserveProject($user, $projectId));
$hash->{'project.view.url'} = $self->getUrl("func=viewProject;projectId=".$projectId);
$hash->{'project.name.data'} = $project->{name};
$hash->{'project.description.data'} = $project->{description};
$hash->{'project.startDate.data'} = $project->{startDate}?$datetime->epochToSet($project->{startDate}):"N/A";
$hash->{'project.endDate.data'} = $project->{endDate}?$datetime->epochToSet($project->{endDate}):"N/A";
$hash->{'project.cost.data.int'} = WebGUI::Utility::commify(int($project->{targetBudget}));
$hash->{'project.cost.data.float'} = WebGUI::Utility::commify($project->{targetBudget});
$hash->{'project.complete.data.int'} = int($project->{percentComplete});
$hash->{'project.complete.data.int'} = 100 if($hash->{'project.complete.data.int'} > 100);
$hash->{'project.complete.data.float'} = sprintf("%2.2f",$project->{percentComplete});
if($var->{'canEditProjects'}) {
$hash->{'project.edit.url'} = $self->getUrl("func=editProject;projectId=".$projectId);
$hash->{'project.edit.title'} = $i18n->get("project edit title");
$hash->{'project.delete.url'} = $self->getUrl("func=deleteProject;projectId=".$projectId);
$hash->{'project.delete.title'} = $i18n->get("project delete title");
}
push(@projects, $hash);
}
$sth->finish;
my $warning = $i18n->get("project delete warning");
$warning =~ s/'/\\'/g;
$var->{'project.delete.warning'} = $warning;
$var->{'noProjects'} = $i18n->get("no projects") if(scalar(@projects) == 0);
$var->{'project.loop'} = \@projects;
return $self->processTemplate($var, undef, $self->{_viewTemplate});
}
#-------------------------------------------------------------------
sub www_deleteProject {
my $self = shift;
#Set Method Helpers
my ($session,$privilege,$form,$db,$datetime,$i18n,$user) = $self->setSessionVars;
#Check Privileges
return $privilege->insufficient unless $self->_userCanManageProjectList($user);
my $projectId = $form->get("projectId");
#Delete Project
$db->write("delete from PM_project where projectId=?",[$projectId]);
#Delete Associated Tasks
$db->write("delete from PM_task where projectId=?",[$projectId]);
return "";
}
#-------------------------------------------------------------------
sub www_deleteTask {
my $self = shift;
#Set Method Helpers
my ($session,$privilege,$form,$db,$datetime,$i18n,$user) = $self->setSessionVars;
#Set Local Vars
my $taskId = $form->get("taskId");
my $task = $self->getCollateral('PM_task', 'taskId', $taskId);
my $projectId = $task->{projectId};
my $taskRank = $task->{sequenceNumber};
#Check Privileges
return $privilege->insufficient unless $self->_userCanManageProject($user, $projectId);
#Remove dependencies to this task
$db->write("update PM_task set dependants=NULL where projectId=? and dependants=?",[$projectId,$taskId]);
#Remove task
$self->deleteCollateral("PM_task","taskId",$taskId);
#Reorder dependants and tasks
my $tasks = $db->buildArrayRefOfHashRefs("select * from PM_task where projectId=? order by sequenceNumber",[$projectId]);
my $taskLen = scalar(@{$tasks});
#$eh->warn("Task Length = $taskLen");
foreach my $tsk (@{$tasks}) {
my $seqNum = $tsk->{sequenceNumber};
next unless ($seqNum >= $taskRank);
#$eh->warn("Fixing task $seqNum");
my $dependant = $tsk->{dependants};
#$eh->warn("Dependant is $dependant");
#Only decrement the dependant if it's greater than the rank point of the deleted task
if($dependant >= $taskRank){
$dependant--;
}
#$eh->warn("New dependant is $dependant");
$db->write("update PM_task set dependants=? where taskId=?",[$dependant,$tsk->{taskId}]);
}
$self->reorderCollateral("PM_task","taskId","projectId",$projectId);
return $self->www_viewProject($projectId);
}
#-------------------------------------------------------------------
sub www_editProject {
my $self = shift;
#Set Method Helpers
my ($session,$privilege,$form,$db,$datetime,$i18n,$user) = $self->setSessionVars;
#Check Privileges
return $privilege->insufficient unless $self->_userCanManageProjectList($user);
#Set Local Vars
my $projectId = $form->get("projectId");
my $project = $db->quickHashRef("select * from PM_project where projectId=?",[$projectId]);
my $addEditText = ($projectId eq "new")?$i18n->get("create project"):$i18n->get("edit project");
#Build Form
my $f = WebGUI::HTMLForm->new($self->session,-action=>$self->getUrl, -extras=>q|onsubmit="return checkform(this);"|);
$f->hidden(
-name=>"func",
-value=>"editProjectSave"
);
$f->hidden(
-name=>"projectId",
-value=>$projectId
);
$f->readOnly(
-label=>$i18n->get("project id"),
-hoverHelp => $i18n->get('project name hoverhelp'),
-value=>$projectId
);
$f->text(
-name => "name",
-value => $form->get("name") || $project->{name},
-hoverHelp => $i18n->get('project name hoverhelp'),
-label => $i18n->get('project name label')
);
$f->HTMLArea(
-name => "description",
-value => $form->get("description") || $project->{description},
-hoverHelp => $i18n->get('project description hoverhelp'),
-label => $i18n->get('project description label')
);
$f->group(
-name=> "projectManager",
-value=> $form->get("projectManager") || $project->{projectManager} || $self->get("groupToAdd"),
-hoverHelp=> $i18n->get('project manager hoverhelp'),
-label => $i18n->get('project manager label')
);
$f->group(
-name=> "projectObserver",
-value=> $form->get("projectObserver") || $project->{projectObserver} || '7',
-hoverHelp=> $i18n->get('project observer hoverhelp'),
-label => $i18n->get('project observer label')
);
my $dunitValue = $form->get("durationUnits") || $project->{durationUnits} || "hours";
$f->selectBox(
-name=>"durationUnits",
-value=> $dunitValue,
-options=>$self->_getDurationUnitHash,
-hoverHelp => $i18n->get('duration units hoverhelp'),
-label => $i18n->get('duration units label'),
-extras=> q|onchange="if(this.value == 'hours'){ document.getElementById('hoursper').style.display='' } else { document.getElementById('hoursper').style.display='none' }"|
);
my $hpdLabel = $i18n->get('hours per day label');
my $hpdHoverHelp = $i18n->get('hours per day hoverhelp');
$hpdHoverHelp =~ s/'/\\'/g;
my $hpdValue = $form->get("hoursPerDay") || $project->{hoursPerDay} || "8.0";
my $hpdStyle = ($dunitValue eq "days"?"display:none":"");
my $html = qq|
<tr id="hoursper" style="$hpdStyle">
<td class="formDescription" onmouseover="return escape('$hpdHoverHelp')" valign="top" style="width: 180px;">
<label for="hoursPerDay_formId">$hpdLabel</label>
</td>
<td valign="top" class="tableData" style="width: *;">
<input id="hoursPerDay_formId" type="text" name="hoursPerDay" value="$hpdValue" size="11" maxlength="14" />
</td>
</tr>|;
$f->raw($html);
$f->float (
-name=>"targetBudget",
-value=> $form->get("targetBudget") || $project->{targetBudget} || "0.00",
-hoverHelp => $i18n->get('target budget hoverhelp'),
-label=> $i18n->get('target budget label')
);
$f->submit(
-extras=>"name='subbutton'",
-value=>$addEditText
);
my $jscript = qq|
<script language="JavaScript">
function checkform(form) {
if(form.name.value == ""){
alert("You must enter a project name");
form.subbutton.value='$addEditText';
return false;
}
return true;
}
</script>
|;
my $errors = $self->processErrors($_[0]);
my $output = $jscript."\n".$errors.$f->print;
return $self->getAdminConsole->render($output,$addEditText);
}
#-------------------------------------------------------------------
sub www_editProjectSave {
my $self = shift;
#Set Method Helpers
my ($session,$privilege,$form,$db,$dt,$i18n,$user) = $self->setSessionVars;
my $eh = $session->errorHandler;
#Check Privileges
return $privilege->insufficient unless $self->_userCanManageProjectList($user);
my $now = $dt->time();
my $uid = $user->userId;
my $projectId = $form->process("projectId","hidden");
my $origProjId = $projectId;
#Set Properties
my $props = {};
$props->{projectId} = $projectId;
$props->{name} = $form->process("name","text");
$props->{description} = $form->process("description","HTMLArea");
$props->{projectManager} = $form->process("projectManager","group");
$props->{projectObserver} = $form->process("projectObserver","group");
$props->{durationUnits} = $form->process("durationUnits","selectBox");
$props->{hoursPerDay} = $form->process("hoursPerDay","float") || 8.0;
$props->{targetBudget} = $form->process("targetBudget","float");
if($projectId eq "new") {
$props->{creationDate} = $now;
$props->{createdBy} = $uid;
}
$props->{lastUpdateDate} = $now;
$props->{lastUpdatedBy} = $uid;
#Process Errors
my @errors = ();
push(@errors,"You must enter a project name") unless ($props->{name});
if(scalar(@errors) > 0) {
return $self->www_editProject(\@errors);
}
#Save the extended project data
my $projectId = $self->setCollateral("PM_project","projectId",$props,0,1);
if($origProjId eq "new") {
#Create Project Start Milestone Task for new projects
$props = {};
$props->{taskId} = "new";
$props->{projectId} = $projectId;
$props->{taskName} = $i18n->get("project start task label");
$props->{duration} = 0;
$props->{lagTime} = 0;
$props->{startDate} = $dt->time();
$props->{endDate} = $dt->time();
$props->{taskType} = 'milestone';
$props->{creationDate} = $now;
$props->{createdBy} = $uid;
$props->{lastUpdateDate} = $now;
$props->{lastUpdatedBy} = $uid;
#Save the extended task data
my $taskId = $self->setCollateral("PM_task","taskId",$props,1,0,"projectId",$projectId);
}
return $self->www_viewProject($projectId);
}
#-------------------------------------------------------------------
sub _htmlOfResourceList {
my $self = shift;
my %args = %{+shift};
my @resources = @_;
my @listItems;
my $assetExtras = $self->session->url->extras('wobject/ProjectManager');
my $var = {};
$var->{assetExtras} = $assetExtras;
$var->{resourceLoop} = [];
my $lastOdd = 0;
foreach my $row (@resources) {
my $subvar = {};
my ($resourceKind, $resourceId) = @$row{qw{resourceKind resourceId}};
my $odd = ($lastOdd = !$lastOdd);
$subvar->{resourceKind} = $resourceKind;
$subvar->{resourceId} = $resourceId;
$subvar->{opCallbackJs} = $args{opCallbackJs};
$subvar->{opIcon} = $args{opIcon};
$subvar->{opTitle} = $args{opTitle};
$subvar->{assetExtras} = $assetExtras;
$subvar->{odd} = $odd;
$subvar->{hiddenFields} = $args{hiddenFields};
if ($resourceKind eq 'group') {
my $group = WebGUI::Group->new($self->session, $resourceId);
$subvar->{resourceName} = WebGUI::HTML::format($group->name, 'text');
$subvar->{resourceIcon} = 'groups.gif';
} elsif ($resourceKind eq 'user') {
my $user = WebGUI::User->new($self->session, $resourceId);
my ($firstName, $lastName, $username) = ($user->profileField('firstName'), $user->profileField('lastName'), $user->username);
my $displayName = do {
if (length($firstName) && length($lastName)) { "$lastName, $firstName" }
elsif (length($firstName)) { $firstName }
elsif (length($lastName)) { $lastName }
else { $username }
};
$subvar->{resourceName} = WebGUI::HTML::format($displayName, 'text');
$subvar->{resourceIcon} = 'users.gif';
} else {
$self->session->errorHandler->fatal("Unknown kind of resource '$resourceKind'!");
}
push @{$var->{resourceLoop}}, $subvar;
}
return $self->processTemplate($var, $self->getValue('resourceListTemplateId'));
}
sub _resourceSearchPopup {
my $self = shift;
my %args = @_;
my $i18n = WebGUI::International->new($self->session,'Asset_ProjectManager');
my $doSearch = $self->session->form->param('doSearch');
my $jsCallback = $self->session->form->param('callback');
$jsCallback =~ tr/A-Za-z0-9_//cd;
my $selfUrlHtml = WebGUI::HTML::format($self->getUrl, 'text');
my $assetExtras = $self->session->url->extras('wobject/ProjectManager');
my ($search, $exclude) = map {scalar $self->session->form->param($_)} ('search', 'exclude');
my ($searchHtml, $excludeHtml) = map {WebGUI::HTML::format($_, 'text')} ($search, $exclude);
my $var = {};
my $i18nprefix = $args{i18nprefix};
foreach my $key (qw/title searchText foundMessage notFoundMessage/) {
$var->{$key} = $i18n->get("$i18nprefix $key");
}
$var->{assetExtras} = $assetExtras;
$var->{func} = $args{func};
$var->{callback} = $jsCallback;
$var->{exclude} = $excludeHtml;
$var->{previousSearch} = $searchHtml;
$var->{selfUrl} = $selfUrlHtml;
if ($doSearch) {
my @resources = @{$self->session->db->buildArrayRefOfHashRefs($args{queryCallback}->($exclude, $search))};
$var->{doingSearch} = 1;
$var->{foundResults} = scalar @resources;
$var->{resourceDiv} = '<div id="taskEdit_resourceList_div">'.$self->_htmlOfResourceList({opCallbackJs => 'searchPopup_itemSelected', opIcon => 'add.gif', opTitle => $i18n->get('resource add opTitle'), hiddenFields => 0}, @resources).'</div>';
} else {
$var->{doingSearch} = 0;
}
return $self->processTemplate($var, $self->getValue('resourcePopupTemplateId'));
}
#-------------------------------------------------------------------
sub _userSearchQuery {
my $self = shift;
my $exclude = shift;
my $searchPattern = lc('%'.shift().'%');
my @exclude = ('1', '3', split /\;/, $exclude);
my $excludePlaceholders = '('.join(',', ('?') x @exclude).')';
my $query = <<"SQL";
SELECT 'user' AS resourceKind, users.userId AS resourceId
FROM users
LEFT JOIN userProfileData AS lastName ON users.userId = lastName.userId
AND lastName.fieldName = 'lastName'
LEFT JOIN userProfileData AS firstName ON users.userId = firstName.userId
AND firstName.fieldName = 'firstName'
WHERE (LOWER(lastName.fieldData) LIKE ? OR LOWER(firstName.fieldData) LIKE ?
OR LOWER(users.username) LIKE ?) AND (users.userId NOT IN $excludePlaceholders)
ORDER BY lastName.fieldData, firstName.fieldData
SQL
my @placeholders = (($searchPattern) x 3, @exclude);
return ($query, \@placeholders);
}
sub www_userSearchPopup {
my $self = shift;
my %args = (func => 'userSearchPopup',
i18nprefix => 'user add popup',
queryCallback => sub { $self->_userSearchQuery(@_) },
);
$self->_resourceSearchPopup(%args);
}
#-------------------------------------------------------------------
sub _groupSearchQuery {
my $self = shift;
my $exclude = shift;
my $searchPattern = lc('%'.shift().'%');
my @exclude = ('1', '7', split /\;/, $exclude);
my $excludePlaceholders = '('.join(',', ('?') x @exclude).')';
my $query = <<"SQL";
SELECT 'group' AS resourceKind, groups.groupId AS resourceId
FROM groups
WHERE (LOWER(groups.groupName) LIKE ?) AND (groups.groupId NOT IN $excludePlaceholders)
AND groups.isEditable = 1
ORDER BY groups.groupName
SQL
my @placeholders = ($searchPattern, @exclude);
return ($query, \@placeholders);
}
sub www_groupSearchPopup {
my $self = shift;
my %args = (func => 'groupSearchPopup',
i18nprefix => 'group add popup',
queryCallback => sub { $self->_groupSearchQuery(@_) },
);
$self->_resourceSearchPopup(%args);
}
#-------------------------------------------------------------------
sub _resourceListOfTask {
# TODO: Should there be a getAllCollateral in Asset::Wobject?
my $self = shift;
my $taskId = shift;
return ($taskId eq 'new')? () :
@{$self->session->db->buildArrayRefOfHashRefs("SELECT resourceKind, resourceId FROM PM_taskResource WHERE taskId = ? ORDER BY sequenceNumber", [$taskId])};
}
sub _innerHtmlOfResources {
my $self = shift;
my @resources = @_;
my $i18n = WebGUI::International->new($self->session, 'Asset_ProjectManager');
return $self->_htmlOfResourceList({opCallbackJs => 'taskEdit_deleteResource', opIcon => 'delete.gif', opTitle => $i18n->get('resource remove opTitle'), hiddenFields => 1}, @resources);
}
sub www_innerHtmlOfResources {
my $self = shift;
my @resources = map {
my ($resourceKind, $resourceId) = split / /, $_, 2;
{ resourceKind => $resourceKind, resourceId => $resourceId }
} split /\;/, $self->session->form->param('resources');
return $self->_innerHtmlOfResources(@resources);
}
sub www_editTask {
my $self = shift;
my $var = {};
#Set Method Helpers
my ($session,$privilege,$form,$db,$dt,$i18n,$user) = $self->setSessionVars;
my $config = $session->config;
my $projectId = $form->get("projectId");
my $taskId = $form->get("taskId") || "new";
my $project = $self->getCollateral('PM_project', 'projectId', $projectId);
my $task = $self->getCollateral('PM_task', 'taskId', $taskId);
#Check Privileges
return $privilege->insufficient unless $self->_userCanManageProject($user, $projectId);
my $taskType = ($task->{taskType} || 'timed');
my $seq = $task->{sequenceNumber};
my $disabledIfUntimed = ($taskType eq 'timed')? "" : " disabled";
my $disabledIfMilestone = ($taskType ne 'milestone')? "" : " disabled";
$var->{'form.header'} = WebGUI::Form::formHeader($session,{
action=>$self->getUrl,
extras=>q|name="editTaskForm"|
});
$var->{'form.header'} .= WebGUI::Form::hidden($session, {
-name=>"func",
-value=>"editTaskSave"
});
$var->{'form.header'} .= WebGUI::Form::hidden($session, {
-name=>"projectId",
-value=>$projectId
});
$var->{'form.header'} .= WebGUI::Form::hidden($session, {
-name=>"taskId",
-value=>$taskId
});
$var->{'form.header'} .= WebGUI::Form::hidden($session, {
-name=>"insertAt",
-value=>$form->get("insertAt")
});
my ($startEpoch, $endEpoch) = $db->quickArray('SELECT startDate, startDate FROM PM_task WHERE projectId = ? ORDER BY sequenceNumber LIMIT 1', [$projectId]);
my $dependant = $task->{dependants};
my $duration = $task->{duration} || ($taskType eq 'timed')? (($project->{durationUnits} eq 'hours')? $project->{hoursPerDay} : 1) : 0;
$startEpoch = $endEpoch = time unless defined $startEpoch and defined $endEpoch;
$startEpoch = $task->{startDate} if $task->{startDate};
$endEpoch = $task->{endDate} if $task->{endDate};
$endEpoch += 86400 if $taskType eq 'timed' and !$task->{duration} and !$task->{endDate};
my ($start, $end) = ($dt->epochToSet($startEpoch), $dt->epochToSet($endEpoch));
# Set some hidden variables to make it easy to reset data in javascript
$var->{'form.header'} .= WebGUI::Form::hidden($session, {
-name=>"orig_duration",
-value=>$duration
});
$var->{'form.header'} .= WebGUI::Form::hidden($session, {
-name=>"orig_start",
-value=>$start
});
$var->{'form.header'} .= WebGUI::Form::hidden($session, {
-name=>"orig_end",
-value=>$end
});
$var->{'form.header'} .= WebGUI::Form::hidden($session, {
-name=>"orig_dependant",
-value=>$dependant
});
$var->{'form.name'} = WebGUI::Form::text($session,{
-name=>"name",
-value=>$task->{taskName},
-extras=>q|style="width:95%;"|
});
$var->{'form.seqNum'} = WebGUI::Form::hidden($session, {
-name => "seqNum",
-value => $seq,
});
my $durationEvents = qq|onchange="durationChanged(this.form, '', true)" onblur="if (this.value == 0) durationChanged(this.form, '', true)"|;
my $startDateEvents = qq|onfocus="doCalendar(this.id)" onblur="startDateChanged(this.form, '', true)"|;
my $endDateEvents = qq|onfocus="doCalendar(this.id)" onblur="endDateChanged(this.form, '', true)"|;
$var->{'form.duration'} = WebGUI::Form::float($session,{
-name => "duration",
-value => $duration,
-extras => qq|style="width:70%;" $durationEvents $disabledIfMilestone|
});
$var->{'form.duration.units'} = $self->_getDurationUnitHashAbbrev->{$project->{durationUnits}};
$var->{'form.lagTime'} = WebGUI::Form::float($session,{
-name => "lagTime",
-value => (($taskType eq 'timed')? $task->{lagTime} : 'N/A'),
-extras => qq|style="width:70%;" $durationEvents $disabledIfUntimed|
});
$var->{'form.lagTime.units'} = $self->_getDurationUnitHashAbbrev->{$project->{durationUnits}};
$var->{'form.start'} = WebGUI::Form::text($session,{
-name=>"start",
-value=>$start,
-size=>"10",
-maxlength=>"10",
-extras=>qq|style="width:88%;" $startDateEvents|
});
$var->{'form.end'} = WebGUI::Form::text($session,{
-name=>"end",
-value=>$end,
-size=>"10",
-maxlength=>"10",
-extras=>qq|style="width:88%;" $endDateEvents $disabledIfUntimed|
});
$var->{'form.dependants'} = WebGUI::Form::integer($session,{
-name=>"dependants",
-value=>$dependant || "",
-defaultValue=>"",
-size=>4,
-maxlength=>10,
-extras=>qq|style="width:50%;" onchange="predecessorChanged(this.form, '', true)"|
});
tie my %users, "Tie::IxHash";
%users = $db->buildHash("select userId,username from users where userId not in ('1','3') order by userId");
%users = (""=>$i18n->get("resource none"),%users);
my @resources = $self->_resourceListOfTask($taskId);
my ($searchUserUrlHtml, $searchGroupUrlHtml) = map {
my $kind = $_;
my $exclude = $self->session->url->escape(join ';', map {$_->{resourceId}} grep {$_->{resourceKind} eq $kind} @resources);
my $func = $kind.'SearchPopup';
WebGUI::HTML::format($self->getUrl("func=$func;callback=taskEdit_queueAddResource;exclude=$exclude"), 'text');
} (qw/user group/);
$var->{'form.addUser.id'} = 'taskEdit_resourceList_addUser_a';
$var->{'form.addUser.link'} = $searchUserUrlHtml;
$var->{'form.addUser.text'} = $i18n->get('user add popup hover');
$var->{'form.addGroup.id'} = 'taskEdit_resourceList_addGroup_a';
$var->{'form.addGroup.link'} = $searchGroupUrlHtml;
$var->{'form.addGroup.text'} = $i18n->get('group add popup hover');
$var->{'form.resourceDiv'} =
'<div id="taskEdit_resourceList_div">'.$self->_innerHtmlOfResources(@resources).'</div>';
my %taskTypeOptions;
tie %taskTypeOptions, 'Tie::IxHash';
foreach my $option (qw/timed progressive milestone/) {
$taskTypeOptions{$option} = $i18n->get("taskType $option label");
}
$var->{'form.taskType'} = WebGUI::Form::radioList($session, {
-name=>'taskType',
-vertical=>0,
-options=>\%taskTypeOptions,
-defaultValue=>$taskType,
-extras=>q|onclick="configureForTaskType(this.form)"|,
});
$var->{'form.percentComplete'} = WebGUI::Form::float($session, {
-name => "percentComplete",
-value => (($taskType eq 'timed')? $task->{percentComplete} : 'N/A'),
-extras => $disabledIfUntimed
});
$var->{'form.save'} = WebGUI::Form::submit($session, {
-value=>"Save",
-extras=>q|name="subbutton"|
});
$var->{'form.footer'} = WebGUI::Form::formFooter($session);
$var->{'extras'} = $config->get("extrasURL");
$var->{'assetExtras'} = $config->get("extrasURL").'/wobject/ProjectManager';
return $self->processTemplate($var,$self->getValue("editTaskTemplateId"))
}
#-------------------------------------------------------------------
sub www_editTaskSave {
my $self = shift;
my $var = {};
#Set Method Helpers
my ($session,$privilege,$form,$db,$dt,$i18n,$user) = $self->setSessionVars;
my $config = $session->config;
my $eh = $session->errorHandler;
my $projectId = $form->get("projectId");
my $project = $db->quickHashRef("select * from PM_project where projectId=".$db->quote($projectId));
#Check Privileges
return $privilege->insufficient unless $self->_userCanManageProject($user, $projectId);
my $taskType = $form->process("taskType", "radioList");
my ($isMilestone, $isUntimed) = ($taskType eq 'milestone', $taskType ne 'timed');
my $isProgressive = ($taskType eq 'progressive');
my $props = {};
$props->{taskId} = $form->process("taskId","hidden");
$props->{projectId} = $projectId;
$props->{taskName} = $form->process("name","text");
$props->{duration} = $isMilestone? 0 : $form->process("duration","text");
$props->{lagTime} = $isUntimed? 0 : $form->process("lagTime","text");
$props->{startDate} = $form->process("start","date");
$props->{endDate} = $isMilestone? $props->{startDate} :
$isProgressive? undef : $form->process("end","date");
$props->{dependants} = $form->process("dependants","selectBox") unless $isUntimed;
$props->{taskType} = $taskType;
my @resourceSpecs = $form->process("resources","hiddenList");
$props->{percentComplete} = $isUntimed? 0 : $form->process("percentComplete","float");
unless (defined $props->{endDate}) {
my $totalDuration = $props->{duration} + $props->{lagTime};
my $totalDurationDays = ($project->{durationUnits} eq 'days')? $totalDuration : ($totalDuration / $project->{hoursPerDay});
$props->{endDate} = $props->{startDate} + $totalDurationDays*86400;
}
my $now = $dt->time();
if($props->{taskId} eq "new") {
$props->{creationDate} = $now;
$props->{createdBy} = $user->userId;
}
$props->{lastUpdateDate} = $now;
$props->{lastUpdatedBy} = $user->userId;
# Save the extended task data
my $taskId = $self->setCollateral("PM_task","taskId",$props,1,0,"projectId",$projectId);
$self->deleteCollateral('PM_taskResource', 'taskId', $taskId);
foreach my $resourceSpec (@resourceSpecs) {
my ($resourceKind, $resourceId) = split / /, $resourceSpec, 2;
$self->setCollateral('PM_taskResource', 'taskResourceId', {taskId => $taskId, resourceKind => $resourceKind, resourceId => $resourceId}, 1, 0, 'taskId', $taskId);
}
#Reorder tasks if task is inserted
my $insertAt = $form->get("insertAt");
if($insertAt) {
#$eh->warn("Inserting at $insertAt");
my $tasks = $db->buildArrayRefOfHashRefs("select * from PM_task where projectId=? order by sequenceNumber",[$projectId]);
my $taskLen = scalar(@{$tasks});
#$eh->warn("Task Length = $taskLen");
foreach my $task (@{$tasks}) {
my $seqNum = $task->{sequenceNumber};
next unless ($seqNum >= $insertAt);
#$eh->warn("Fixing task $seqNum");
my $newSeq = $seqNum + 1;
if($seqNum eq $taskLen) {
$newSeq = $insertAt;
}
#$eh->warn("New seqNum is $newSeq");
my $dependant = $task->{dependants};
#$eh->warn("Dependant is $dependant");
#Only increment the dependant if it's greater than or equal to the insertAt point
if($dependant >= $insertAt){
$dependant++;
}
#$eh->warn("New dependant is $dependant");
$db->write("update PM_task set sequenceNumber=?, dependants=? where taskId=?",[$newSeq,$dependant,$task->{taskId}]);
}
$self->reorderCollateral("PM_task","taskId","projectId",$projectId);
}
$self->_updateProject($projectId);
$self->_clobberImproperDependants($projectId);
$self->_updateDependantDates($projectId);
return $self->www_viewProject($projectId,$taskId);
}
#-------------------------------------------------------------------
sub _updateDependantDates {
my $self = shift;
my $db = $self->session->db;
my $dt = $self->session->datetime;
my ($projectId) = @_;
my $project = $self->getCollateral('PM_project', 'projectId', $projectId);
my $tasks = $db->buildArrayRefOfHashRefs("select * from PM_task where projectId=?",[$projectId]);
my $taskHash = {};
foreach my $task (@{$tasks}) {
my $seqNum = $task->{sequenceNumber};
# Calculate initial duration in days and duration floor for this task.
my $totalDurationInDays = $task->{duration} + $task->{lagTime};
$totalDurationInDays = $totalDurationInDays / $project->{hoursPerDay} if( $project->{durationUnits} eq "hours" );
my $totalDurationFloor = floor($totalDurationInDays);
# If we have a predecessor, check against it.
if (my $predecessor = $task->{dependants}) {
my $pred = $taskHash->{$predecessor};
unless ($pred) {
# Predecessor has to have a lower sequence number, right? Right?
$self->session->errorHandler->error("Internal: predecessor '$predecessor' of task with seqNum '$seqNum' not in task hash?!");
next;
}
# Need to fix the dates iff we intersect our predecessor.
if ($task->{startDate} <= $pred->{endDate}) {
# Update for fractional day part.
# Buggo: why?
$totalDurationInDays += $pred->{dayPart};
$totalDurationFloor = floor($totalDurationInDays);
# Set start and end dates of the current task.
$task->{startDate} = $pred->{endDate};
$task->{endDate} = $dt->addToDateTime($task->{startDate}, 0, 0, $totalDurationFloor);
# Update.
$self->setCollateral("PM_task","taskId",$task,1,0,"projectId",$projectId);
}
}
# Extract fractional day part.
my $totalDurationInDaysFrac = $totalDurationInDays - floor($totalDurationInDays);
$taskHash->{$seqNum} = {
'startDate'=>$task->{startDate},
'endDate'=>$task->{endDate},
'duration'=>$task->{duration},
'dayPart'=>$totalDurationInDaysFrac
};
}
}
sub _clobberImproperDependants {
my $self = shift;
my $projectId = shift;
my @nondependTaskIds = $self->session->db->buildArray("SELECT sequenceNumber FROM PM_task WHERE projectId = ? AND taskType <> 'timed'", [$projectId]);
return unless @nondependTaskIds;
$self->session->db->write("UPDATE PM_task SET dependants = NULL WHERE projectId = ? AND dependants IN (".join(', ', ('?') x @nondependTaskIds).")", [$projectId, @nondependTaskIds]);
}
#-------------------------------------------------------------------
sub www_saveExistingTasks {
my $self = shift;
my $var = {};
#Set Method Helpers
my ($session,$privilege,$form,$db,$dt,$i18n,$user) = $self->setSessionVars;
my $config = $session->config;
my $projectId = $form->get("projectId");
my $project = $db->quickHashRef("select * from PM_project where projectId=".$db->quote($projectId));
#Check Privileges
return $privilege->insufficient unless $self->_userCanManageProject($user, $projectId);
my $tasks = $db->buildArrayRefOfHashRefs("select * from PM_task where projectId=".$db->quote($projectId)." order by sequenceNumber asc");
#Save each row
foreach my $task (@{$tasks}) {
my $taskType = $task->{taskType};
my $isUntimed = ($taskType ne 'timed');
my $props = {};
my $taskId = $task->{taskId};
$props->{taskId} = $taskId;
$props->{projectId} = $projectId;
$props->{startDate} = $form->process("start_$taskId","date");
$props->{endDate} = $form->process("end_$taskId","date");
$props->{dependants} = $form->process("dependants_$taskId","selectBox");
unless ($isUntimed) {
$props->{duration} = $form->process("duration_$taskId","float");
$props->{lagTime} = $form->process("lagTime_$taskId","float");
}
$props->{lastUpdateDate} = $dt->time();
$props->{lastUpdatedBy} = $user->userId;
$self->setCollateral("PM_task","taskId",$props,1,0,"projectId",$projectId);
}
$self->_updateProject($projectId);
$self->_updateDependantDates($projectId);
return $self->www_drawGanttChart();
}
#-------------------------------------------------------------------
sub www_viewProject {
my $self = shift;
my $var = {};
#Set Method Helpers
my ($session,$privilege,$form,$db,$dt,$i18n,$user) = $self->setSessionVars;
my $config = $session->config;
my $style = $session->style;
my $eh = $session->errorHandler;
my $projectId = $_[0] || $form->get("projectId");
#Check Privileges
return $privilege->insufficient unless $self->_userCanObserveProject($user, $projectId);
my $extras = $config->get("extrasURL");
my $assetExtras = $config->get("extrasURL")."/wobject/ProjectManager";
$var->{'extras'} = $assetExtras;
$var->{'extras.base'} = $extras;
#Set Some Style stuff
$style->setLink($assetExtras."/subModal.css",{rel=>"stylesheet",type=>"text/css"});
$style->setLink($assetExtras."/taskEdit.css",{rel=>"stylesheet",type=>"text/css"});
$style->setLink($extras."/calendar/calendar-win2k-1.css",{rel=>"stylesheet",type=>"text/css"});
$style->setLink($assetExtras."/cMenu.css",{rel=>"stylesheet",type=>"text/css"});
$style->setScript($assetExtras."/cMenu.js",{ type=>"text/javascript" });
$style->setScript($extras."/js/at/AjaxRequest.js",{ type=>"text/javascript" });
$style->setScript($extras."/js/modal/modal.js",{ type=>"text/javascript" });
$style->setScript($extras."/calendar/calendar.js",{ type=>"text/javascript" });
$style->setScript($extras."/contextMenu/contextMenu.js",{ type=>"text/javascript" });
$style->setScript($extras."/calendar/lang/calendar-en.js",{ type=>"text/javascript" });
$style->setScript($extras."/calendar/calendar-setup.js",{ type=>"text/javascript" });
$style->setScript($assetExtras."/projectDisplay.js",{ type=>"text/javascript" });
$style->setScript($assetExtras."/taskEdit.js",{ type=>"text/javascript" });
#Get Project Data
my $project = $db->quickHashRef("select * from PM_project where projectId=".$db->quote($projectId));
my $canEditTasks = $self->_userCanManageProject($user, $projectId);
my $dunits = $self->_getDurationUnitHashAbbrev->{$project->{durationUnits}};
#Set some JavaScript stuff
$var->{'project.durationUnits'} = $dunits;
$var->{'project.hoursPerDay'} = $project->{hoursPerDay} || "0";
#Get Tasks
my $data = $db->buildArrayRefOfHashRefs("select * from PM_task where projectId=".$db->quote($projectId)." order by sequenceNumber asc");
$var->{'task.name.label'} = $i18n->get("task name label");
$var->{'task.duration.label'} = $i18n->get("task duration label");
$var->{'task.start.label'} = $i18n->get("task start label");
$var->{'task.end.label'} = $i18n->get("task end label");
$var->{'task.dependants.label'} = $i18n->get("task dependant label");
# JavaScript Alert Errors for Tasks
$var->{'form.name.error'} = $i18n->get("task name error");
$var->{'form.start.error'} = $i18n->get("task start error");
$var->{'form.end.error'} = $i18n->get("task end error");
$var->{'form.greaterthan.error'} = $i18n->get("task greaterthan error");
$var->{'form.previousPredecessor.error'} = $i18n->get("task previousPredecessor error");
$var->{'form.samePredecessor.error'} = $i18n->get("task samePredecessor error");
$var->{'form.noPredecessor.error'} = $i18n->get("task noPredecessor error");
$var->{'form.invalidMove.error'} = $i18n->get("task invalidMove error");
$var->{'form.untimedPredecessor.error'} = $i18n->get("task untimedPredecessor error");
my @taskList = ();
my $count = 0;
foreach my $row (@{$data}) {
my $hash = {};
my $seq = $row->{sequenceNumber};
my $id = $row->{taskId};
my $taskType = $row->{taskType};
my $isUntimed = ($taskType ne 'timed');
my $startDate = $dt->epochToSet($row->{startDate});
my $endDate = $dt->epochToSet($row->{endDate});
my $duration = $row->{duration};
my $lagTime = $row->{lagTime};
$hash->{'task.number'} = $seq;
$hash->{'task.row.id'} = $id;
$hash->{'task.name'} = $row->{taskName};
if($canEditTasks) {
my $suffix = '_'.$id;
$hash->{'task.start'} = WebGUI::Form::text($session,{
-name=>'start'.$suffix,
-value=>$startDate,
-size=>"10",
-maxlength=>"10",
-extras=>qq<onfocus="doCalendar(this.id);" class="taskdate" onblur="startDateChanged(this.form, '$suffix', false);">
});
$hash->{'task.start'} .= WebGUI::Form::hidden($session,{
-name=>'orig_start'.$suffix,
-value=>$startDate,
});
$hash->{'task.dependants'} = WebGUI::Form::integer($session,{
-name=>'dependants'.$suffix,
-value=>$row->{dependants} || "",
-defaultValue=>"",
-extras=>qq|class="taskdependant" onchange="predecessorChanged(this.form, '$suffix', false);"|
});
$hash->{'task.dependants'} .= WebGUI::Form::hidden($session,{
-name=>'orig_dependants'.$suffix,
-value=>$row->{dependants},
});
$hash->{'task.end'} = WebGUI::Form::text($session,{
-name=>'end'.$suffix,
-value=>$endDate,
-size=>"10",
-maxlength=>"10",
-extras=>qq|class="taskdate" onfocus="doCalendar(this.id);" onblur="endDateChanged(this.form, '$suffix', false);"|
});
$hash->{'task.end'} .= WebGUI::Form::hidden($session,{
-name=>'orig_end'.$suffix,
-value=>$endDate,
});
# Don't display duration for untimed tasks.
if ($isUntimed) {
$hash->{'task.duration'} = $row->{duration};
$hash->{'task.duration'} .= WebGUI::Form::hidden($session,{
-name=>'duration'.$suffix,
-value=>$duration,
});
} else {
$hash->{'task.duration'} = WebGUI::Form::float($session,{
-name=>'duration'.$suffix,
-value=>$duration,
-extras=>qq|class="taskduration" onchange="durationChanged(this.form, '$suffix', false);" |
});
}
$hash->{'task.lagTime'} = WebGUI::Form::hidden($session,{
-name => 'lagTime'.$suffix,
-value => $lagTime,
});
$hash->{'task.taskType'} = WebGUI::Form::hidden($session, {
-name => 'taskType'.$suffix,
-value => $taskType,
});
$hash->{'task.seqNum'} = WebGUI::Form::hidden($session, {
-name => 'seqNum'.$suffix,
-value => $seq
});
} else {
$hash->{'task.duration'} = $duration;
$hash->{'task.start'} = $startDate;
$hash->{'task.end'} = $endDate;
$hash->{'task.dependants'} = $row->{dependants} || "&nbsp;";
}
$hash->{'task.duration.units'} = $dunits;
if($canEditTasks) {
$hash->{'task.edit.url'} = $self->getUrl("func=editTask;projectId=$projectId;taskId=".$row->{taskId});
$hash->{'task.edit.label'} = $i18n->get("edit task label");
my $num = $row->{sequenceNumber};
$hash->{'task.insertAbove.url'} = $self->getUrl("func=editTask;projectId=$projectId;taskId=new;insertAt=$num");
$hash->{'task.insertAbove.label'} = $i18n->echo("Insert Task Above");
$hash->{'task.insertBelow.url'} = $self->getUrl("func=editTask;projectId=$projectId;taskId=new;insertAt=".($num+1));
$hash->{'task.insertBelow.label'} = $i18n->echo("Insert Task Below");
$hash->{'task.delete.url'} = $self->getUrl("func=deleteTask;taskId=".$row->{taskId});
$hash->{'task.delete.label'} = $i18n->echo("Delete Task");
}
push(@taskList, $hash);
}
$var->{'task.loop'} = \@taskList;
#Set some javascript stuff;
my $taskLength = scalar(@taskList);
$var->{'project.task.length'} = $taskLength;
if($canEditTasks) {
#Build Form for submitting on the fly updates
$var->{'form.header'} = WebGUI::Form::formHeader($session,{
action=>$self->getUrl,
extras=>q|name="editAll"|
});
$var->{'form.header'} .= WebGUI::Form::hidden($session, {
-name=>"func",
-value=>"saveExistingTasks"
});
$var->{'form.header'} .= WebGUI::Form::hidden($session, {
-name=>"projectId",
-value=>$projectId
});
$var->{'form.footer'} = WebGUI::Form::formFooter($session);
$var->{'project.canEdit'} = "true";
$var->{'task.resources.label'} = $i18n->get("task resources label");
$var->{'task.resources.url'} = $self->getUrl("func=manageResources");
}
if($canEditTasks) {
$var->{'task.add.label'} = $i18n->get("add task label");
$var->{'task.add.url'} = $self->getUrl("func=editTask;projectId=$projectId;taskId=new");
$var->{'task.canAdd'} = "true";
}
# Rowspan of gantt chart is 4 plus number of tasks
$var->{'project.gantt.rowspan'} = 4 + $taskLength;
$var->{'project.ganttChart'} = $self->www_drawGanttChart($projectId, $data, $var);
$var->{'task.back.label'} = $i18n->get("task back label");
$var->{'task.back.url'} = $self->getUrl;
return $style->process($self->processTemplate($var,$self->getValue("projectDisplayTemplateId")),$self->getValue("styleTemplateId"));
}
#-------------------------------------------------------------------
sub _doGanttTaskResourceDisplay {
my $self = shift;
my $hash = shift;
my $task = shift;
my @resources = $self->_resourceListOfTask($task->{taskId});
my @resourceNames = ();
foreach my $resource (@resources) {
my ($resourceKind, $resourceId) = @$resource{qw{resourceKind resourceId}};
if ($resourceKind eq 'user') {
my $u = WebGUI::User->new($self->session, $resourceId);
my $name = $u->username;
my $firstName = $u->profileField('firstName');
my $lastName = $u->profileField('lastName');
$name = "$firstName $lastName"
if (length $firstName and length $lastName);
push @resourceNames, $name;
} elsif ($resourceKind eq 'group') {
my $g = WebGUI::Group->new($self->session, $resourceId);
push @resourceNames, $g->name;
} else {
# Whee.
push @resourceNames, "???"
}
}
if (@resources) {
$hash->{'task.hasResource'} = "true";
$hash->{'task.resource.name'} =
join(', ', map { WebGUI::HTML::format($_, 'text') } @resourceNames);
}
}
sub www_drawGanttChart {
my $self = shift;
my $var = {};
#Set Method Helpers
my ($session,$privilege,$form,$db,$dt,$i18n,$user) = $self->setSessionVars;
my $config = $session->config;
my $style = $session->style;
my $eh = $session->errorHandler;
#Set up some the task data
my $projectId = $_[0];
my $taskList = $_[1] || [];
my $projVar = $_[2] || {};
my $calledByAjax = 0;
unless($projectId) {
$projectId = $form->get("projectId");
$taskList = $db->buildArrayRefOfHashRefs("select * from PM_task where projectId=".$db->quote($projectId)." order by sequenceNumber asc");
$calledByAjax = 1;
}
#Check Privileges
return $privilege->insufficient unless $self->_userCanObserveProject($user, $projectId);
my ($dunits,$hoursPerDay) = $db->quickArray("select durationUnits,hoursPerDay from PM_project where projectId=".$db->quote($projectId));
$var->{'extras'} = $config->get("extrasURL")."/wobject/ProjectManager";
#Initialize display settings
my $projectDisplay = "weeks";
my $monthInterval = 5;
my $dayLimit = 150;
if($projectDisplay eq "weeks") {
$monthInterval = 3;
$dayLimit = 60;
}
#Find start and end months: Set start and end months to always display at least 4 months if display in weeks, 6 months if display in months
my ($startMonth,$endMonth);
if(scalar(@{$taskList}) > 0) {
($startMonth) = $db->quickArray("select min(startDate) from PM_task where projectId=".$db->quote($projectId));
($endMonth) = $db->quickArray("select max(endDate) from PM_task where projectId=".$db->quote($projectId));
#$eh->warn("Interval is: ".$dt->getDaysInInterval($startMonth,$endMonth));
if($dt->getDaysInInterval($startMonth,$endMonth) < 60) {
$endMonth = $dt->addToDate($startMonth,0,3);
}
} else {
$startMonth = $dt->time;
$endMonth = $dt->addToDate($startMonth,0,3);
}
#$eh->warn($dt->epochToSet($startMonth));
#$eh->warn($dt->epochToSet($endMonth));
#Build the loops of weeks and days
my @monthsLoop = ();
my @daysLoop = ();
if($projectDisplay eq "weeks") {
#Week View Below
my $dayOfWeek = $dt->getDayOfWeek($startMonth);
my $sundayAdjust = (0 - ($dayOfWeek % 7));
$startMonth = $dt->addToDateTime($startMonth,0,0,$sundayAdjust,1);
my @days = (
$i18n->get("sunday label"),
$i18n->get("monday label"),
$i18n->get("tuesday label"),
$i18n->get("wednesday label"),
$i18n->get("thursday label"),
$i18n->get("friday label"),
$i18n->get("saturday label"),
);
#$eh->warn($dt->epochToSet($sundayOfFirstWeek));
#$eh->warn($dt->epochToSet($endMonth));
my $datecounter = $startMonth;
my $counter = 0;
while($datecounter <= $endMonth || $counter++ == 1000) {
my $hash = {};
$hash->{'month.name'} = $dt->epochToHuman($datecounter,"%C %d, %Y");
$hash->{'month.colspan'} = 7;
push(@monthsLoop,$hash);
#Add 7 days for this week
foreach (@days) {
push(@daysLoop,{'day.number' => $_ });
}
#$eh->warn($dt->epochToSet($datecounter));
$datecounter = $dt->addToDateTime($datecounter,0,0,7);
}
} else {
#Months View Below
my $junk;
($startMonth,$junk) = $dt->monthStartEnd($startMonth);
($junk,$endMonth) = $dt->monthStartEnd($endMonth);
my $numMonths = ($dt->monthCount($startMonth,$endMonth) + 1);
for(my $i = 0; $i < $numMonths; $i++) {
my $hash = {};
my $month = $dt->addToDateTime($startMonth,0,$i,0,1);
$hash->{'month.name'} = $dt->epochToHuman($month,"%C %Y");
$self->_addDaysForMonth(\@daysLoop,$hash,$month);
push(@monthsLoop,$hash);
}
}
$var->{'months.loop'} = \@monthsLoop;
$var->{'days.loop'} = \@daysLoop;
#Build tasks and divs
my @taskCount = ();
my @taskDiv = ();
my $taskCount = 0;
my $pixelSize = ($projectDisplay eq "weeks")?23:3.2857;
$var->{'project.task.array'} = "{\n";
my $taskHash = {};
foreach my $task (@{$taskList}) {
my $hash = {};
my $id = $task->{taskId};
my $seq = $task->{sequenceNumber};
my $taskType = $task->{taskType};
my $startDate = $task->{startDate};
my $endDate = $task->{endDate};
my $duration = $task->{duration};
my $lagTime = $task->{lagTime};
my $totalDuration = $duration + $lagTime;
my $predecessor = $task->{dependants};
$self->_doGanttTaskResourceDisplay($hash, $task);
if ($dunits eq 'hours') {
foreach ($duration, $lagTime, $totalDuration) {
$_ /= $hoursPerDay;
}
}
my ($durationFloor, $lagTimeFloor, $totalDurationFloor) =
map {floor($_)} ($duration, $lagTime, $totalDuration);
# Each day is 23 pixels so calculate the days and round
$hash->{'task.div.width'} = int(($totalDuration * $pixelSize));
# Lerp RGB: probably not the best way, but it's good enough.
my @zerolag_color = (0x7a, 0xb7, 0xe9);
my @alllag_color = (0x7a, 0xb7, 0xe9);
$hash->{'task.div.color'} = sprintf "#%02x%02x%02x",
($totalDuration > 0)? do {
my $lerp = $lagTime / $totalDuration;
map { $zerolag_color[$_] +
($alllag_color[$_] - $zerolag_color[$_]) * $lerp }
(0..2);
} : @zerolag_color;
$hash->{'task.div.label.left'} = $hash->{'task.div.width'} + 3;
$hash->{'task.div.label.left'} = 12 if $hash->{'task.div.label.left'} < 12;
my $predDayPart = 0;
my $predEndDate = "";
if($predecessor) {
my $pred = $taskHash->{$predecessor};
$predEndDate = $pred->{endDate};
#Get the day part of the predecessor
$predDayPart = $pred->{dayPart};
if($startDate eq $predEndDate && $predDayPart > 0) {
#The previous task took up a part of the same day. Add the additional day part to get the correct duration
$duration += $predDayPart;
$totalDuration += $predDayPart;
$durationFloor = floor($duration);
$totalDurationFloor = floor($totalDuration);
}
}
#Adjust top for MSIE
my $isMSIE = ($session->env->get("HTTP_USER_AGENT") =~ /msie/i);
my $divTop = $isMSIE ? 45 : 45;
#Start at 45 px and add 20px as the start of the new task
#Set the propert mutiplier
my $multiplier = $isMSIE ? 22 : 20;
$hash->{'task.div.top'} = $divTop + ($multiplier*$taskCount);
#Interval includes current day if the start date is not the start of the month so add 1
my $adder = 1;
if($dt->epochToSet($startMonth) eq $dt->epochToSet($startDate)) {
$adder = 0;
}
my $daysFromStart = ($dt->getDaysInInterval($startMonth,$startDate)+$adder);
#Add day part of predecessor if necessary
#$eh->warn("Task $seq is currently $daysFromStart days from the first day on ".$dt->epochToHuman($startDate));
my $daysLeft = $daysFromStart;
#Only adjust for predecessor if the start date of the task falls on the same day as it's predecessors end date
if($startDate eq $predEndDate) {
$daysLeft += $predDayPart;
#$eh->warn("Adjusting this by $predDayPart days");
}
$hash->{'task.div.left'} = int(($daysLeft * $pixelSize)); #Each day is 23 pixels so calculate the days and round
#$eh->warn("Starts at: $daysLeft * $pixelSize :".$hash->{'task.div.left'});
# Buggo. Refactor.
$hash->{'task.isUntimed'} = ($task->{taskType} ne 'timed');
$hash->{'task.hasDuration'} = ($task->{taskType} ne 'milestone');
push(@taskDiv, $hash);
push(@taskCount, { 'task.counter' => $task->{sequenceNumber} } );
$var->{'project.task.array'} .= ",\n" if($taskCount > 0);
$startDate = $dt->epochToSet($startDate);
$endDate = $dt->epochToSet($endDate);
my $rduration = $task->{duration};
#Adjust duration of days to only include the part of the day used
#$eh->warn("day part is being set to: $duration - ".floor($duration)." : ".($duration-floor($duration)));
$duration = $duration - floor($duration);
$hash->{'task.div.percentComplete'} = $task->{percentComplete} || 0;
$var->{'project.task.array'} .= qq|"$seq": { "id":"$id" ,"start":"$startDate" ,"end":"$endDate", "duration":"$rduration", "dayPart":"$duration", "lagTime":"$lagTime", "predecessor":"$predecessor", "type":"$taskType" }|;
$taskCount++;
$taskHash->{$seq} = {
'startDate'=>$task->{startDate},
'endDate'=>$task->{endDate},
'duration'=>$task->{duration},
'lagTime'=>$task->{lagTime},
'dayPart'=>$duration
};
}
$var->{'task.count.loop'} = \@taskCount;
$var->{'task.div.loop'} = \@taskDiv;
$var->{'project.task.array'} .= "\n}";
#Set Gantt Chart template vars
my $cols = scalar(@daysLoop);
$var->{'total.colspan'} = $cols;
$var->{'scrollWidth'} = $cols * 23;
#Set project template vars
$var->{'project.table.width'} = $projVar->{'project.table.width'} = 560 + $var->{'scrollWidth'};
my $scrollWidth = (1- (560 / $projVar->{'project.table.width'})) * 100;
$var->{'project.scroll.percentWidth'} = $projVar->{'project.scroll.percentWidth'} = sprintf("%2.2f",$scrollWidth);
return $self->processTemplate($var,$self->getValue("ganttChartTemplateId"));
}
1;