Merge pull request #463 from allerta-vvf/master

Alert improvements
This commit is contained in:
Matteo Gheza 2022-03-13 00:40:09 +01:00 committed by GitHub
commit 05bc474875
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 304 additions and 48 deletions

View File

@ -1,14 +1,113 @@
<?php
require_once 'utils.php';
final class NoChiefAvailableException extends Exception {}
final class NoDriverAvailableException extends Exception {}
final class NotEnoughAvailableUsersException extends Exception {}
function callsList($type) {
global $db;
$crew = [];
if ($type == 'full') {
} else if ($type == 'support') {
if($db->selectValue("SELECT COUNT(id) FROM `allerta01_profiles` WHERE `available` = 1") < 2) {
throw new NotEnoughAvailableUsersException();
return;
}
$chief_result = $db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0 AND `available` = 1 AND `chief` = 1 ORDER BY services ASC, trainings DESC, availability_minutes ASC, name ASC LIMIT 1");
if(is_null($chief_result)) {
throw new NoChiefAvailableException();
return;
}
$crew[] = $chief_result;
if($chief_result["driver"]) {
$result = $db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0 AND `available` = 1 ORDER BY chief ASC, services ASC, trainings DESC, availability_minutes ASC, name ASC LIMIT 1");
$crew[] = $result;
} else {
$driver_result = $db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0 AND `available` = 1 AND `driver` = 1 ORDER BY chief ASC, services ASC, trainings DESC, availability_minutes ASC, name ASC LIMIT 1");
if(is_null($driver_result)) {
throw new NoDriverAvailableException();
return;
}
$crew[] = $driver_result;
}
}
return $crew;
}
function loadCrewMemberData($input) {
global $db;
$result = $db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$input["id"]]);
if(is_null($result)) {
throw new Exception("Crew member not found");
return;
}
return array_merge($input, $result);
}
function setAlertResponse($response, $userId, $alertId) {
global $db, $Bot;
$alert = $db->selectRow(
"SELECT * FROM `".DB_PREFIX."_alerts` WHERE `id` = ?", [$alertId]
);
$crew = json_decode($alert["crew"], true);
$messageText = $response ? "🟢 Partecipazione accettata." : "🔴 Partecipazione rifiutata.";
foreach($crew as &$member) {
if($member["id"] == $userId) {
$message_id = $member["telegram_message_id"];
$chat_id = $member["telegram_chat_id"];
$Bot->sendMessage([
"chat_id" => $chat_id,
"text" => $messageText,
"reply_to_message_id" => $message_id
]);
$Bot->editMessageReplyMarkup([
"chat_id" => $chat_id,
"message_id" => $message_id,
"reply_markup" => [
'inline_keyboard' => [
]
]
]);
$member["response"] = $response;
$member["response_time"] = get_timestamp();
break;
}
}
$db->update(
DB_PREFIX."_alerts",
[
"crew" => json_encode($crew)
],
[
"id" => $alertId
]
);
$notification_messages = json_decode($alert["notification_messages"], true);
$notification_text = generateAlertReportMessage($alert["type"], $crew);
foreach($notification_messages as $chat_id => $message_id) {
$Bot->editMessageText([
"chat_id" => $chat_id,
"message_id" => $message_id,
"text" => $notification_text
]);
}
}
function alertsRouter (FastRoute\RouteCollector $r) {
$r->addRoute(
'GET',
'',
function ($vars) {
global $db;
global $db, $users;
requireLogin();
$alerts = $db->select("SELECT * FROM `".DB_PREFIX."_alerts`");
$alerts = $db->select("SELECT * FROM `".DB_PREFIX."_alerts` WHERE `enabled` = 1");
if(is_null($alerts)) $alerts = [];
foreach($alerts as &$alert) {
if(isset($_GET["load_less"])) {
@ -18,6 +117,9 @@ function alertsRouter (FastRoute\RouteCollector $r) {
];
} else {
$alert["crew"] = json_decode($alert["crew"], true);
$alert["crew"] = array_map(function($crew_member) {
return loadCrewMemberData($crew_member);
}, $alert["crew"]);
}
}
apiResponse($alerts);
@ -32,34 +134,62 @@ function alertsRouter (FastRoute\RouteCollector $r) {
requireLogin();
$users->online_time_update();
if(!$users->hasRole(Role::SUPER_EDITOR)) {
apiResponse(["error" => "access denied"]);
apiResponse(["status" => "error", "message" => "Access denied"]);
return;
}
$crew = [
[
"name" => "Nome1",
try {
$crew_members = callsList($_POST["type"]);
} catch (NoChiefAvailableException) {
apiResponse(["status" => "error", "message" => "Nessun caposquadra disponibile. Contattare i vigili manualmente."]);
return;
} catch (NoDriverAvailableException) {
apiResponse(["status" => "error", "message" => "Nessun autista disponibile. Contattare i vigili manualmente."]);
return;
} catch (NotEnoughAvailableUsersException) {
apiResponse(["status" => "error", "message" => "Nessun utente disponibile. Distaccamento non operativo."]);
return;
}
$crew = [];
foreach($crew_members as $member) {
$crew[] = [
"id" => $member["id"],
"response" => "waiting"
],
[
"name" => "Nome2",
"response" => true
],
[
"name" => "Nome3",
"response" => false
]
];
];
}
$notifications = sendAlertReportMessage($_POST["type"], $crew);
$db->insert(
DB_PREFIX."_alerts",
[
"crew" => json_encode($crew),
"type" => $_POST["type"],
"created_at" => get_timestamp()
"created_at" => get_timestamp(),
"created_by" => $users->auth->getUserId(),
"notification_messages" => json_encode($notifications)
]
);
$alertId = $db->getLastInsertId();
foreach($crew as &$member) {
list($member["telegram_message_id"], $member["telegram_chat_id"]) = sendAlertRequestMessage($_POST["type"], $member["id"], $alertId);
}
$db->update(
DB_PREFIX."_alerts",
[
"crew" => json_encode($crew)
],
[
"id" => $alertId
]
);
apiResponse([
"crew" => $crew,
"id" => $db->getLastInsertId()
"id" => $alertId
]);
}
);
@ -76,6 +206,9 @@ function alertsRouter (FastRoute\RouteCollector $r) {
return;
}
$alert["crew"] = json_decode($alert["crew"], true);
$alert["crew"] = array_map(function($crew_member) {
return loadCrewMemberData($crew_member);
}, $alert["crew"]);
apiResponse($alert);
}
);
@ -88,7 +221,7 @@ function alertsRouter (FastRoute\RouteCollector $r) {
requireLogin();
$users->online_time_update();
if(!$users->hasRole(Role::SUPER_EDITOR)) {
apiResponse(["error" => "access denied"]);
apiResponse(["status" => "error", "message" => "Access denied"]);
return;
}
$db->update(
@ -111,11 +244,14 @@ function alertsRouter (FastRoute\RouteCollector $r) {
requireLogin();
$users->online_time_update();
if(!$users->hasRole(Role::SUPER_EDITOR)) {
apiResponse(["error" => "access denied"]);
apiResponse(["status" => "error", "message" => "Access denied"]);
return;
}
$db->delete(
$db->update(
DB_PREFIX."_alerts",
[
"enabled" => 0
],
[
"id" => $vars["id"]
]

View File

@ -231,7 +231,7 @@ function apiRouter (FastRoute\RouteCollector $r) {
if(!$users->hasRole(Role::SUPER_EDITOR) && $_POST["id"] !== $users->auth->getUserId()){
exit;
}
apiResponse($users->get_user($vars["userId"]));
apiResponse($users->getUserById($vars["userId"]));
}
);
$r->addRoute(

View File

@ -1,6 +1,7 @@
<?php
use skrtdev\NovaGram\Bot;
use skrtdev\Telegram\Message;
use skrtdev\Telegram\CallbackQuery;
require_once 'utils.php';
@ -32,10 +33,15 @@ function initializeBot($mode = WEBHOOK) {
}
}
function getUserIdByMessage(Message $message)
function getUserIdByFrom($from_id)
{
global $db;
return $db->selectValue("SELECT user FROM `".DB_PREFIX."_bot_telegram` WHERE `chat_id` = ?", [$message->from->id]);
return $db->selectValue("SELECT user FROM `".DB_PREFIX."_bot_telegram` WHERE `chat_id` = ?", [$from_id]);
}
function getUserIdByMessage(Message $message)
{
return getUserIdByFrom($message->from->id);
}
function requireBotLogin(Message $message)
@ -58,20 +64,22 @@ function requireBotLogin(Message $message)
}
}
function sendTelegramNotification($message)
function sendTelegramNotification($message, $do_not_send_if_same=true)
{
global $Bot, $db;
if(is_null($Bot)) initializeBot(NONE);
$sentMessages = [];
//TODO: implement different types of notifications
//TODO: add command for subscribing to notifications
$chats = $db->select("SELECT * FROM `".DB_PREFIX."_bot_telegram_notifications`");
if(!is_null($chats)) {
foreach ($chats as $chat) {
if(urldecode($chat['last_notification']) === $message) continue;
if($do_not_send_if_same && urldecode($chat['last_notification']) === $message) continue;
$chat = $chat['chat_id'];
$Bot->sendMessage([
$sendMessage = $Bot->sendMessage([
"chat_id" => $chat,
"text" => $message
]);
@ -80,11 +88,13 @@ function sendTelegramNotification($message)
["last_notification" => urlencode($message)],
["chat_id" => $chat]
);
$sentMessages[$chat] = $sendMessage->message_id;
}
}
return $sentMessages;
}
function sendTelegramNotificationToUser($message, $userId)
function sendTelegramNotificationToUser($message, $userId, $options = [])
{
global $Bot, $db;
@ -92,13 +102,78 @@ function sendTelegramNotificationToUser($message, $userId)
$chat = $db->selectValue("SELECT `chat_id` FROM `".DB_PREFIX."_bot_telegram` WHERE `user` = ?", [$userId]);
if(!is_null($chat)) {
$Bot->sendMessage([
$message_response = $Bot->sendMessage(array_merge([
"chat_id" => $chat,
"text" => $message
]);
], $options));
return [$message_response->message_id, $chat];
}
}
function generateAlertReportMessage($alertType, $crew) {
global $users;
$message =
"<b><i><u>Allertamento in corso:</u></i></b> ".
($alertType === "full" ? "Richiesta <b>squadra completa 🚒</b>" : "<b>Supporto 🧯</b>\n").
"Squadra:\n";
foreach($crew as $member) {
$user = $users->getUserById($member['id']);
$message .= "<i>".$user["name"]."</i> ";
if($user["chief"]) $message .= "CS";
if($user["driver"]) $message .= "🚒";
$message .= "- ";
if($member["response"] === "waiting") {
$message .= "In attesa 🟡";
} else if($member["response"] === true) {
$message .= "Presente 🟢";
} else if($member["response"] === false) {
$message .= "Assente 🔴";
}
$message .= "\n";
}
return $message;
}
function generateAlertRequestMessage($alertType, $live=true) {
$message =
"<b><i><u>". ($live ? "Allertamento in corso" : "Notifica di allertamento ricevuta") .":</u></i></b> ".
($alertType === "full" ? "Richiesta <b>squadra completa 🚒</b>" : "<b>Supporto 🧯</b>\n");
return $message;
}
function sendAlertReportMessage($alertType, $crew) {
global $Bot;
$message = generateAlertReportMessage($alertType, $crew);
return sendTelegramNotification($message, false);
}
function sendAlertRequestMessage($alertType, $userId, $alertId) {
global $Bot;
return sendTelegramNotificationToUser(generateAlertRequestMessage($alertType), $userId, [
'reply_markup' => [
'inline_keyboard' => [
[
[
'text' => '✅ Partecipo',
'callback_data' => "alert_yes_".$alertType."_".$alertId
],
[
'text' => 'Non partecipo ❌',
'callback_data' => "alert_no_".$alertType."_".$alertId
]
]
]
]
]);
}
function yesOrNo($value)
{
return ($value === 1 || $value) ? '<b>SI</b>' : '<b>NO</b>';
@ -172,7 +247,7 @@ function telegramBotRouter() {
if(is_null($user_id)) {
$message->chat->sendMessage('⚠️ Questo account Telegram non è associato a nessun utente di Allerta.');
} else {
$user = $users->get_user($user_id);
$user = $users->getUserById($user_id);
$message->chat->sendMessage(
" Informazioni sul profilo:".
"\n<i>Nome:</i> <b>".$user["name"]."</b>".
@ -248,6 +323,31 @@ function telegramBotRouter() {
}
$message->reply($msg);
});
$Bot->onCallbackQuery(function (CallbackQuery $callback_query) use ($Bot) {
$user = $callback_query->from;
$message = $callback_query->message;
$chat = $message->chat;
if(strpos($callback_query->data, 'alert_') === 0) {
$data = explode("_", str_replace("alert_", "", $callback_query->data));
$alert_type = $data[1];
$alert_id = $data[2];
setAlertResponse($data[0] === "yes", getUserIdByFrom($user->id), $alert_id);
/*
if($data[0] === "yes") {
$callback_query->answer(" Partecipazione registrata con successo.");
$message->reply("🟢 Partecipazione accettata.");
} else if($data[0] === "no") {
$callback_query->answer(" Rifiuto alla partecipazione registrato con successo.");
$message->reply("🔴 Partecipazione rifiutata.");
}
$message->editReplyMarkup([]);
*/
return;
}
});
$Bot->start();
}
}

View File

@ -208,7 +208,7 @@ class Users
return $this->db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0");
}
public function get_user($id)
public function getUserById($id)
{
return $this->db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$id]);
}

