Improve dashboard rendering speed by using ajax for graphs. Improve perfs by using more index on query. Add function to find invalid numbers and export as csv
This commit is contained in:
parent
52c849e043
commit
2be8242d5e
|
@ -127,6 +127,23 @@
|
||||||
.mr-4 { margin-right: 1.5rem !important; }
|
.mr-4 { margin-right: 1.5rem !important; }
|
||||||
.mr-5 { margin-right: 3rem !important; }
|
.mr-5 { margin-right: 3rem !important; }
|
||||||
|
|
||||||
|
/* HTML: <div class="loader"></div> */
|
||||||
|
.loader {
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
background:
|
||||||
|
radial-gradient(farthest-side,#999 94%,#0000) top/8px 8px no-repeat,
|
||||||
|
conic-gradient(#0000 30%,#999);
|
||||||
|
-webkit-mask: radial-gradient(farthest-side,#0000 calc(100% - 8px),#000 0);
|
||||||
|
animation: l13 1s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes l13{
|
||||||
|
100%{transform: rotate(1turn)}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/** POPUPS ALERT **/
|
/** POPUPS ALERT **/
|
||||||
.popup-alerts-container
|
.popup-alerts-container
|
||||||
|
|
|
@ -253,9 +253,9 @@ use Exception;
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function get_discussions_for_user(int $id_user)
|
public function get_discussions_for_user(int $id_user, ?int $nb_entry = null, ?int $page = null)
|
||||||
{
|
{
|
||||||
return $this->get_model()->get_discussions_for_user($id_user);
|
return $this->get_model()->get_discussions_for_user($id_user, $nb_entry, $page);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -376,6 +376,22 @@ use Exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of invalid phone number we've sent message to
|
||||||
|
*
|
||||||
|
* @param int $id_user : user id
|
||||||
|
* @param int $volume : Minimum number of sms sent to the number
|
||||||
|
* @param float $percent_failed : Minimum ratio of failed message
|
||||||
|
* @param float $percent_unknown : Minimum ratio of unknown message
|
||||||
|
* @param int $limit : Limit of results
|
||||||
|
* @param int $page : Page of results (offset = page * limit)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function get_invalid_numbers (int $id_user, int $volume, float $percent_failed, float $percent_unknown, int $limit, int $page)
|
||||||
|
{
|
||||||
|
return $this->get_model()->get_invalid_numbers($id_user, $volume, $percent_failed, $percent_unknown, $limit, $page);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the model for the Controller.
|
* Get the model for the Controller.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1189,4 +1189,68 @@ namespace controllers\publics;
|
||||||
|
|
||||||
return $this->json($return);
|
return $this->json($return);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return statistics about invalid numbers
|
||||||
|
*
|
||||||
|
* @param int $page : Pagination number, Default = 0. Group of 25 results.
|
||||||
|
* @param int $_GET['volume'] : Minimum number of SMS sent to the number
|
||||||
|
* @param int $_GET['percent_failed'] : Minimum percentage of failed SMS to the number
|
||||||
|
* @param int $_GET['percent_unknown'] : Minimum percentage of unknown SMS to the number
|
||||||
|
*
|
||||||
|
* @return : List of entries
|
||||||
|
*/
|
||||||
|
public function get_invalid_numbers($page = 0)
|
||||||
|
{
|
||||||
|
$page = (int) $page;
|
||||||
|
$limit = 25;
|
||||||
|
$volume = $_GET['volume'] ?? false;
|
||||||
|
$percent_failed = $_GET['percent_failed'] ?? false;
|
||||||
|
$percent_unknown = $_GET['percent_unknown'] ?? false;
|
||||||
|
|
||||||
|
if ($volume === false || $percent_failed === false || $percent_unknown === false)
|
||||||
|
{
|
||||||
|
$return = self::DEFAULT_RETURN;
|
||||||
|
$return['error'] = self::ERROR_CODES['MISSING_PARAMETER'];
|
||||||
|
$return['message'] = self::ERROR_MESSAGES['MISSING_PARAMETER'] . 'volume, percent_failed and percent_unknown are required.';
|
||||||
|
$this->auto_http_code(false);
|
||||||
|
|
||||||
|
return $this->json($return);
|
||||||
|
}
|
||||||
|
|
||||||
|
$volume = (int) $volume;
|
||||||
|
$percent_failed = ((float) $percent_failed) / 100;
|
||||||
|
$percent_unknown = ((float) $percent_unknown) / 100;
|
||||||
|
|
||||||
|
$return = self::DEFAULT_RETURN;
|
||||||
|
|
||||||
|
$invalid_numbers = $this->internal_sended->get_invalid_numbers($this->user['id'], $volume, $percent_failed, $percent_unknown, $limit, $page);
|
||||||
|
|
||||||
|
$return = self::DEFAULT_RETURN;
|
||||||
|
|
||||||
|
if (\count($invalid_numbers) === $limit)
|
||||||
|
{
|
||||||
|
$return['next'] = \descartes\Router::url('Api', __FUNCTION__, ['page' => $page + 1], [
|
||||||
|
'api_key' => $this->user['api_key'],
|
||||||
|
'volume' => $volume,
|
||||||
|
'percent_failed' => $percent_failed * 100,
|
||||||
|
'percent_unknown' => $percent_unknown * 100
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($page > 0)
|
||||||
|
{
|
||||||
|
$return['prev'] = \descartes\Router::url('Api', __FUNCTION__, ['page' => $page - 1], [
|
||||||
|
'api_key' => $this->user['api_key'],
|
||||||
|
'volume' => $volume,
|
||||||
|
'percent_failed' => $percent_failed * 100,
|
||||||
|
'percent_unknown' => $percent_unknown * 100
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$return['response'] = $invalid_numbers;
|
||||||
|
$this->auto_http_code(true);
|
||||||
|
|
||||||
|
return $this->json($return, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,16 +87,50 @@ namespace controllers\publics;
|
||||||
$stats_start_date_formated = $stats_start_date->format('Y-m-d');
|
$stats_start_date_formated = $stats_start_date->format('Y-m-d');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->render('dashboard/show', [
|
||||||
|
'nb_contacts' => $nb_contacts,
|
||||||
|
'nb_groups' => $nb_groups,
|
||||||
|
'nb_scheduleds' => $nb_scheduleds,
|
||||||
|
'nb_sendeds' => $nb_sendeds,
|
||||||
|
'nb_receiveds' => $nb_receiveds,
|
||||||
|
'nb_unreads' => $nb_unreads,
|
||||||
|
'quota_unused' => $quota_unused,
|
||||||
|
'sendeds' => $sendeds,
|
||||||
|
'receiveds' => $receiveds,
|
||||||
|
'events' => $events,
|
||||||
|
'stats_start_date_formated' => $stats_start_date_formated,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return stats about sended sms
|
||||||
|
*/
|
||||||
|
public function stats_sended()
|
||||||
|
{
|
||||||
|
$id_user = $_SESSION['user']['id'];
|
||||||
|
|
||||||
|
//Création de la date d'il y a 30 jours
|
||||||
|
$now = new \DateTime();
|
||||||
|
$one_month = new \DateInterval('P1M');
|
||||||
|
$stats_start_date = clone $now;
|
||||||
|
$stats_start_date->sub($one_month);
|
||||||
|
$stats_start_date_formated = $stats_start_date->format('Y-m-d');
|
||||||
|
|
||||||
|
//If user have a quota and the quota start before today, use quota start date instead
|
||||||
|
$quota = $this->internal_quota->get_user_quota($id_user);
|
||||||
|
if ($quota && (new \DateTime($quota['start_date']) <= $now) && (new \DateTime($quota['expiration_date']) > $now))
|
||||||
|
{
|
||||||
|
$stats_start_date = new \DateTime($quota['start_date']);
|
||||||
|
$stats_start_date_formated = $stats_start_date->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
$nb_sendeds_by_day = $this->internal_sended->count_by_day_and_status_since_for_user($id_user, $stats_start_date_formated);
|
$nb_sendeds_by_day = $this->internal_sended->count_by_day_and_status_since_for_user($id_user, $stats_start_date_formated);
|
||||||
$nb_receiveds_by_day = $this->internal_received->count_by_day_since_for_user($id_user, $stats_start_date_formated);
|
|
||||||
|
|
||||||
//On va traduire ces données pour les afficher en graphique
|
//On va traduire ces données pour les afficher en graphique
|
||||||
$array_bar_chart_sended = [];
|
$array_bar_chart_sended = [];
|
||||||
$array_bar_chart_received = [];
|
|
||||||
|
|
||||||
$date = clone $stats_start_date;
|
$date = clone $stats_start_date;
|
||||||
$one_day = new \DateInterval('P1D');
|
$one_day = new \DateInterval('P1D');
|
||||||
$i = 0;
|
|
||||||
|
|
||||||
//On va construire un tableau avec la date en clef, et les données pour chaque date
|
//On va construire un tableau avec la date en clef, et les données pour chaque date
|
||||||
while ($date <= $now)
|
while ($date <= $now)
|
||||||
|
@ -109,15 +143,13 @@ namespace controllers\publics;
|
||||||
'sendeds_delivered' => 0,
|
'sendeds_delivered' => 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
$array_bar_chart_received[$date_f] = ['period' => $date_f, 'receiveds' => 0];
|
|
||||||
|
|
||||||
$date->add($one_day);
|
$date->add($one_day);
|
||||||
}
|
}
|
||||||
|
|
||||||
$total_sendeds = 0;
|
$total_sendeds = 0;
|
||||||
$total_receiveds = 0;
|
$total_receiveds = 0;
|
||||||
|
|
||||||
//0n remplie le tableau avec les données adaptées
|
//On remplie le tableau avec les données adaptées
|
||||||
foreach ($nb_sendeds_by_day as $nb_sended)
|
foreach ($nb_sendeds_by_day as $nb_sended)
|
||||||
{
|
{
|
||||||
$array_bar_chart_sended[$nb_sended['at_ymd']]['sendeds_' . $nb_sended['status']] = $nb_sended['nb'];
|
$array_bar_chart_sended[$nb_sended['at_ymd']]['sendeds_' . $nb_sended['status']] = $nb_sended['nb'];
|
||||||
|
@ -125,6 +157,59 @@ namespace controllers\publics;
|
||||||
$total_sendeds += $nb_sended['nb'];
|
$total_sendeds += $nb_sended['nb'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$nb_days = $stats_start_date->diff($now)->days + 1;
|
||||||
|
$avg_sendeds = round($total_sendeds / $nb_days, 2);
|
||||||
|
|
||||||
|
$array_bar_chart_sended = array_values($array_bar_chart_sended);
|
||||||
|
|
||||||
|
header('content-type:application/json');
|
||||||
|
echo json_encode([
|
||||||
|
'data_bar_chart_sended' => $array_bar_chart_sended,
|
||||||
|
'avg_sendeds' => $avg_sendeds,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return stats about received sms
|
||||||
|
*/
|
||||||
|
public function stats_received()
|
||||||
|
{
|
||||||
|
$id_user = $_SESSION['user']['id'];
|
||||||
|
|
||||||
|
//Création de la date d'il y a 30 jours
|
||||||
|
$now = new \DateTime();
|
||||||
|
$one_month = new \DateInterval('P1M');
|
||||||
|
$stats_start_date = clone $now;
|
||||||
|
$stats_start_date->sub($one_month);
|
||||||
|
$stats_start_date_formated = $stats_start_date->format('Y-m-d');
|
||||||
|
|
||||||
|
$quota = $this->internal_quota->get_user_quota($id_user);
|
||||||
|
if ($quota && (new \DateTime($quota['start_date']) <= $now) && (new \DateTime($quota['expiration_date']) > $now))
|
||||||
|
{
|
||||||
|
$stats_start_date = new \DateTime($quota['start_date']);
|
||||||
|
$stats_start_date_formated = $stats_start_date->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
$nb_receiveds_by_day = $this->internal_received->count_by_day_since_for_user($id_user, $stats_start_date_formated);
|
||||||
|
|
||||||
|
//On va traduire ces données pour les afficher en graphique
|
||||||
|
$array_bar_chart_received = [];
|
||||||
|
|
||||||
|
$date = clone $stats_start_date;
|
||||||
|
$one_day = new \DateInterval('P1D');
|
||||||
|
|
||||||
|
//On va construire un tableau avec la date en clef, et les données pour chaque date
|
||||||
|
while ($date <= $now)
|
||||||
|
{
|
||||||
|
$date_f = $date->format('Y-m-d');
|
||||||
|
$array_bar_chart_received[$date_f] = ['period' => $date_f, 'receiveds' => 0];
|
||||||
|
|
||||||
|
$date->add($one_day);
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_receiveds = 0;
|
||||||
|
|
||||||
foreach ($nb_receiveds_by_day as $date => $nb_received)
|
foreach ($nb_receiveds_by_day as $date => $nb_received)
|
||||||
{
|
{
|
||||||
$array_bar_chart_received[$date]['receiveds'] = $nb_received;
|
$array_bar_chart_received[$date]['receiveds'] = $nb_received;
|
||||||
|
@ -132,28 +217,15 @@ namespace controllers\publics;
|
||||||
}
|
}
|
||||||
|
|
||||||
$nb_days = $stats_start_date->diff($now)->days + 1;
|
$nb_days = $stats_start_date->diff($now)->days + 1;
|
||||||
$avg_sendeds = round($total_sendeds / $nb_days, 2);
|
|
||||||
$avg_receiveds = round($total_receiveds / $nb_days, 2);
|
$avg_receiveds = round($total_receiveds / $nb_days, 2);
|
||||||
|
|
||||||
$array_bar_chart_sended = array_values($array_bar_chart_sended);
|
|
||||||
$array_bar_chart_received = array_values($array_bar_chart_received);
|
$array_bar_chart_received = array_values($array_bar_chart_received);
|
||||||
|
|
||||||
$this->render('dashboard/show', [
|
header('content-type:application/json');
|
||||||
'nb_contacts' => $nb_contacts,
|
echo json_encode([
|
||||||
'nb_groups' => $nb_groups,
|
'data_bar_chart_received' => $array_bar_chart_received,
|
||||||
'nb_scheduleds' => $nb_scheduleds,
|
|
||||||
'nb_sendeds' => $nb_sendeds,
|
|
||||||
'nb_receiveds' => $nb_receiveds,
|
|
||||||
'nb_unreads' => $nb_unreads,
|
|
||||||
'avg_sendeds' => $avg_sendeds,
|
|
||||||
'avg_receiveds' => $avg_receiveds,
|
'avg_receiveds' => $avg_receiveds,
|
||||||
'quota_unused' => $quota_unused,
|
|
||||||
'sendeds' => $sendeds,
|
|
||||||
'receiveds' => $receiveds,
|
|
||||||
'events' => $events,
|
|
||||||
'data_bar_chart_sended' => json_encode($array_bar_chart_sended),
|
|
||||||
'data_bar_chart_received' => json_encode($array_bar_chart_received),
|
|
||||||
'stats_start_date_formated' => $stats_start_date_formated,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ namespace controllers\publics;
|
||||||
*/
|
*/
|
||||||
public function list_json()
|
public function list_json()
|
||||||
{
|
{
|
||||||
$entities = $this->internal_received->get_discussions_for_user($_SESSION['user']['id']);
|
$entities = $this->internal_received->get_discussions_for_user($_SESSION['user']['id'], 1000);
|
||||||
|
|
||||||
foreach ($entities as &$entity)
|
foreach ($entities as &$entity)
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
class AddIndexSendedStatus extends AbstractMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Change Method.
|
||||||
|
*
|
||||||
|
* Write your reversible migrations using this method.
|
||||||
|
*
|
||||||
|
* More information on writing migrations is available here:
|
||||||
|
* https://book.cakephp.org/phinx/0/en/migrations.html
|
||||||
|
*
|
||||||
|
* The following commands can be used in this method and Phinx will
|
||||||
|
* automatically reverse them when rolling back:
|
||||||
|
*
|
||||||
|
* createTable
|
||||||
|
* renameTable
|
||||||
|
* addColumn
|
||||||
|
* addCustomColumn
|
||||||
|
* renameColumn
|
||||||
|
* addIndex
|
||||||
|
* addForeignKey
|
||||||
|
*
|
||||||
|
* Any other destructive changes will result in an error when trying to
|
||||||
|
* rollback the migration.
|
||||||
|
*
|
||||||
|
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||||
|
* with the Table class.
|
||||||
|
*/
|
||||||
|
public function change()
|
||||||
|
{
|
||||||
|
$table = $this->table('sended');
|
||||||
|
$table->addIndex(['id_user', 'status']);
|
||||||
|
$table->update();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
class AddIndexDestinationStatusSended extends AbstractMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Change Method.
|
||||||
|
*
|
||||||
|
* Write your reversible migrations using this method.
|
||||||
|
*
|
||||||
|
* More information on writing migrations is available here:
|
||||||
|
* https://book.cakephp.org/phinx/0/en/migrations.html
|
||||||
|
*
|
||||||
|
* The following commands can be used in this method and Phinx will
|
||||||
|
* automatically reverse them when rolling back:
|
||||||
|
*
|
||||||
|
* createTable
|
||||||
|
* renameTable
|
||||||
|
* addColumn
|
||||||
|
* addCustomColumn
|
||||||
|
* renameColumn
|
||||||
|
* addIndex
|
||||||
|
* addForeignKey
|
||||||
|
*
|
||||||
|
* Any other destructive changes will result in an error when trying to
|
||||||
|
* rollback the migration.
|
||||||
|
*
|
||||||
|
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||||
|
* with the Table class.
|
||||||
|
*/
|
||||||
|
public function change()
|
||||||
|
{
|
||||||
|
$table = $this->table('sended');
|
||||||
|
$table->addIndex(['destination', 'status']);
|
||||||
|
$table->update();
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,7 +74,7 @@ namespace models;
|
||||||
WHERE
|
WHERE
|
||||||
total >= :min_volume
|
total >= :min_volume
|
||||||
AND
|
AND
|
||||||
(unreliable / total) > :rate_limit;
|
(unreliable / total) >= :rate_limit;
|
||||||
", [
|
", [
|
||||||
'id_user' => $id_user,
|
'id_user' => $id_user,
|
||||||
'sms_status' => $sms_status,
|
'sms_status' => $sms_status,
|
||||||
|
|
|
@ -267,27 +267,33 @@ namespace models;
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function get_discussions_for_user(int $id_user)
|
public function get_discussions_for_user(int $id_user, ?int $nb_entry = null, ?int $page = null)
|
||||||
{
|
{
|
||||||
$query = '
|
$query = '
|
||||||
SELECT discussions.at, discussions.number, contact.name as contact_name
|
SELECT at, destination AS number, contact.name AS contact_name
|
||||||
FROM (
|
FROM sended
|
||||||
SELECT at, destination as number FROM sended
|
LEFT JOIN contact ON contact.number = sended.destination
|
||||||
WHERE id_user = :id_user
|
WHERE sended.id_user = :id_user
|
||||||
UNION (
|
|
||||||
SELECT at, origin as number FROM received
|
UNION ALL
|
||||||
WHERE id_user = :id_user
|
|
||||||
)
|
SELECT at, origin AS number, contact.name AS contact_name
|
||||||
) as discussions
|
FROM received
|
||||||
LEFT JOIN contact
|
LEFT JOIN contact ON contact.number = received.origin
|
||||||
ON discussions.number = contact.number AND id_user = :id_user
|
WHERE received.id_user = :id_user
|
||||||
GROUP BY number
|
|
||||||
ORDER BY at DESC
|
ORDER BY at DESC
|
||||||
';
|
';
|
||||||
|
|
||||||
$params = ['id_user' => $id_user];
|
$params = ['id_user' => $id_user];
|
||||||
|
|
||||||
return $this->_run_query($query, $params);
|
if ($nb_entry !== null)
|
||||||
|
{
|
||||||
|
$query .= 'LIMIT ' . intval($nb_entry) * intval($page) . ', ' . intval($nb_entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = $this->_run_query($query, $params);
|
||||||
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -338,6 +338,45 @@ namespace models;
|
||||||
return $this->_run_query($query, $params);
|
return $this->_run_query($query, $params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of invalid phone number we've sent message to
|
||||||
|
*
|
||||||
|
* @param int $id_user : user id
|
||||||
|
* @param int $volume : Minimum number of sms sent to the number
|
||||||
|
* @param float $percent_failed : Minimum ratio of failed message
|
||||||
|
* @param float $percent_unknown : Minimum ratio of unknown message
|
||||||
|
* @param int $limit : Limit of results
|
||||||
|
* @param int $page : Page of results (offset = page * limit)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function get_invalid_numbers (int $id_user, int $volume, float $percent_failed, float $percent_unknown, int $limit, int $page)
|
||||||
|
{
|
||||||
|
$query = "
|
||||||
|
SELECT
|
||||||
|
destination,
|
||||||
|
COUNT(*) AS total_sms_sent,
|
||||||
|
ROUND(SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) / COUNT(*), 2) AS failed_percentage,
|
||||||
|
ROUND(SUM(CASE WHEN status = 'unknown' THEN 1 ELSE 0 END) / COUNT(*), 2) AS unknown_percentage
|
||||||
|
FROM
|
||||||
|
sended
|
||||||
|
GROUP BY
|
||||||
|
destination
|
||||||
|
HAVING
|
||||||
|
total_sms_sent >= :volume
|
||||||
|
AND failed_percentage >= :percent_failed
|
||||||
|
AND unknown_percentage >= :percent_unknown
|
||||||
|
LIMIT " . intval($page * $limit) . "," . intval($limit) . "
|
||||||
|
";
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'volume' => $volume,
|
||||||
|
'percent_failed' => $percent_failed,
|
||||||
|
'percent_unknown' => $percent_unknown
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->_run_query($query, $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return table name.
|
* Return table name.
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
|
|
||||||
'Dashboard' => [
|
'Dashboard' => [
|
||||||
'show' => '/dashboard/',
|
'show' => '/dashboard/',
|
||||||
|
'stats_sended' => '/dashboard/stats/sended.json/',
|
||||||
|
'stats_received' => '/dashboard/stats/received.json/',
|
||||||
],
|
],
|
||||||
|
|
||||||
'Account' => [
|
'Account' => [
|
||||||
|
@ -215,6 +217,10 @@
|
||||||
],
|
],
|
||||||
'get_usage' => '/api/usage/',
|
'get_usage' => '/api/usage/',
|
||||||
'get_sms_status_stats' => '/api/stats/sms-status/',
|
'get_sms_status_stats' => '/api/stats/sms-status/',
|
||||||
|
'get_invalid_numbers' => [
|
||||||
|
'/api/invalid_number/',
|
||||||
|
'/api/invalid_number/{page}/',
|
||||||
|
],
|
||||||
'post_scheduled' => [
|
'post_scheduled' => [
|
||||||
'/api/scheduled/',
|
'/api/scheduled/',
|
||||||
],
|
],
|
||||||
|
|
|
@ -121,13 +121,14 @@
|
||||||
<div class="panel panel-default dashboard-panel-chart">
|
<div class="panel panel-default dashboard-panel-chart">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title"><i class="fa fa-area-chart fa-fw"></i> SMS envoyés depuis le <?= $stats_start_date_formated; ?> : </h3>
|
<h3 class="panel-title"><i class="fa fa-area-chart fa-fw"></i> SMS envoyés depuis le <?= $stats_start_date_formated; ?> : </h3>
|
||||||
<span style="color: #5CB85C;">SMS envoyés (moyenne = <?php echo $avg_sendeds; ?> par jour).</span><br/>
|
<span style="color: #5CB85C;">SMS envoyés (moyenne = <span id="avg_sendeds">0</span> par jour).</span><br/>
|
||||||
<?php if ($quota_unused) { ?>
|
<?php if ($quota_unused) { ?>
|
||||||
<br/>
|
<br/>
|
||||||
<span style="color: #d9534f">Crédits restants : <?= $quota_unused; ?>.</span>
|
<span style="color: #d9534f">Crédits restants : <?= $quota_unused; ?>.</span>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
<div id="morris-bar-chart-sended-loader" class="text-center"><div class="loader"></div></div>
|
||||||
<div id="morris-bar-chart-sended"></div>
|
<div id="morris-bar-chart-sended"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -139,9 +140,10 @@
|
||||||
<div class="panel panel-default dashboard-panel-chart">
|
<div class="panel panel-default dashboard-panel-chart">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title"><i class="fa fa-area-chart fa-fw"></i> SMS reçus depuis le <?= $stats_start_date_formated; ?> : </h3>
|
<h3 class="panel-title"><i class="fa fa-area-chart fa-fw"></i> SMS reçus depuis le <?= $stats_start_date_formated; ?> : </h3>
|
||||||
<span style="color: #EDAB4D">SMS reçus (moyenne = <?php echo $avg_receiveds; ?> par jour).</span>
|
<span style="color: #EDAB4D">SMS reçus (moyenne = <span id="avg_receiveds">0</span> par jour).</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
<div id="morris-bar-chart-received-loader" class="text-center"><div class="loader"></div></div>
|
||||||
<div id="morris-bar-chart-received"></div>
|
<div id="morris-bar-chart-received"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -255,18 +257,23 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
jQuery(document).ready(function()
|
async function drawChartSended() {
|
||||||
{
|
let url = <?= json_encode(\descartes\Router::url('Dashboard', 'stats_sended'))?>;
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('avg_sendeds').textContent = data.avg_sendeds;
|
||||||
|
|
||||||
Morris.Bar({
|
Morris.Bar({
|
||||||
element: 'morris-bar-chart-sended',
|
element: 'morris-bar-chart-sended',
|
||||||
fillOpacity: 0.4,
|
fillOpacity: 0.4,
|
||||||
data: <?php echo $data_bar_chart_sended;?>,
|
data: data.data_bar_chart_sended,
|
||||||
xkey: 'period',
|
xkey: 'period',
|
||||||
parseTime: false,
|
parseTime: false,
|
||||||
ykeys: ['sendeds_failed', 'sendeds_unknown', 'sendeds_delivered'],
|
ykeys: ['sendeds_failed', 'sendeds_unknown', 'sendeds_delivered'],
|
||||||
labels: ['SMS échoués', 'SMS inconnus', 'SMS délivrés'],
|
labels: ['SMS échoués', 'SMS inconnus', 'SMS délivrés'],
|
||||||
barColors: ['#D9534F', '#337AB7', '#5CB85C'],
|
barColors: ['#D9534F', '#337AB7', '#5CB85C'],
|
||||||
goals: [<?php echo $avg_sendeds; ?>,],
|
goals: [data.avg_sendeds],
|
||||||
goalLineColors: ['#5CB85C'],
|
goalLineColors: ['#5CB85C'],
|
||||||
goalStrokeWidth: 2,
|
goalStrokeWidth: 2,
|
||||||
pointSize: 4,
|
pointSize: 4,
|
||||||
|
@ -290,22 +297,42 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('morris-bar-chart-sended-loader').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drawChartReceived() {
|
||||||
|
let url = <?= json_encode(\descartes\Router::url('Dashboard', 'stats_received'))?>;
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById('avg_receiveds').textContent = data.avg_receiveds;
|
||||||
Morris.Bar({
|
Morris.Bar({
|
||||||
element: 'morris-bar-chart-received',
|
element: 'morris-bar-chart-received',
|
||||||
fillOpacity: 0.4,
|
fillOpacity: 0.4,
|
||||||
data: <?php echo $data_bar_chart_received;?>,
|
data: data.data_bar_chart_received,
|
||||||
xkey: 'period',
|
xkey: 'period',
|
||||||
parseTime: false,
|
parseTime: false,
|
||||||
ykeys: ['receiveds'],
|
ykeys: ['receiveds'],
|
||||||
labels: ['SMS reçus'],
|
labels: ['SMS reçus'],
|
||||||
barColors: ['#EDAB4D'],
|
barColors: ['#EDAB4D'],
|
||||||
goals: [<?php echo $avg_receiveds; ?>],
|
goals: [data.avg_receiveds],
|
||||||
goalLineColors: ['#EDAB4D'],
|
goalLineColors: ['#EDAB4D'],
|
||||||
goalStrokeWidth: 2,
|
goalStrokeWidth: 2,
|
||||||
pointSize: 4,
|
pointSize: 4,
|
||||||
hideHover: 'auto',
|
hideHover: 'auto',
|
||||||
resize: true,
|
resize: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('morris-bar-chart-received-loader').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
jQuery(document).ready(function()
|
||||||
|
{
|
||||||
|
drawChartSended();
|
||||||
|
drawChartReceived();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<!-- /#wrapper -->
|
<!-- /#wrapper -->
|
||||||
|
|
|
@ -64,7 +64,7 @@ jQuery(document).ready(function ()
|
||||||
{
|
{
|
||||||
jQuery('.datatable').DataTable({
|
jQuery('.datatable').DataTable({
|
||||||
"pageLength": 25,
|
"pageLength": 25,
|
||||||
"lengthMenu": [[25, 50, 100, 1000, 10000, -1], [25, 50, 100, 1000, 10000, "All"]],
|
"lengthMenu": [[25, 50, 100, 1000], [25, 50, 100, 1000]],
|
||||||
"language": {
|
"language": {
|
||||||
"url": HTTP_PWD + "/assets/js/datatables/french.json",
|
"url": HTTP_PWD + "/assets/js/datatables/french.json",
|
||||||
},
|
},
|
||||||
|
@ -73,7 +73,6 @@ jQuery(document).ready(function ()
|
||||||
'targets': 'checkcolumn',
|
'targets': 'checkcolumn',
|
||||||
'orderable': false,
|
'orderable': false,
|
||||||
}],
|
}],
|
||||||
|
|
||||||
"ajax": {
|
"ajax": {
|
||||||
'url': '<?php echo \descartes\Router::url('Discussion', 'list_json'); ?>',
|
'url': '<?php echo \descartes\Router::url('Discussion', 'list_json'); ?>',
|
||||||
'dataSrc': 'data',
|
'dataSrc': 'data',
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<h1 class="page-header">
|
<h1 class="page-header">
|
||||||
Dashboard <small>SMS envoyés</small>
|
Dashboard <small>SMS envoyés</small>
|
||||||
|
<a class="btn btn-warning float-right" id="btn-invalid-numbers" href="#"><span class="fa fa-eraser"></span> Télécharger les numéros invalides</a>
|
||||||
</h1>
|
</h1>
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li>
|
<li>
|
||||||
|
@ -65,9 +66,122 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" tabindex="-1" id="invalid-numbers-modal">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="invalid-numbers-form" action="<?php $this->s(\descartes\Router::url('Api', 'get_invalid_numbers')); ?>" method="GET">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
<h4 class="modal-title">Télécharger les numéros invalides</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="help">Vous pouvez téléchager une liste de destinataires qui affichent un taux d'erreur anormal selon les critères de votre choix (liste limitée à 25 000 numéros).</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Volume minimum de SMS envoyés au numéros</label>
|
||||||
|
<div class="form-group input-group">
|
||||||
|
<span class="input-group-addon"><span class="fa fa-arrow-circle-up"></span></span>
|
||||||
|
<input name="volume" class="form-control" type="number" min="1" step="1" placeholder="" autofocus required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Pourcentage d'échecs minimum</label>
|
||||||
|
<div class="form-group input-group">
|
||||||
|
<span class="input-group-addon"><span class="fa fa-percent"></span></span>
|
||||||
|
<input name="percent_failed" class="form-control" type="number" min="0" step="1" placeholder="" autofocus required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Pourcentage d'inconnus minimum</label>
|
||||||
|
<div class="form-group input-group">
|
||||||
|
<span class="input-group-addon"><span class="fa fa-percent"></span></span>
|
||||||
|
<input name="percent_unknown" class="form-control" type="number" min="0" step="1" placeholder="" autofocus required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="invalid-numbers-loader" class="text-center hidden"><div class="loader"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a type="button" class="btn btn-danger" data-dismiss="modal">Annuler</a>
|
||||||
|
<input type="submit" class="btn btn-success" value="Valider" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
jQuery(document).ready(function ()
|
jQuery(document).ready(function ()
|
||||||
{
|
{
|
||||||
|
jQuery('body').on('click', '#btn-invalid-numbers', function ()
|
||||||
|
{
|
||||||
|
jQuery('#invalid-numbers-modal').modal({'keyboard': true});
|
||||||
|
});
|
||||||
|
|
||||||
|
jQuery('body').on('submit', '#invalid-numbers-form', function (e)
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
jQuery('#invalid-numbers-loader').removeClass('hidden');
|
||||||
|
|
||||||
|
const form = this;
|
||||||
|
const formData = jQuery(form).serialize();
|
||||||
|
|
||||||
|
let invalidNumbers = []; // Array to store cumulative results
|
||||||
|
|
||||||
|
// Function to fetch data and handle pagination
|
||||||
|
const fetchData = (url, limit = -1, params = null) => {
|
||||||
|
if (params) {
|
||||||
|
url += '?' + params;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(jsonResponse => {
|
||||||
|
invalidNumbers = invalidNumbers.concat(jsonResponse.response);
|
||||||
|
|
||||||
|
// Check if there is a "next" URL to fetch more data
|
||||||
|
if (jsonResponse.next && limit != 0) {
|
||||||
|
fetchData(jsonResponse.next, limit - 1); // Recursive call for next page
|
||||||
|
} else {
|
||||||
|
exportToCSV(invalidNumbers);
|
||||||
|
jQuery('#invalid-numbers-loader').addClass('hidden');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('There was a problem with the fetch operation:', error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to export data to CSV
|
||||||
|
const exportToCSV = (results) => {
|
||||||
|
// Define the CSV headers
|
||||||
|
let csvContent = "Destination,Total SMS Sent,Failed Percentage,Unknown Percentage\n";
|
||||||
|
|
||||||
|
// Append each row of data to the CSV content
|
||||||
|
results.forEach(item => {
|
||||||
|
csvContent += `${item.destination},${item.total_sms_sent},${item.failed_percentage},${item.unknown_percentage}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a downloadable link for the CSV file
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const downloadLink = document.createElement('a');
|
||||||
|
downloadLink.href = url;
|
||||||
|
downloadLink.download = 'invalid_numbers.csv';
|
||||||
|
|
||||||
|
// Trigger download
|
||||||
|
document.body.appendChild(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
document.body.removeChild(downloadLink); // Clean up
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial call to fetch data
|
||||||
|
fetchData(form.action, 1000, formData);
|
||||||
|
});
|
||||||
|
|
||||||
jQuery('.datatable').DataTable({
|
jQuery('.datatable').DataTable({
|
||||||
"pageLength": 25,
|
"pageLength": 25,
|
||||||
"lengthMenu": [[25, 50, 100, 1000, 10000, Math.pow(10, 10)], [25, 50, 100, 1000, 10000, "All"]],
|
"lengthMenu": [[25, 50, 100, 1000, 10000, Math.pow(10, 10)], [25, 50, 100, 1000, 10000, "All"]],
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
<input type="submit" class="btn btn-success ml-4" value="Valider" />
|
<input type="submit" class="btn btn-success ml-4" value="Valider" />
|
||||||
</form>
|
</form>
|
||||||
<canvas id="bar-chart-sms-status"></canvas>
|
<canvas id="bar-chart-sms-status"></canvas>
|
||||||
|
<div id="bar-chart-sms-status-loader" class="text-center mb-5"><div class="loader"></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -81,9 +82,8 @@
|
||||||
const formatedEndDate = endDate.toISOString().split('T')[0]
|
const formatedEndDate = endDate.toISOString().split('T')[0]
|
||||||
const id_phone = document.getElementById('id_phone').value;
|
const id_phone = document.getElementById('id_phone').value;
|
||||||
|
|
||||||
const query_infos = <?= json_encode(['url' => \descartes\Router::url('Api', 'get_sms_status_stats')])?>;
|
let url = <?= json_encode(\descartes\Router::url('Api', 'get_sms_status_stats'))?>;
|
||||||
|
url += `?start=${formatedStartDate}&end=${formatedEndDate}`;
|
||||||
let url = `${query_infos.url}?start=${formatedStartDate}&end=${formatedEndDate}`;
|
|
||||||
url += id_phone ? `&id_phone=${id_phone}` : '';
|
url += id_phone ? `&id_phone=${id_phone}` : '';
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const data = (await response.json()).response;
|
const data = (await response.json()).response;
|
||||||
|
@ -187,6 +187,8 @@
|
||||||
plugins: [noDataPlugin],
|
plugins: [noDataPlugin],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
document.getElementById('bar-chart-sms-status-loader').classList.add('hidden');
|
||||||
|
|
||||||
// On first run create chart, after update
|
// On first run create chart, after update
|
||||||
if (!smsStatusChart) {
|
if (!smsStatusChart) {
|
||||||
smsStatusChart = new Chart(ctx, config);
|
smsStatusChart = new Chart(ctx, config);
|
||||||
|
|
Loading…
Reference in New Issue