diff --git a/assets/css/style.css b/assets/css/style.css index 86adda6..fc15815 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -127,6 +127,23 @@ .mr-4 { margin-right: 1.5rem !important; } .mr-5 { margin-right: 3rem !important; } +/* HTML:
*/ +.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 **/ .popup-alerts-container diff --git a/controllers/internals/Received.php b/controllers/internals/Received.php index 8c96abf..6bace82 100644 --- a/controllers/internals/Received.php +++ b/controllers/internals/Received.php @@ -253,9 +253,9 @@ use Exception; * * @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); } /** diff --git a/controllers/internals/Sended.php b/controllers/internals/Sended.php index 393dbd9..266ca49 100644 --- a/controllers/internals/Sended.php +++ b/controllers/internals/Sended.php @@ -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. */ diff --git a/controllers/publics/Api.php b/controllers/publics/Api.php index be9cc23..40afc8e 100644 --- a/controllers/publics/Api.php +++ b/controllers/publics/Api.php @@ -1189,4 +1189,68 @@ namespace controllers\publics; 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); + } } diff --git a/controllers/publics/Dashboard.php b/controllers/publics/Dashboard.php index bc7b3ed..fec39b8 100644 --- a/controllers/publics/Dashboard.php +++ b/controllers/publics/Dashboard.php @@ -87,16 +87,50 @@ namespace controllers\publics; $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_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_sended = []; - $array_bar_chart_received = []; $date = clone $stats_start_date; $one_day = new \DateInterval('P1D'); - $i = 0; //On va construire un tableau avec la date en clef, et les données pour chaque date while ($date <= $now) @@ -109,15 +143,13 @@ namespace controllers\publics; 'sendeds_delivered' => 0, ]; - $array_bar_chart_received[$date_f] = ['period' => $date_f, 'receiveds' => 0]; - $date->add($one_day); } $total_sendeds = 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) { $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']; } + $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) { $array_bar_chart_received[$date]['receiveds'] = $nb_received; @@ -132,28 +217,15 @@ namespace controllers\publics; } $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); - $array_bar_chart_sended = array_values($array_bar_chart_sended); $array_bar_chart_received = array_values($array_bar_chart_received); - $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, - 'avg_sendeds' => $avg_sendeds, + header('content-type:application/json'); + echo json_encode([ + 'data_bar_chart_received' => $array_bar_chart_received, '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, ]); } + } diff --git a/controllers/publics/Discussion.php b/controllers/publics/Discussion.php index 77e6829..37d60e8 100644 --- a/controllers/publics/Discussion.php +++ b/controllers/publics/Discussion.php @@ -56,7 +56,7 @@ namespace controllers\publics; */ 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) { diff --git a/db/migrations/20241026162247_add_index_sended_status.php b/db/migrations/20241026162247_add_index_sended_status.php new file mode 100644 index 0000000..786b5e7 --- /dev/null +++ b/db/migrations/20241026162247_add_index_sended_status.php @@ -0,0 +1,38 @@ +table('sended'); + $table->addIndex(['id_user', 'status']); + $table->update(); + } +} diff --git a/db/migrations/20241027140954_add_index_destination_status_sended.php b/db/migrations/20241027140954_add_index_destination_status_sended.php new file mode 100644 index 0000000..7693ebd --- /dev/null +++ b/db/migrations/20241027140954_add_index_destination_status_sended.php @@ -0,0 +1,38 @@ +table('sended'); + $table->addIndex(['destination', 'status']); + $table->update(); + } +} diff --git a/models/PhoneReliabilityHistory.php b/models/PhoneReliabilityHistory.php index a9b3c5d..3da6507 100644 --- a/models/PhoneReliabilityHistory.php +++ b/models/PhoneReliabilityHistory.php @@ -74,7 +74,7 @@ namespace models; WHERE total >= :min_volume AND - (unreliable / total) > :rate_limit; + (unreliable / total) >= :rate_limit; ", [ 'id_user' => $id_user, 'sms_status' => $sms_status, diff --git a/models/Received.php b/models/Received.php index 66fc4b4..f9f90cf 100644 --- a/models/Received.php +++ b/models/Received.php @@ -267,27 +267,33 @@ namespace models; * * @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 = ' - SELECT discussions.at, discussions.number, contact.name as contact_name - FROM ( - SELECT at, destination as number FROM sended - WHERE id_user = :id_user - UNION ( - SELECT at, origin as number FROM received - WHERE id_user = :id_user - ) - ) as discussions - LEFT JOIN contact - ON discussions.number = contact.number AND id_user = :id_user - GROUP BY number - ORDER BY at DESC + SELECT at, destination AS number, contact.name AS contact_name + FROM sended + LEFT JOIN contact ON contact.number = sended.destination + WHERE sended.id_user = :id_user + + UNION ALL + + SELECT at, origin AS number, contact.name AS contact_name + FROM received + LEFT JOIN contact ON contact.number = received.origin + WHERE received.id_user = :id_user + + ORDER BY at DESC '; $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; } /** diff --git a/models/Sended.php b/models/Sended.php index cba178c..6878147 100644 --- a/models/Sended.php +++ b/models/Sended.php @@ -338,6 +338,45 @@ namespace models; 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. diff --git a/routes.php b/routes.php index 9265e38..e3746db 100644 --- a/routes.php +++ b/routes.php @@ -11,6 +11,8 @@ 'Dashboard' => [ 'show' => '/dashboard/', + 'stats_sended' => '/dashboard/stats/sended.json/', + 'stats_received' => '/dashboard/stats/received.json/', ], 'Account' => [ @@ -215,6 +217,10 @@ ], 'get_usage' => '/api/usage/', 'get_sms_status_stats' => '/api/stats/sms-status/', + 'get_invalid_numbers' => [ + '/api/invalid_number/', + '/api/invalid_number/{page}/', + ], 'post_scheduled' => [ '/api/scheduled/', ], diff --git a/templates/dashboard/show.php b/templates/dashboard/show.php index 137ec52..94af42b 100644 --- a/templates/dashboard/show.php +++ b/templates/dashboard/show.php @@ -121,13 +121,14 @@