View File

@ -19,7 +19,12 @@
</thead>
<tbody>
<tr *ngFor="let user of users">
<td>{{ user.name }}</td>
<td>
<img alt="red helmet" src="./assets/icons/red_helmet.png" width="20px" *ngIf="user.chief">
<img alt="red helmet" src="./assets/icons/black_helmet.png" width="20px" *ngIf="!user.chief">
{{ user.name }}
<img alt="driver" src="./assets/icons/wheel.png" width="20px" *ngIf="user.driver">
</td>
<ng-container *ngIf="user.response == 'waiting'">
<td style="width: 1px;"><i class="fas fa-spinner fa-spin"></i></td>
<td>In attesa di risposta</td>

View File

@ -5,6 +5,8 @@ import { AuthService } from 'src/app/_services/auth.service';
import { ToastrService } from 'ngx-toastr';
import Swal from 'sweetalert2';
const isEqual = (...objects: any[]) => objects.every(obj => JSON.stringify(obj) === JSON.stringify(objects[0]));
@Component({
selector: 'modal-alert',
templateUrl: './modal-alert.component.html',
@ -13,7 +15,7 @@ import Swal from 'sweetalert2';
export class ModalAlertComponent implements OnInit, OnDestroy {
id = 0;
users: { name: string, response: string|boolean }[] = [];
users: any[] = [];
isAdvancedCollapsed = true;
loadDataInterval: NodeJS.Timer | undefined = undefined;
@ -29,12 +31,10 @@ export class ModalAlertComponent implements OnInit, OnDestroy {
loadResponsesData() {
this.api.get(`alerts/${this.id}`).then((response) => {
console.log(response);
this.users = response.crew;
console.log(this.users !== response.crew, this.users, response.crew);
if(!isEqual(this.users, response.crew)) this.users = response.crew;
if (response.notes !== "" && response.notes !== null) {
if(this.notes !== response.notes) {
this.notes = response.notes;
}
if(!isEqual(this.notes, response.notes)) this.notes = response.notes;
}
});
}

View File

@ -36,12 +36,13 @@ export class TableComponent implements OnInit, OnDestroy {
}
loadTableData() {
this.api.get(this.sourceType || "list").then((data: any) => {
if(!this.sourceType) this.sourceType = "list";
this.api.get(this.sourceType).then((data: any) => {
console.log(data);
this.data = data.filter((row: any) => {
if(typeof row.hidden !== 'undefined') return !row.hidden;
return true;
});
this.data = data.filter((row: any) => typeof row.hidden !== 'undefined' ? !row.hidden : true);
if(this.sourceType === 'list') {
this.api.availableUsers = this.data.filter((row: any) => row.available).length;
}
});
}

View File

@ -20,10 +20,10 @@
<owner-image></owner-image>
<div class="text-center" *ngIf="auth.profile.hasRole('SUPER_EDITOR')">
<div class="btn-group" role="group">
<button type="button" class="btn btn-danger" (click)="addAlertFull()">
<button type="button" class="btn btn-danger" (click)="addAlertFull()" [disabled]="!api?.availableUsers || api.availableUsers! < 5">
🚒 Richiedi squadra completa
</button>
<button type="button" class="btn btn-warning" (click)="addAlertSupport()">
<button type="button" class="btn btn-warning" (click)="addAlertSupport()" [disabled]="!api?.availableUsers || api.availableUsers! < 2">
Richiedi squadra di supporto 🧯
</button>
</div>

View File

@ -24,7 +24,7 @@ export class ListComponent implements OnInit, OnDestroy {
public manual_mode: boolean | undefined = undefined;
constructor(
private api: ApiClientService,
public api: ApiClientService,
public auth: AuthService,
private toastr: ToastrService,
private modalService: BsModalService,
@ -76,6 +76,12 @@ export class ListComponent implements OnInit, OnDestroy {
this.api.post("alerts", {
type: "full"
}).then((response) => {
if(response?.status === "error") {
this.toastr.error(response.message, undefined, {
timeOut: 5000
});
return;
}
this.alertModalRef = this.modalService.show(ModalAlertComponent, {
initialState: {
id: response.id
@ -90,6 +96,12 @@ export class ListComponent implements OnInit, OnDestroy {
this.api.post("alerts", {
type: "support"
}).then((response) => {
if(response?.status === "error") {
this.toastr.error(response.message, undefined, {
timeOut: 5000
});
return;
}
this.alertModalRef = this.modalService.show(ModalAlertComponent, {
initialState: {
id: response.id

View File

@ -7,7 +7,9 @@ import { Subject } from "rxjs";
})
export class ApiClientService {
private apiRoot = 'api/';
public alertsChanged = new Subject<void>();
public availableUsers: undefined | number = undefined;
constructor(private http: HttpClient) { }