Add burndown chart
This commit is contained in:
@@ -75,6 +75,7 @@ Documentation
|
|||||||
- [Swimlanes](docs/swimlanes.markdown)
|
- [Swimlanes](docs/swimlanes.markdown)
|
||||||
- [Calendar](docs/calendar.markdown)
|
- [Calendar](docs/calendar.markdown)
|
||||||
- [Budget](docs/budget.markdown)
|
- [Budget](docs/budget.markdown)
|
||||||
|
- [Analytics](docs/analytics.markdown)
|
||||||
|
|
||||||
#### Working with tasks
|
#### Working with tasks
|
||||||
|
|
||||||
|
|||||||
@@ -125,4 +125,46 @@ class Analytic extends Base
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show burndown chart
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
public function burndown()
|
||||||
|
{
|
||||||
|
$project = $this->getProject();
|
||||||
|
$values = $this->request->getValues();
|
||||||
|
|
||||||
|
$from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week')));
|
||||||
|
$to = $this->request->getStringParam('to', date('Y-m-d'));
|
||||||
|
|
||||||
|
if (! empty($values)) {
|
||||||
|
$from = $values['from'];
|
||||||
|
$to = $values['to'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
$this->response->json(array(
|
||||||
|
'metrics' => $this->projectDailySummary->getRawMetricsByDay($project['id'], $from, $to),
|
||||||
|
'labels' => array(
|
||||||
|
'day' => t('Date'),
|
||||||
|
'score' => t('Complexity'),
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->response->html($this->layout('analytic/burndown', array(
|
||||||
|
'values' => array(
|
||||||
|
'from' => $from,
|
||||||
|
'to' => $to,
|
||||||
|
),
|
||||||
|
'display_graph' => $this->projectDailySummary->countDays($project['id'], $from, $to) >= 2,
|
||||||
|
'project' => $project,
|
||||||
|
'date_format' => $this->config->get('application_date_format'),
|
||||||
|
'date_formats' => $this->dateParser->getAvailableFormats(),
|
||||||
|
'title' => t('Burndown chart for "%s"', $project['name']),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -852,4 +852,7 @@ return array(
|
|||||||
'uploaded by: %s' => 'Télécharger par : %s',
|
'uploaded by: %s' => 'Télécharger par : %s',
|
||||||
'uploaded on: %s' => 'Télécharger le : %s',
|
'uploaded on: %s' => 'Télécharger le : %s',
|
||||||
'size: %s' => 'Taille : %s',
|
'size: %s' => 'Taille : %s',
|
||||||
|
'Burndown chart for "%s"' => 'Graphique d\'avancement pour « %s »',
|
||||||
|
'Burndown chart' => 'Graphique d\'avancement',
|
||||||
|
'This chart show the task complexity over the time (Work Remaining).' => 'Ce graphique représente la complexité des tâches en fonction du temps (travail restant).',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -850,4 +850,7 @@ return array(
|
|||||||
// 'uploaded by: %s' => '',
|
// 'uploaded by: %s' => '',
|
||||||
// 'uploaded on: %s' => '',
|
// 'uploaded on: %s' => '',
|
||||||
// 'size: %s' => '',
|
// 'size: %s' => '',
|
||||||
|
// 'Burndown chart for "%s"' => '',
|
||||||
|
// 'Burndown chart' => '',
|
||||||
|
// 'This chart show the task complexity over the time (Work Remaining).' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ class ProjectAnalytic extends Base
|
|||||||
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
|
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ksort($metrics);
|
||||||
|
|
||||||
return array_values($metrics);
|
return array_values($metrics);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ class ProjectDailySummary extends Base
|
|||||||
/**
|
/**
|
||||||
* Update daily totals for the project
|
* Update daily totals for the project
|
||||||
*
|
*
|
||||||
|
* "total" is the number open of tasks in the column
|
||||||
|
* "score" is the sum of tasks score in the column
|
||||||
|
*
|
||||||
* @access public
|
* @access public
|
||||||
* @param integer $project_id Project id
|
* @param integer $project_id Project id
|
||||||
* @param string $date Record date (YYYY-MM-DD)
|
* @param string $date Record date (YYYY-MM-DD)
|
||||||
@@ -40,6 +43,7 @@ class ProjectDailySummary extends Base
|
|||||||
'project_id' => $project_id,
|
'project_id' => $project_id,
|
||||||
'column_id' => $column_id,
|
'column_id' => $column_id,
|
||||||
'total' => 0,
|
'total' => 0,
|
||||||
|
'score' => 0,
|
||||||
));
|
));
|
||||||
|
|
||||||
$db->table(ProjectDailySummary::TABLE)
|
$db->table(ProjectDailySummary::TABLE)
|
||||||
@@ -47,6 +51,11 @@ class ProjectDailySummary extends Base
|
|||||||
->eq('column_id', $column_id)
|
->eq('column_id', $column_id)
|
||||||
->eq('day', $date)
|
->eq('day', $date)
|
||||||
->update(array(
|
->update(array(
|
||||||
|
'score' => $db->table(Task::TABLE)
|
||||||
|
->eq('project_id', $project_id)
|
||||||
|
->eq('column_id', $column_id)
|
||||||
|
->eq('is_active', Task::STATUS_OPEN)
|
||||||
|
->sum('score'),
|
||||||
'total' => $db->table(Task::TABLE)
|
'total' => $db->table(Task::TABLE)
|
||||||
->eq('project_id', $project_id)
|
->eq('project_id', $project_id)
|
||||||
->eq('column_id', $column_id)
|
->eq('column_id', $column_id)
|
||||||
@@ -92,12 +101,39 @@ class ProjectDailySummary extends Base
|
|||||||
ProjectDailySummary::TABLE.'.column_id',
|
ProjectDailySummary::TABLE.'.column_id',
|
||||||
ProjectDailySummary::TABLE.'.day',
|
ProjectDailySummary::TABLE.'.day',
|
||||||
ProjectDailySummary::TABLE.'.total',
|
ProjectDailySummary::TABLE.'.total',
|
||||||
|
ProjectDailySummary::TABLE.'.score',
|
||||||
Board::TABLE.'.title AS column_title'
|
Board::TABLE.'.title AS column_title'
|
||||||
)
|
)
|
||||||
->join(Board::TABLE, 'id', 'column_id')
|
->join(Board::TABLE, 'id', 'column_id')
|
||||||
->eq(ProjectDailySummary::TABLE.'.project_id', $project_id)
|
->eq(ProjectDailySummary::TABLE.'.project_id', $project_id)
|
||||||
->gte('day', $from)
|
->gte('day', $from)
|
||||||
->lte('day', $to)
|
->lte('day', $to)
|
||||||
|
->asc(ProjectDailySummary::TABLE.'.day')
|
||||||
|
->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get raw metrics for the project within a data range grouped by day
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $project_id Project id
|
||||||
|
* @param string $from Start date (ISO format YYYY-MM-DD)
|
||||||
|
* @param string $to End date
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getRawMetricsByDay($project_id, $from, $to)
|
||||||
|
{
|
||||||
|
return $this->db->table(ProjectDailySummary::TABLE)
|
||||||
|
->columns(
|
||||||
|
ProjectDailySummary::TABLE.'.day',
|
||||||
|
'SUM('.ProjectDailySummary::TABLE.'.total) AS total',
|
||||||
|
'SUM('.ProjectDailySummary::TABLE.'.score) AS score'
|
||||||
|
)
|
||||||
|
->eq(ProjectDailySummary::TABLE.'.project_id', $project_id)
|
||||||
|
->gte('day', $from)
|
||||||
|
->lte('day', $to)
|
||||||
|
->asc(ProjectDailySummary::TABLE.'.day')
|
||||||
|
->groupBy(ProjectDailySummary::TABLE.'.day')
|
||||||
->findAll();
|
->findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ use PDO;
|
|||||||
use Core\Security;
|
use Core\Security;
|
||||||
use Model\Link;
|
use Model\Link;
|
||||||
|
|
||||||
const VERSION = 63;
|
const VERSION = 64;
|
||||||
|
|
||||||
|
function version_64($pdo)
|
||||||
|
{
|
||||||
|
$pdo->exec('ALTER TABLE project_daily_summaries ADD COLUMN score INT NOT NULL DEFAULT 0');
|
||||||
|
}
|
||||||
|
|
||||||
function version_63($pdo)
|
function version_63($pdo)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ use PDO;
|
|||||||
use Core\Security;
|
use Core\Security;
|
||||||
use Model\Link;
|
use Model\Link;
|
||||||
|
|
||||||
const VERSION = 44;
|
const VERSION = 45;
|
||||||
|
|
||||||
|
function version_45($pdo)
|
||||||
|
{
|
||||||
|
$pdo->exec('ALTER TABLE project_daily_summaries ADD COLUMN score INTEGER NOT NULL DEFAULT 0');
|
||||||
|
}
|
||||||
|
|
||||||
function version_44($pdo)
|
function version_44($pdo)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ use Core\Security;
|
|||||||
use PDO;
|
use PDO;
|
||||||
use Model\Link;
|
use Model\Link;
|
||||||
|
|
||||||
const VERSION = 62;
|
const VERSION = 63;
|
||||||
|
|
||||||
|
function version_63($pdo)
|
||||||
|
{
|
||||||
|
$pdo->exec('ALTER TABLE project_daily_summaries ADD COLUMN score INTEGER NOT NULL DEFAULT 0');
|
||||||
|
}
|
||||||
|
|
||||||
function version_62($pdo)
|
function version_62($pdo)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class ProjectDailySummarySubscriber extends Base implements EventSubscriberInter
|
|||||||
{
|
{
|
||||||
return array(
|
return array(
|
||||||
Task::EVENT_CREATE => array('execute', 0),
|
Task::EVENT_CREATE => array('execute', 0),
|
||||||
|
Task::EVENT_UPDATE => array('execute', 0),
|
||||||
Task::EVENT_CLOSE => array('execute', 0),
|
Task::EVENT_CLOSE => array('execute', 0),
|
||||||
Task::EVENT_OPEN => array('execute', 0),
|
Task::EVENT_OPEN => array('execute', 0),
|
||||||
Task::EVENT_MOVE_COLUMN => array('execute', 0),
|
Task::EVENT_MOVE_COLUMN => array('execute', 0),
|
||||||
|
|||||||
34
app/Template/analytic/burndown.php
Normal file
34
app/Template/analytic/burndown.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<div class="page-header">
|
||||||
|
<h2><?= t('Burndown chart') ?></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (! $display_graph): ?>
|
||||||
|
<p class="alert"><?= t('Not enough data to show the graph.') ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<section id="analytic-burndown">
|
||||||
|
<div id="chart" data-url="<?= $this->u('analytic', 'burndown', array('project_id' => $project['id'], 'from' => $values['from'], 'to' => $values['to'])) ?>"></div>
|
||||||
|
</section>
|
||||||
|
<?php endif ?>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<form method="post" class="form-inline" action="<?= $this->u('analytic', 'burndown', array('project_id' => $project['id'])) ?>" autocomplete="off">
|
||||||
|
|
||||||
|
<?= $this->formCsrf() ?>
|
||||||
|
|
||||||
|
<div class="form-inline-group">
|
||||||
|
<?= $this->formLabel(t('Start Date'), 'from') ?>
|
||||||
|
<?= $this->formText('from', $values, array(), array('required', 'placeholder="'.$this->inList($date_format, $date_formats).'"'), 'form-date') ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-inline-group">
|
||||||
|
<?= $this->formLabel(t('End Date'), 'to') ?>
|
||||||
|
<?= $this->formText('to', $values, array(), array('required', 'placeholder="'.$this->inList($date_format, $date_formats).'"'), 'form-date') ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-inline-group">
|
||||||
|
<input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="alert alert-info"><?= t('This chart show the task complexity over the time (Work Remaining).') ?></p>
|
||||||
@@ -10,5 +10,8 @@
|
|||||||
<li>
|
<li>
|
||||||
<?= $this->a(t('Cumulative flow diagram'), 'analytic', 'cfd', array('project_id' => $project['id'])) ?>
|
<?= $this->a(t('Cumulative flow diagram'), 'analytic', 'cfd', array('project_id' => $project['id'])) ?>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<?= $this->a(t('Burndown chart'), 'analytic', 'burndown', array('project_id' => $project['id'])) ?>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,7 +160,8 @@ Kanboard.Calendar=function(){function a(a){var b=$("#calendar").data("save-url")
|
|||||||
a.fullCalendar("addEventSource",b);a.fullCalendar("rerenderEvents")})}function b(a){var b=$("#calendar"),c=b.data("check-url"),d={start:b.fullCalendar("getView").start.format(),end:b.fullCalendar("getView").end.format()};jQuery.extend(d,a);for(var e in d)c+="&"+e+"="+d[e];$.getJSON(c,function(a){b.fullCalendar("removeEvents");b.fullCalendar("addEventSource",a);b.fullCalendar("rerenderEvents")})}function d(){var a=Kanboard.GetStorageItem(f);if(""!==a){var a=JSON.parse(a),c;for(c in a)$("select[name="+
|
a.fullCalendar("addEventSource",b);a.fullCalendar("rerenderEvents")})}function b(a){var b=$("#calendar"),c=b.data("check-url"),d={start:b.fullCalendar("getView").start.format(),end:b.fullCalendar("getView").end.format()};jQuery.extend(d,a);for(var e in d)c+="&"+e+"="+d[e];$.getJSON(c,function(a){b.fullCalendar("removeEvents");b.fullCalendar("addEventSource",a);b.fullCalendar("rerenderEvents")})}function d(){var a=Kanboard.GetStorageItem(f);if(""!==a){var a=JSON.parse(a),c;for(c in a)$("select[name="+
|
||||||
c+"]").val(a[c])}b(a||{});$(".calendar-filter").change(e)}function e(){var a={};$(".calendar-filter").each(function(){a[$(this).attr("name")]=$(this).val()});Kanboard.SetStorageItem(f,JSON.stringify(a));b(a)}var f="";jQuery(document).ready(function(){Kanboard.Exists("calendar")?(f="calendar_filters_"+$("#calendar").data("project-id"),$("#calendar").fullCalendar({lang:$("body").data("js-lang"),editable:!0,eventLimit:!0,defaultView:"month",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},
|
c+"]").val(a[c])}b(a||{});$(".calendar-filter").change(e)}function e(){var a={};$(".calendar-filter").each(function(){a[$(this).attr("name")]=$(this).val()});Kanboard.SetStorageItem(f,JSON.stringify(a));b(a)}var f="";jQuery(document).ready(function(){Kanboard.Exists("calendar")?(f="calendar_filters_"+$("#calendar").data("project-id"),$("#calendar").fullCalendar({lang:$("body").data("js-lang"),editable:!0,eventLimit:!0,defaultView:"month",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},
|
||||||
viewRender:d,eventDrop:a}),d()):Kanboard.Exists("user-calendar")&&$("#user-calendar").fullCalendar({lang:$("body").data("js-lang"),editable:!0,eventLimit:!0,height:Kanboard.Exists("dashboard-calendar")?500:"auto",defaultView:"agendaWeek",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},viewRender:c,eventDrop:a})})}();
|
viewRender:d,eventDrop:a}),d()):Kanboard.Exists("user-calendar")&&$("#user-calendar").fullCalendar({lang:$("body").data("js-lang"),editable:!0,eventLimit:!0,height:Kanboard.Exists("dashboard-calendar")?500:"auto",defaultView:"agendaWeek",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},viewRender:c,eventDrop:a})})}();
|
||||||
Kanboard.Analytic=function(){jQuery(document).ready(function(){Kanboard.Exists("analytic-task-repartition")?Kanboard.Analytic.TaskRepartition.Init():Kanboard.Exists("analytic-user-repartition")?Kanboard.Analytic.UserRepartition.Init():Kanboard.Exists("analytic-cfd")&&Kanboard.Analytic.CFD.Init()});return{}}();
|
Kanboard.Analytic=function(){jQuery(document).ready(function(){Kanboard.Exists("analytic-task-repartition")?Kanboard.Analytic.TaskRepartition.Init():Kanboard.Exists("analytic-user-repartition")?Kanboard.Analytic.UserRepartition.Init():Kanboard.Exists("analytic-cfd")?Kanboard.Analytic.CFD.Init():Kanboard.Exists("analytic-burndown")&&Kanboard.Analytic.Burndown.Init()});return{}}();
|
||||||
|
Kanboard.Analytic.Burndown=function(){return{Init:function(){jQuery.getJSON($("#chart").attr("data-url"),function(a){var c=a.labels,b=a.metrics;a=[];for(var d=0;d<b.length;d++){var e={},f=parseInt(b[d].score);e[c.day]=b[d].day;e[c.score]=f;a.push(e)}b=dimple.newSvg("#chart","100%",380);a=new dimple.chart(b,a);a.addCategoryAxis("x",c.day).addOrderRule("Date");a.addMeasureAxis("y",c.score);a.addSeries(null,dimple.plot.line);a.draw()})}}}();
|
||||||
Kanboard.Analytic.CFD=function(){return{Init:function(){jQuery.getJSON($("#chart").attr("data-url"),function(a){var c=a.labels,b=a.columns,d=a.metrics;a=[];for(var e=0;e<d.length;e++){var f={};f[c.column]=d[e].column_title;f[c.day]=d[e].day;f[c.total]=d[e].total;a.push(f)}d=dimple.newSvg("#chart","100%",380);a=new dimple.chart(d,a);a.addCategoryAxis("x",c.day).addOrderRule("Date");a.addMeasureAxis("y",c.total);a.addSeries(c.column,dimple.plot.area).addOrderRule(b.reverse());a.addLegend(10,10,500,
|
Kanboard.Analytic.CFD=function(){return{Init:function(){jQuery.getJSON($("#chart").attr("data-url"),function(a){var c=a.labels,b=a.columns,d=a.metrics;a=[];for(var e=0;e<d.length;e++){var f={};f[c.column]=d[e].column_title;f[c.day]=d[e].day;f[c.total]=d[e].total;a.push(f)}d=dimple.newSvg("#chart","100%",380);a=new dimple.chart(d,a);a.addCategoryAxis("x",c.day).addOrderRule("Date");a.addMeasureAxis("y",c.total);a.addSeries(c.column,dimple.plot.area).addOrderRule(b.reverse());a.addLegend(10,10,500,
|
||||||
30,"left");a.draw()})}}}();Kanboard.Analytic.TaskRepartition=function(){return{Init:function(){jQuery.getJSON($("#chart").attr("data-url"),function(a){var c=a.labels,b=a.metrics;a=[];for(var d=0;d<b.length;d++){var e={};e[c.nb_tasks]=b[d].nb_tasks;e[c.column_title]=b[d].column_title;a.push(e)}b=dimple.newSvg("#chart","100%",350);a=new dimple.chart(b,a);a.addMeasureAxis("p",c.nb_tasks);a.addSeries(c.column_title,dimple.plot.pie).innerRadius="50%";a.addLegend(0,0,100,"100%","left");a.draw()})}}}();
|
30,"left");a.draw()})}}}();Kanboard.Analytic.TaskRepartition=function(){return{Init:function(){jQuery.getJSON($("#chart").attr("data-url"),function(a){var c=a.labels,b=a.metrics;a=[];for(var d=0;d<b.length;d++){var e={};e[c.nb_tasks]=b[d].nb_tasks;e[c.column_title]=b[d].column_title;a.push(e)}b=dimple.newSvg("#chart","100%",350);a=new dimple.chart(b,a);a.addMeasureAxis("p",c.nb_tasks);a.addSeries(c.column_title,dimple.plot.pie).innerRadius="50%";a.addLegend(0,0,100,"100%","left");a.draw()})}}}();
|
||||||
Kanboard.Analytic.UserRepartition=function(){return{Init:function(){jQuery.getJSON($("#chart").attr("data-url"),function(a){var c=a.labels,b=a.metrics;a=[];for(var d=0;d<b.length;d++){var e={};e[c.nb_tasks]=b[d].nb_tasks;e[c.user]=b[d].user;a.push(e)}b=dimple.newSvg("#chart","100%",350);a=new dimple.chart(b,a);a.addMeasureAxis("p",c.nb_tasks);a.addSeries(c.user,dimple.plot.pie).innerRadius="50%";a.addLegend(0,0,100,"100%","left");a.draw()})}}}();
|
Kanboard.Analytic.UserRepartition=function(){return{Init:function(){jQuery.getJSON($("#chart").attr("data-url"),function(a){var c=a.labels,b=a.metrics;a=[];for(var d=0;d<b.length;d++){var e={};e[c.nb_tasks]=b[d].nb_tasks;e[c.user]=b[d].user;a.push(e)}b=dimple.newSvg("#chart","100%",350);a=new dimple.chart(b,a);a.addMeasureAxis("p",c.nb_tasks);a.addSeries(c.user,dimple.plot.pie).innerRadius="50%";a.addLegend(0,0,100,"100%","left");a.draw()})}}}();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Kanboard.Analytic = (function() {
|
Kanboard.Analytic = (function() {
|
||||||
|
|
||||||
jQuery(document).ready(function() {
|
jQuery(document).ready(function() {
|
||||||
|
|
||||||
if (Kanboard.Exists("analytic-task-repartition")) {
|
if (Kanboard.Exists("analytic-task-repartition")) {
|
||||||
Kanboard.Analytic.TaskRepartition.Init();
|
Kanboard.Analytic.TaskRepartition.Init();
|
||||||
}
|
}
|
||||||
@@ -12,12 +12,62 @@ Kanboard.Analytic = (function() {
|
|||||||
else if (Kanboard.Exists("analytic-cfd")) {
|
else if (Kanboard.Exists("analytic-cfd")) {
|
||||||
Kanboard.Analytic.CFD.Init();
|
Kanboard.Analytic.CFD.Init();
|
||||||
}
|
}
|
||||||
|
else if (Kanboard.Exists("analytic-burndown")) {
|
||||||
|
Kanboard.Analytic.Burndown.Init();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
Kanboard.Analytic.Burndown = (function() {
|
||||||
|
|
||||||
|
function fetchData()
|
||||||
|
{
|
||||||
|
jQuery.getJSON($("#chart").attr("data-url"), function(data) {
|
||||||
|
drawGraph(data.metrics, data.labels);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGraph(metrics, labels)
|
||||||
|
{
|
||||||
|
var series = prepareSeries(metrics, labels);
|
||||||
|
|
||||||
|
var svg = dimple.newSvg("#chart", "100%", 380);
|
||||||
|
var chart = new dimple.chart(svg, series);
|
||||||
|
|
||||||
|
var x = chart.addCategoryAxis("x", labels['day']);
|
||||||
|
x.addOrderRule("Date");
|
||||||
|
|
||||||
|
chart.addMeasureAxis("y", labels['score']);
|
||||||
|
chart.addSeries(null, dimple.plot.line);
|
||||||
|
|
||||||
|
chart.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareSeries(metrics, labels)
|
||||||
|
{
|
||||||
|
var series = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < metrics.length; i++) {
|
||||||
|
|
||||||
|
var row = {};
|
||||||
|
var score = parseInt(metrics[i]['score']);
|
||||||
|
row[labels['day']] = metrics[i]['day'];
|
||||||
|
row[labels['score']] = score;
|
||||||
|
series.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Init: fetchData
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
Kanboard.Analytic.CFD = (function() {
|
Kanboard.Analytic.CFD = (function() {
|
||||||
|
|
||||||
function fetchData()
|
function fetchData()
|
||||||
|
|||||||
43
composer.lock
generated
43
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"hash": "466ee3928d7d3b0bbee15de3b4c76676",
|
"hash": "01ebe465ed3a59d8350670ebd4ef8793",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "christian-riesen/base32",
|
"name": "christian-riesen/base32",
|
||||||
@@ -190,12 +190,12 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/fguillot/picoDb.git",
|
"url": "https://github.com/fguillot/picoDb.git",
|
||||||
"reference": "cd6a571d2de5c0b30d538d7cd6603dc16b25b844"
|
"reference": "35c8d2d3f70b713f66e1dc14c1d6481abe5db3ac"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/fguillot/picoDb/zipball/cd6a571d2de5c0b30d538d7cd6603dc16b25b844",
|
"url": "https://api.github.com/repos/fguillot/picoDb/zipball/35c8d2d3f70b713f66e1dc14c1d6481abe5db3ac",
|
||||||
"reference": "cd6a571d2de5c0b30d538d7cd6603dc16b25b844",
|
"reference": "35c8d2d3f70b713f66e1dc14c1d6481abe5db3ac",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
],
|
],
|
||||||
"description": "Minimalist database query builder",
|
"description": "Minimalist database query builder",
|
||||||
"homepage": "https://github.com/fguillot/picoDb",
|
"homepage": "https://github.com/fguillot/picoDb",
|
||||||
"time": "2015-03-27 02:21:18"
|
"time": "2015-04-12 02:46:43"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "fguillot/simple-validator",
|
"name": "fguillot/simple-validator",
|
||||||
@@ -227,12 +227,12 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/fguillot/simpleValidator.git",
|
"url": "https://github.com/fguillot/simpleValidator.git",
|
||||||
"reference": "5ebdb6df4c5f3aa2539b633eb4ae94c9e8c4ada7"
|
"reference": "41655dc7b9224395f5bb3b5623f6e428fe6d64e8"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/fguillot/simpleValidator/zipball/5ebdb6df4c5f3aa2539b633eb4ae94c9e8c4ada7",
|
"url": "https://api.github.com/repos/fguillot/simpleValidator/zipball/41655dc7b9224395f5bb3b5623f6e428fe6d64e8",
|
||||||
"reference": "5ebdb6df4c5f3aa2539b633eb4ae94c9e8c4ada7",
|
"reference": "41655dc7b9224395f5bb3b5623f6e428fe6d64e8",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
],
|
],
|
||||||
"description": "The most easy to use validator library for PHP :)",
|
"description": "The most easy to use validator library for PHP :)",
|
||||||
"homepage": "https://github.com/fguillot/simpleValidator",
|
"homepage": "https://github.com/fguillot/simpleValidator",
|
||||||
"time": "2015-02-14 21:04:14"
|
"time": "2015-04-05 21:44:06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "fguillot/simpleLogger",
|
"name": "fguillot/simpleLogger",
|
||||||
@@ -495,17 +495,17 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/console",
|
"name": "symfony/console",
|
||||||
"version": "v2.6.5",
|
"version": "v2.6.6",
|
||||||
"target-dir": "Symfony/Component/Console",
|
"target-dir": "Symfony/Component/Console",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/Console.git",
|
"url": "https://github.com/symfony/Console.git",
|
||||||
"reference": "53f86497ccd01677e22435cfb7262599450a90d1"
|
"reference": "5b91dc4ed5eb08553f57f6df04c4730a73992667"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/Console/zipball/53f86497ccd01677e22435cfb7262599450a90d1",
|
"url": "https://api.github.com/repos/symfony/Console/zipball/5b91dc4ed5eb08553f57f6df04c4730a73992667",
|
||||||
"reference": "53f86497ccd01677e22435cfb7262599450a90d1",
|
"reference": "5b91dc4ed5eb08553f57f6df04c4730a73992667",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -549,11 +549,11 @@
|
|||||||
],
|
],
|
||||||
"description": "Symfony Console Component",
|
"description": "Symfony Console Component",
|
||||||
"homepage": "http://symfony.com",
|
"homepage": "http://symfony.com",
|
||||||
"time": "2015-03-13 17:37:22"
|
"time": "2015-03-30 15:54:10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/event-dispatcher",
|
"name": "symfony/event-dispatcher",
|
||||||
"version": "v2.6.5",
|
"version": "v2.6.6",
|
||||||
"target-dir": "Symfony/Component/EventDispatcher",
|
"target-dir": "Symfony/Component/EventDispatcher",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -614,17 +614,17 @@
|
|||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
{
|
{
|
||||||
"name": "symfony/stopwatch",
|
"name": "symfony/stopwatch",
|
||||||
"version": "v2.6.5",
|
"version": "v2.6.6",
|
||||||
"target-dir": "Symfony/Component/Stopwatch",
|
"target-dir": "Symfony/Component/Stopwatch",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/Stopwatch.git",
|
"url": "https://github.com/symfony/Stopwatch.git",
|
||||||
"reference": "ba4e774f71e2ce3e3f65cabac4031b9029972af5"
|
"reference": "5f196e84b5640424a166d2ce9cca161ce1e9d912"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/Stopwatch/zipball/ba4e774f71e2ce3e3f65cabac4031b9029972af5",
|
"url": "https://api.github.com/repos/symfony/Stopwatch/zipball/5f196e84b5640424a166d2ce9cca161ce1e9d912",
|
||||||
"reference": "ba4e774f71e2ce3e3f65cabac4031b9029972af5",
|
"reference": "5f196e84b5640424a166d2ce9cca161ce1e9d912",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -660,7 +660,7 @@
|
|||||||
],
|
],
|
||||||
"description": "Symfony Stopwatch Component",
|
"description": "Symfony Stopwatch Component",
|
||||||
"homepage": "http://symfony.com",
|
"homepage": "http://symfony.com",
|
||||||
"time": "2015-02-24 11:52:21"
|
"time": "2015-03-22 16:55:57"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
@@ -674,7 +674,8 @@
|
|||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": ">=5.3",
|
"php": ">=5.3",
|
||||||
"ext-mbstring": "*"
|
"ext-mbstring": "*",
|
||||||
|
"ext-gd": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": []
|
"platform-dev": []
|
||||||
}
|
}
|
||||||
|
|||||||
33
docs/analytics.markdown
Normal file
33
docs/analytics.markdown
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
Analytics
|
||||||
|
=========
|
||||||
|
|
||||||
|
User repartition
|
||||||
|
----------------
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This pie chart show the number of open tasks assigned per user.
|
||||||
|
|
||||||
|
Task distribution
|
||||||
|
-----------------
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This pie chart gives an overview of the number of open tasks per column.
|
||||||
|
|
||||||
|
Cumulative flow diagram
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This chart show the number of tasks cumulatively for each column over the time.
|
||||||
|
|
||||||
|
Burndown chart
|
||||||
|
--------------
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The [burn down chart](http://en.wikipedia.org/wiki/Burn_down_chart) is available for each project.
|
||||||
|
This chart is a graphical representation of work left to do versus time.
|
||||||
|
|
||||||
|
Kanboard use the complexity or story point to generate this diagram.
|
||||||
55
scripts/create-sample-burndown.php
Executable file
55
scripts/create-sample-burndown.php
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__.'/../app/common.php';
|
||||||
|
|
||||||
|
use Model\ProjectDailySummary;
|
||||||
|
use Model\TaskCreation;
|
||||||
|
use Model\TaskStatus;
|
||||||
|
|
||||||
|
$pds = new ProjectDailySummary($container);
|
||||||
|
$taskCreation = new TaskCreation($container);
|
||||||
|
$taskStatus = new TaskStatus($container);
|
||||||
|
|
||||||
|
for ($i = 1; $i <= 15; $i++) {
|
||||||
|
|
||||||
|
$task = array(
|
||||||
|
'title' => 'Task #'.$i,
|
||||||
|
'project_id' => 1,
|
||||||
|
'column_id' => rand(1, 4),
|
||||||
|
'score' => rand(1, 21)
|
||||||
|
);
|
||||||
|
|
||||||
|
$taskCreation->create($task);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-7 days')));
|
||||||
|
|
||||||
|
$taskStatus->close(1);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-6 days')));
|
||||||
|
|
||||||
|
$taskStatus->close(2);
|
||||||
|
$taskStatus->close(3);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-5 days')));
|
||||||
|
|
||||||
|
$taskStatus->close(4);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-4 days')));
|
||||||
|
|
||||||
|
$taskStatus->close(5);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-3 days')));
|
||||||
|
|
||||||
|
$taskStatus->close(6);
|
||||||
|
$taskStatus->close(7);
|
||||||
|
$taskStatus->close(8);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-2 days')));
|
||||||
|
|
||||||
|
$taskStatus->close(9);
|
||||||
|
$taskStatus->close(10);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-2 days')));
|
||||||
|
|
||||||
|
$taskStatus->close(12);
|
||||||
|
$taskStatus->close(13);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-1 days')));
|
||||||
|
|
||||||
|
$taskStatus->close(1);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d'));
|
||||||
Reference in New Issue
Block a user