Merge pull request #430 from allerta-vvf/master

Update staging env with new Telegram bot and services
This commit is contained in:
Matteo Gheza 2022-01-12 00:27:53 +01:00 committed by GitHub
commit ea8d192466
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1205 additions and 252 deletions

View File

@ -38,6 +38,16 @@ function apiRouter (FastRoute\RouteCollector $r) {
}
}
);
$r->addRoute(
'GET',
'/place_image',
function ($vars) {
header('Cache-control: max-age='.(60*60*24*31));
header('Expires: '.gmdate(DATE_RFC1123,time()+60*60*24*31));
header('Content-Type: image/png');
readfile("tmp/".md5($_GET["lat"].";".$_GET["lng"]).".jpg");
}
);
$r->addRoute(
'GET',
@ -87,9 +97,9 @@ function apiRouter (FastRoute\RouteCollector $r) {
requireLogin() || accessDenied();
$users->online_time_update();
if($users->hasRole(Role::FULL_VIEWER)) {
$response = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` ORDER BY available DESC, chief DESC, services ASC, trainings DESC, availability_minutes ASC, name ASC");
$response = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` ORDER BY available DESC, chief DESC, services ASC, trainings DESC, availability_minutes DESC, name ASC WHERE `hidden` = 0");
} else {
$response = $db->select("SELECT `id`, `chief`, `online_time`, `available`, `name` FROM `".DB_PREFIX."_profiles` ORDER BY available DESC, chief DESC, services ASC, trainings DESC, availability_minutes ASC, name ASC");
$response = $db->select("SELECT `id`, `chief`, `online_time`, `available`, `availability_minutes`, `name`, `driver`, `services` FROM `".DB_PREFIX."_profiles` ORDER BY available DESC, chief DESC, services ASC, trainings DESC, availability_minutes DESC, name ASC WHERE `hidden` = 0");
}
apiResponse(
!is_null($response) ? $response : []
@ -134,7 +144,40 @@ function apiRouter (FastRoute\RouteCollector $r) {
global $services, $users;
requireLogin() || accessDenied();
$users->online_time_update();
apiResponse([]);
apiResponse(["response" => $services->add($_POST["start"], $_POST["end"], $_POST["code"], $_POST["chief"], $_POST["drivers"], $_POST["crew"], $_POST["place"], $_POST["notes"], $_POST["type"], $users->auth->getUserId())]);
}
);
$r->addRoute(
['GET'],
'/services/{id}',
function ($vars) {
global $services, $users;
requireLogin() || accessDenied();
$users->online_time_update();
apiResponse($services->get($vars['id']));
}
);
$r->addRoute(
['DELETE'],
'/services/{id}',
function ($vars) {
global $services, $users;
requireLogin() || accessDenied();
$users->online_time_update();
apiResponse(["response" => $services->delete($vars["id"])]);
}
);
$r->addRoute(
['GET'],
'/place_details',
function ($vars) {
global $db, $users;
requireLogin() || accessDenied();
$users->online_time_update();
$response = $db->selectRow("SELECT * FROM `".DB_PREFIX."_places_info` WHERE `lat` = ? and `lng` = ? LIMIT 0,1;", [$_GET["lat"], $_GET["lng"]]);
apiResponse(!is_null($response) ? $response : []);
}
);
@ -239,18 +282,10 @@ function apiRouter (FastRoute\RouteCollector $r) {
"POST",
"/manual_mode",
function ($vars) {
global $users, $db;
global $users, $availability;
requireLogin() || accessDenied();
$users->online_time_update();
$db->update(
DB_PREFIX."_profiles",
[
"manual_mode" => $_POST["manual_mode"]
],
[
"id" => $users->auth->getUserId()
]
);
$availability->change_manual_mode($_POST["manual_mode"]);
apiResponse(["status" => "success"]);
}
);
@ -302,6 +337,16 @@ function apiRouter (FastRoute\RouteCollector $r) {
}
);
$r->addRoute(
['GET'],
'/places/search',
function ($vars) {
global $places;
requireLogin() || accessDenied();
apiResponse($places->search($_GET["q"]));
}
);
$r->addRoute(
['POST'],
'/telegram_login_token',

64
backend/composer.lock generated
View File

@ -207,12 +207,12 @@
"source": {
"type": "git",
"url": "https://github.com/allerta-vvf/PHP-Auth-JWT",
"reference": "3ea0aa3d7e74528c57932872bbda339e995a9d9a"
"reference": "a39b9e746d056145c31bb9c72f613c751d85e105"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/allerta-vvf/PHP-Auth-JWT/zipball/3ea0aa3d7e74528c57932872bbda339e995a9d9a",
"reference": "3ea0aa3d7e74528c57932872bbda339e995a9d9a",
"url": "https://api.github.com/repos/allerta-vvf/PHP-Auth-JWT/zipball/a39b9e746d056145c31bb9c72f613c751d85e105",
"reference": "a39b9e746d056145c31bb9c72f613c751d85e105",
"shasum": ""
},
"require": {
@ -240,7 +240,7 @@
"login",
"security"
],
"time": "2021-12-27T18:35:45+00:00"
"time": "2022-01-09T13:37:14+00:00"
},
{
"name": "delight-im/base64",
@ -2554,16 +2554,16 @@
},
{
"name": "symfony/http-client",
"version": "v6.0.1",
"version": "v6.0.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "99e42b54cedf061d898aa796a0b3758598021607"
"reference": "7f1cbd44590cb0acc6208c1711a52733e9a91663"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/99e42b54cedf061d898aa796a0b3758598021607",
"reference": "99e42b54cedf061d898aa796a0b3758598021607",
"url": "https://api.github.com/repos/symfony/http-client/zipball/7f1cbd44590cb0acc6208c1711a52733e9a91663",
"reference": "7f1cbd44590cb0acc6208c1711a52733e9a91663",
"shasum": ""
},
"require": {
@ -2618,7 +2618,7 @@
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-client/tree/v6.0.1"
"source": "https://github.com/symfony/http-client/tree/v6.0.2"
},
"funding": [
{
@ -2634,7 +2634,7 @@
"type": "tidelift"
}
],
"time": "2021-12-08T15:13:44+00:00"
"time": "2021-12-29T10:14:09+00:00"
},
{
"name": "symfony/http-client-contracts",
@ -2783,21 +2783,24 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.23.1",
"version": "v1.24.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6"
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6",
"reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
@ -2843,7 +2846,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0"
},
"funding": [
{
@ -2859,20 +2862,20 @@
"type": "tidelift"
}
],
"time": "2021-05-27T12:26:48+00:00"
"time": "2021-11-30T18:21:41+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.23.1",
"version": "v1.24.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be"
"reference": "57b712b08eddb97c762a8caa32c84e037892d2e9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be",
"reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/57b712b08eddb97c762a8caa32c84e037892d2e9",
"reference": "57b712b08eddb97c762a8caa32c84e037892d2e9",
"shasum": ""
},
"require": {
@ -2926,7 +2929,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.24.0"
},
"funding": [
{
@ -2942,25 +2945,28 @@
"type": "tidelift"
}
],
"time": "2021-07-28T13:41:28+00:00"
"time": "2021-09-13T13:58:33+00:00"
},
{
"name": "symfony/polyfill-uuid",
"version": "v1.23.0",
"version": "v1.24.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git",
"reference": "9165effa2eb8a31bb3fa608df9d529920d21ddd9"
"reference": "7529922412d23ac44413d0f308861d50cf68d3ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/9165effa2eb8a31bb3fa608df9d529920d21ddd9",
"reference": "9165effa2eb8a31bb3fa608df9d529920d21ddd9",
"url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/7529922412d23ac44413d0f308861d50cf68d3ee",
"reference": "7529922412d23ac44413d0f308861d50cf68d3ee",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-uuid": "*"
},
"suggest": {
"ext-uuid": "For best performance"
},
@ -3005,7 +3011,7 @@
"uuid"
],
"support": {
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.23.0"
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.24.0"
},
"funding": [
{
@ -3021,7 +3027,7 @@
"type": "tidelift"
}
],
"time": "2021-02-19T12:13:01+00:00"
"time": "2021-10-20T20:35:02+00:00"
},
{
"name": "symfony/service-contracts",
@ -3265,5 +3271,5 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.2.0"
"plugin-api-version": "2.1.0"
}

View File

@ -108,36 +108,34 @@ function job_schedule_availability() {
"minutes" => (int) date("i")
];
$manual_mode = $db->select("SELECT manual_mode FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$user_id]);
$manual_mode = $db->selectValue("SELECT `manual_mode` FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$user_id]);
if(
!$manual_mode &&
$manual_mode == 0 &&
$schedule["day"] == $now["day"] &&
$schedule["hour"] == $now["hour"] &&
$schedule["minutes"] <= $now["minutes"] &&
$now["minutes"] - $schedule["minutes"] <= 30
){
if(!in_array($user_id,$schedules_users)) $schedules_users[] = $user_id;
if(is_null($last_exec) || (is_array($last_exec) && $schedule["hour"] == $last_exec["hour"] ? $schedule["minutes"] !== $last_exec["minutes"] : true)/* && !in_array(date('Y-m-d'), $selected_holidays_dates)*/){
$last_exec_new = $schedule["day"].";".sprintf("%02d", $schedule["hour"]).":".sprintf("%02d", $schedule["minutes"]);
$db->update(
DB_PREFIX."_schedules",
["last_exec" => $last_exec_new],
["id" => $id]
);
$availability->change(1, $user_id, false);
$schedules_check["schedules"][] = [
"schedule" => $schedule,
"now" => $now,
"last_exec" => $last_exec,
"last_exec_new" => $last_exec_new,
];
}
$last_exec_new = $schedule["day"].";".sprintf("%02d", $schedule["hour"]).":".sprintf("%02d", $schedule["minutes"]);
$db->update(
DB_PREFIX."_schedules",
["last_exec" => $last_exec_new],
["id" => $id]
);
$availability->change(1, $user_id, false);
$schedules_check["schedules"][] = [
"schedule" => $schedule,
"now" => $now,
"last_exec" => $last_exec,
"last_exec_new" => $last_exec_new,
];
}
}
}
$schedules_check["users"] = $schedules_users;
$profiles = $db->select("SELECT id FROM `".DB_PREFIX."_profiles`");
$profiles = $db->select("SELECT id FROM `".DB_PREFIX."_profiles` WHERE `manual_mode` = 0");
foreach ($profiles as $profile) {
if(!in_array($profile["id"],$schedules_users)){
$availability->change(0, $profile["id"], false);
@ -157,6 +155,33 @@ function job_schedule_availability() {
];
}
function job_send_notification_if_manual_mode() {
global $db, $executed_actions;
if(
(int) date("H") === 7 &&
(int) date("i") - 5 < 0
) {
$profiles = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `manual_mode` = 1");
$notified_users = [];
foreach ($profiles as $profile) {
$notified_users[] = $profiles["id"];
$stato = $profile["available"] ? "disponibile" : "non disponibile";
sendTelegramNotificationToUser("⚠️ Attenzione! La tua disponibilità <b>non segue la programmazione oraria</b>.\nAttualmente sei <b>{$stato}</b>.\nScrivi \"/programma\" se vuoi ripristinare la programmazione.", $profile["id"]);
}
$output = $notified_users;
$output_status = "ok";
} else {
$output = ["notification not sent"];
$output_status = "ok";
}
$executed_actions[] = [
"title" => "Send notification if manual mode",
"description" => "Send notification to users at 7:00 if they are in manual mode",
"output" => $output,
"output_status" => $output_status
];
}
function cronRouter (FastRoute\RouteCollector $r) {
$r->addRoute(
'POST',
@ -171,10 +196,11 @@ function cronRouter (FastRoute\RouteCollector $r) {
}
job_schedule_availability();
//job_reset_availability();
job_reset_availability();
job_increment_availability();
job_send_notification_if_manual_mode();
apiResponse(["excuted_actions" => $executed_actions]);
}
);
}
}

View File

@ -58,18 +58,39 @@ function sendTelegramNotification($message)
//TODO: implement different types of notifications
//TODO: add command for subscribing to notifications
$chats = $db->select("SELECT `chat_id` FROM `".DB_PREFIX."_bot_telegram_notifications`");
$chats = $db->select("SELECT * FROM `".DB_PREFIX."_bot_telegram_notifications`");
if(!is_null($chats)) {
foreach ($chats as $chat) {
if($chat['last_notification'] === $message) continue;
$chat = $chat['chat_id'];
$Bot->sendMessage([
"chat_id" => $chat,
"text" => $message
]);
$db->update(
"`".DB_PREFIX."_bot_telegram_notifications`",
["last_notification" => $message],
["id" => $chat["id"]]
);
}
}
}
function sendTelegramNotificationToUser($message, $userId)
{
global $Bot, $db;
if(is_null($Bot)) initializeBot(NONE);
$chat = $db->selectValue("SELECT `chat_id` FROM `".DB_PREFIX."_bot_telegram` WHERE `user` = ?", [$userId]);
if(!is_null($chat)) {
$Bot->sendMessage([
"chat_id" => $chat,
"text" => $message
]);
}
}
function yesOrNo($value)
{
return ($value === 1 || $value) ? '<b>SI</b>' : '<b>NO</b>';
@ -131,7 +152,9 @@ function telegramBotRouter() {
"\n/help - Ottieni informazioni sui comandi".
"\n/attiva - Modifica la tua disponibilità in \"reperibile\"".
"\n/disattiva - Modifica la tua disponibilità in \"non reperibile\"".
"\n/elenco_disponibili - Mostra un elenco dei vigili attualmente disponibili"
"\n/programma - Abilita programmazione oraria".
"\n/disponibili - Mostra un elenco dei vigili attualmente disponibili".
"\n/stato - Mostra lo stato della disponibilità della squadra"
);
});
@ -163,7 +186,7 @@ function telegramBotRouter() {
if(count(explode(" ", $message->text)) > 3) return;
$user_id = getUserIdByMessage($message);
$availability->change(1, $user_id, true);
$Bot->sendMessage($message->from->id, "Disponibilità aggiorata con successo.\nOra sei <b>operativo</b>.");
$Bot->sendMessage($message->from->id, "Disponibilità aggiornata con successo.\nOra sei <b>operativo</b>.");
});
$Bot->onText("/\/?(Io |Io sono )?(Disattiva|Disattivo|Disattivami|Non( |_)attivo|Non( |_)(Sono |sono )?disponibile|Non( |_)(Sono |sono )?operativo|disattiva|disattivo|sisattivami|non( |_)(Sono |sono )?attivo|non( |_)(Sono |sono )?disponibile|non( |_)(Sono |sono )?operativo)/", function (Message $message, $matches = []) {
@ -172,14 +195,37 @@ function telegramBotRouter() {
if(count(explode(" ", $message->text)) > 4) return;
$user_id = getUserIdByMessage($message);
$availability->change(0, $user_id, true);
$Bot->sendMessage($message->from->id, "Disponibilità aggiorata con successo.\nOra sei <b>non operativo</b>.");
$Bot->sendMessage($message->from->id, "Disponibilità aggiornata con successo.\nOra sei <b>non operativo</b>.");
});
$Bot->onText("/\/?(Elenco|elenco|Elenca|elenca)(_| )(Disponibili|disponibili)/", function (Message $message, $matches = []) {
$Bot->onText("/\/?(Abilita( |_)|abilita( |_)|Attiva( |_)|attiva( |_))?(Programma|Programmazione|programmazione|Programmazione( |_)oraria|programma|programmazione( |_)oraria)/", function (Message $message, $matches = []) {
global $Bot, $availability;
requireBotLogin($message);
if(count(explode(" ", $message->text)) > 3) return;
$userId = getUserIdByMessage($message);
$availability->change_manual_mode(0, $userId);
$Bot->sendMessage($message->from->id, "Programmazione oraria <b>abilitata</b>.\nPer disabilitarla (e tornare in modalità manuale), cambiare la disponbilità usando i comandi \"/attiva\" e \"/disattiva\"");
});
$Bot->onText("/\/?(Stato|stato)( |_)?(Distaccamento|distaccamento)?/", function (Message $message, $matches = []) {
global $db;
requireBotLogin($message);
if(count(explode(" ", $message->text)) > 2) return;
$available_users_count = $db->selectValue("SELECT COUNT(id) FROM `".DB_PREFIX."_profiles` WHERE `available` = 1 AND `hidden` = 0");
if($available_users_count >= 5) {
$message->reply("🚒 Distaccamento operativo con squadra completa");
} else if($available_users_count >= 2) {
$message->reply("🧯 Distaccamento operativo per supporto");
} else if($available_users_count >= 0) {
$message->reply("⚠️ Distaccamento non operativo");
}
});
$Bot->onText("/\/?(Elenco|elenco|Elenca|elenca)?(_| )?(Disponibili|disponibili)/", function (Message $message, $matches = []) {
global $db, $users;
requireBotLogin($message);
if(count(explode(" ", $message->text)) > 2) return;
$result = $db->select("SELECT `chief`, `driver`, `available`, `name` FROM `".DB_PREFIX."_profiles` WHERE available = 1 and hidden = 0 ORDER BY chief DESC, services ASC, trainings DESC, availability_minutes ASC, name ASC");
$result = $db->select("SELECT `chief`, `driver`, `available`, `name` FROM `".DB_PREFIX."_profiles` WHERE available = 1 and hidden = 0 ORDER BY chief DESC, services ASC, trainings DESC, availability_minutes DESC, name ASC");
if(!is_null($result) && count($result) > 0) {
$msg = " Vigili attualmente disponibili:";
foreach($result as $user) {

View File

@ -106,7 +106,7 @@ function logger($action, $changed=null, $editor=null, $timestamp=null, $source_t
}
}
class options
class Options
{
protected $db;
protected $cache;
@ -120,7 +120,7 @@ class options
try {
$this->optionsCache = $this->cache->getItem("options");
if (is_null($this->optionsCache->get())) {
$this->optionsCache->set($db->select("SELECT * FROM `".DB_PREFIX."_options` WHERE `enabled` = 1"))->expiresAfter(60);
$this->optionsCache->set($db->select("SELECT * FROM `".DB_PREFIX."_options` WHERE `enabled` = 1"))->expiresAfter(60*60*24*7);
$this->cache->save($this->optionsCache);
}
$this->options = $this->optionsCache->get();
@ -189,7 +189,7 @@ class Users
public function get_users()
{
return $this->db->select("SELECT * FROM `".DB_PREFIX."_profiles`");
return $this->db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0");
}
public function get_user($id)
@ -234,15 +234,13 @@ class Users
{
if(is_null($id)) $id = $this->auth->getUserId();
if(is_null($id)) return true;
$user = $this->db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$id]);
return $user["hidden"];
return $this->db->selectValue("SELECT hidden FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$id]);
}
public function getName($id=null)
{
if(is_null($id)) $id = $this->auth->getUserId();
$user = $this->db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$id]);
return $user["name"];
return $this->db->selectValue("SELECT name FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$id]);
}
public function hasRole($role, $adminGranted=true)
@ -261,6 +259,20 @@ class Availability {
$this->users = $users;
}
public function change_manual_mode($manual_mode, $user_id = null) {
global $db, $users;
if(is_null($user_id)) $user_id = $users->auth->getUserId();
$db->update(
DB_PREFIX."_profiles",
[
"manual_mode" => $manual_mode
],
[
"id" => $user_id
]
);
}
public function change($availability, $user_id, $is_manual_mode=true)
{
if($is_manual_mode) logger("Disponibilità cambiata in ".($availability ? '"disponibile"' : '"non disponibile"'), $user_id, $this->users->auth->getUserId());
@ -277,7 +289,7 @@ class Availability {
if(!$this->users->isHidden($user_id)) {
$available_users_count = $this->db->selectValue("SELECT COUNT(id) FROM `".DB_PREFIX."_profiles` WHERE `available` = 1 AND `hidden` = 0");
if($available_users_count === 5) {
sendTelegramNotification(" Distaccamento operativo con squadra completa");
sendTelegramNotification("🚒 Distaccamento operativo con squadra completa");
} else if($available_users_count === 2) {
sendTelegramNotification("🧯 Distaccamento operativo per supporto");
} else if($available_users_count === 1 && !$availability) {
@ -291,19 +303,65 @@ class Availability {
class Services {
private $db = null;
private $users = null;
private $places = null;
public function __construct($db)
public function __construct($db, $users, $places)
{
$this->db = $db;
$this->users = $users;
$this->places = $places;
}
public function list() {
$response = $this->db->select("SELECT * FROM `".DB_PREFIX."_services` ORDER BY date DESC, beginning DESC");
return !is_null($response) ? $response : [];
$response = $this->db->select("SELECT ".DB_PREFIX."_services.*, place.id as place_id, place.lat as lat, place.lng as lng, place.place_name as place_name, place.country as country, place.country_code as country_code, place.postcode as postcode, place.state as state, place.municipality as municipality, place.village as village, place.hamlet as hamlet, place.road as road, place.building_service_name as building_service_name, place.house_number as house_number FROM `".DB_PREFIX."_services` JOIN ".DB_PREFIX."_places_info place ON ".DB_PREFIX."_services.place_reverse = place.id ORDER BY start DESC");
$response = is_null($response) ? [] : $response;
foreach($response as &$service) {
$service["chief"] = $this->users->getName($service["chief"]);
$drivers = explode(";", $service["drivers"]);
foreach($drivers as &$driver) {
$driver = $this->users->getName($driver);
}
$service["drivers"] = implode(", ", $drivers);
$crew = explode(";", $service["crew"]);
foreach($crew as &$member) {
$member = $this->users->getName($member);
}
$service["crew"] = implode(", ", $crew);
$service["type"] = $this->db->selectValue("SELECT name FROM `".DB_PREFIX."_type` WHERE `id` = ?", [$service["type"]]);
}
return $response;
}
public function get($id) {
$response = $this->db->selectRow("SELECT ".DB_PREFIX."_services.*, place.id as place_id, place.lat as lat, place.lng as lng, place.place_name as place_name, place.country as country, place.country_code as country_code, place.postcode as postcode, place.state as state, place.municipality as municipality, place.village as village, place.hamlet as hamlet, place.road as road, place.building_service_name as building_service_name, place.house_number as house_number FROM `".DB_PREFIX."_services` JOIN ".DB_PREFIX."_places_info place ON ".DB_PREFIX."_services.place_reverse = place.id WHERE ".DB_PREFIX."_services.id = ? ORDER BY start DESC", [$id]);
if(is_null($response)) return [];
return $response;
$response["chief"] = $this->users->getName($response["chief"]);
$response = explode(";", $response["drivers"]);
foreach($response as &$driver) {
$driver = $this->users->getName($driver);
}
$response["drivers"] = implode(", ", $response);
$crew = explode(";", $response["crew"]);
foreach($crew as &$member) {
$member = $this->users->getName($member);
}
$response["crew"] = implode(", ", $crew);
$response["type"] = $this->db->selectValue("SELECT name FROM `".DB_PREFIX."_type` WHERE `id` = ?", [$response["type"]]);
return $response;
}
public function increment_counter($increment)
{
$increment = str_replace(";", ",", $increment);
$this->db->exec(
"UPDATE `".DB_PREFIX."_profiles` SET `services`= services + 1 WHERE id IN ($increment)"
);
@ -311,6 +369,7 @@ class Services {
public function decrement_counter($decrement)
{
$decrement = str_replace(";", ",", $decrement);
$this->db->exec(
"UPDATE `".DB_PREFIX."_profiles` SET `services`= services - 1 WHERE id IN ($decrement)"
);
@ -318,24 +377,153 @@ class Services {
public function get_selected_users($id)
{
return $this->db->selectValue(
"SELECT `increment` FROM `".DB_PREFIX."_services` WHERE `id` = :id LIMIT 0, 1",
$response = $this->db->selectRow(
"SELECT `chief`, `drivers`, `crew` FROM `".DB_PREFIX."_services` WHERE `id` = :id",
["id" => $id]
);
return $response["chief"].";".$response["drivers"].";".$response["crew"];
}
public function add($date, $code, $beginning, $end, $chief, $drivers, $crew, $place, $notes, $type, $increment, $inserted_by)
public function add($start, $end, $code, $chief, $drivers, $crew, $place, $notes, $type, $inserted_by)
{
$drivers = implode(",", $drivers);
$crew = implode(",", $crew);
$increment = implode(",", $increment);
$date = date('Y-m-d H:i:s', strtotime($date));
$this->db->insert(
DB_PREFIX."_services",
["date" => $date, "code" => $code, "beginning" => $beginning, "end" => $end, "chief" => $chief, "drivers" => $drivers, "crew" => $crew, "place" => $place, "place_reverse" => $this->tools->savePlaceReverse($place), "notes" => $notes, "type" => $type, "increment" => $increment, "inserted_by" => $inserted_by]
["start" => $start, "end" => $end, "code" => $code, "chief" => $chief, "drivers" => $drivers, "crew" => $crew, "place" => $place, "place_reverse" => $this->places->save_place_reverse(explode(";", $place)[0], explode(";", $place)[1]), "notes" => $notes, "type" => $type, "inserted_by" => $inserted_by]
);
$this->increment_counter($increment);
$serviceId = $this->db->getLastInsertId();
$this->increment_counter($chief.";".$drivers.";".$crew);
logger("Service added");
return $serviceId;
}
public function delete($id)
{
$service = $this->db->selectRow(
"SELECT `chief`, `drivers`, `crew` FROM `".DB_PREFIX."_services` WHERE `id` = :id",
["id" => $id]
);
$this->decrement_counter($service["chief"].";".$service["drivers"].";".$service["crew"]);
$this->db->delete(
DB_PREFIX."_services",
["id" => $id]
);
logger("Intervento eliminato");
return true;
}
}
function curl_call($url, $is_response_json=true)
{
$useragent = "Allerta-VVF (https://github.com/allerta-vvf/allerta-vvf) place search proxy (see utils.php class Places)";
try {
$hostname = gethostname();
if(!is_null($hostname) && $hostname != "") $useragent .= " - server hostname: ".$hostname;
} catch (Exception $e) {
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
$response = curl_exec($ch);
if($is_response_json) $response = json_decode($response, true);
curl_close($ch);
return $response;
}
class Places {
private $cache;
private $users;
private $db;
private $placesCache;
public function __construct($cache, $users, $db)
{
$this->cache = $cache;
$this->users = $users;
$this->db = $db;
}
public function search($query)
{
$this->placesCache = $this->cache->getItem("place_".md5($query));
$cache_element = $this->placesCache->get();
if (is_null($cache_element)) {
$place_response = curl_call("https://nominatim.openstreetmap.org/search?format=json&limit=6&q=".urlencode($query));
if(is_null($place_response)) {
$place_response = [];
}
$this->placesCache->set($place_response)->expiresAfter(60*60*24*365);
$this->cache->save($this->placesCache);
return $place_response;
} else {
return $cache_element;
}
}
public function save_place_reverse($lat, $lng)
{
$this->save_static_map_image($lat, $lng);
$response = curl_call("https://nominatim.openstreetmap.org/reverse?format=json&lat=".$lat."&lon=".$lng);
if(is_null($response) || empty($response)) {
$response = "{}";
$place_name = "";
$address = [];
} else {
$place_name = $response["display_name"];
$address = $response["address"];
}
$row = ["lat" => $lat, "lng" => $lng, "place_name" => $place_name, "place" => json_encode($response)];
if(isset($address["country"])) $row["country"] = $address["country"];
if(isset($address["country_code"])) $row["country_code"] = $address["country_code"];
if(isset($address["postcode"])) $row["postcode"] = $address["postcode"];
if(isset($address["region"])) $row["state"] = $address["region"];
if(isset($address["state"])) $row["state"] = $address["state"];
if(isset($address["municipality"])) $row["municipality"] = $address["municipality"];
if(isset($address["village"])) $row["village"] = $address["village"];
if(isset($address["hamlet"])) $row["hamlet"] = $address["hamlet"];
if(isset($address["road"])) $row["road"] = $address["road"];
if(isset($address["tourism"])) $row["building_service_name"] = $address["tourism"];
if(isset($address["croft"])) $row["building_service_name"] = $address["croft"];
if(isset($address["isolated_dwelling"])) $row["building_service_name"] = $address["isolated_dwelling"];
if(isset($address["amenity"])) $row["building_service_name"] = $address["amenity"];
if(isset($address["building"])) $row["building_service_name"] = $address["building"];
if(isset($address["house_number"])) $row["house_number"] = $address["house_number"];
$this->db->insert(
DB_PREFIX."_places_info",
$row
);
return $this->db->getLastInsertId();
}
function save_static_map_image($lat, $lng)
{
if(get_option("use_static_map_image_generator", false)) {
$url = get_option("static_map_image_generator_url", "");
$url = str_replace("{{lat}}", $lat, $url);
$url = str_replace("{{lng}}", $lng, $url);
} else {
$tile_x = floor($lng / 360 * pow(2, get_option("static_map_image_zoom", 18)));
$tile_y = floor(log(tan((90 + $lat) * pi() / 360)) / pi() * pow(2, get_option("static_map_image_zoom", 18)));
$url = "https://a.tile.openstreetmap.org/".get_option("static_map_image_zoom", 18)."/".$tile_x."/".$tile_y.".png";
}
$image = curl_call($url, false);
$image_path = "tmp/".md5($lat.";".$lng).".jpg";
file_put_contents($image_path, $image);
}
}
@ -379,5 +567,6 @@ class Schedules {
$users = new Users($db, $auth);
$availability = new Availability($db, $users);
$services = new Services($db);
$places = new Places($cache, $users, $db);
$services = new Services($db, $users, $places);
$schedules = new Schedules($db, $users);

View File

@ -27,6 +27,7 @@
"ngx-bootstrap": "^7.1.2",
"ngx-toastr": "^14.2.1",
"rxjs": "~7.4.0",
"sweetalert2": "^11.3.4",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
},
@ -7944,12 +7945,12 @@
"optional": true
},
"node_modules/node-forge": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.0.tgz",
"integrity": "sha512-M4AsdaP0bGNaSPtatd/+f76asocI0cFaURRdeQVZvrJBrYp2Qohv5hDbGHykuNqCb1BYjWHjdS6HlN50qbztwA==",
"dev": true,
"engines": {
"node": ">= 6.0.0"
"node": ">= 6.13.0"
}
},
"node_modules/node-gyp": {
@ -11010,12 +11011,12 @@
"dev": true
},
"node_modules/selfsigned": {
"version": "1.10.11",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz",
"integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==",
"version": "1.10.13",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.13.tgz",
"integrity": "sha512-UmLwTKZwNmXYDAlRFhaEdgEg0dp9k5gfJXuO7uKvSqioN1M0+Mgf4V39IlVZMSuqGoCi6h5legkhNXvWy0rFSg==",
"dev": true,
"dependencies": {
"node-forge": "^0.10.0"
"node-forge": "^1.2.0"
}
},
"node_modules/semver": {
@ -11625,6 +11626,14 @@
"node": ">=4"
}
},
"node_modules/sweetalert2": {
"version": "11.3.4",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.3.4.tgz",
"integrity": "sha512-o7owEtVtpqnB8y3BDCRn2taYb7URcf6RIUm120LjFxt8IHfPIm0UeHuiS3nEp5r9RKm5GlSSJOh/yuapUQcYcQ==",
"funding": {
"url": "https://sweetalert2.github.io/#donations"
}
},
"node_modules/symbol-observable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
@ -18487,9 +18496,9 @@
"optional": true
},
"node-forge": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.0.tgz",
"integrity": "sha512-M4AsdaP0bGNaSPtatd/+f76asocI0cFaURRdeQVZvrJBrYp2Qohv5hDbGHykuNqCb1BYjWHjdS6HlN50qbztwA==",
"dev": true
},
"node-gyp": {
@ -20770,12 +20779,12 @@
"dev": true
},
"selfsigned": {
"version": "1.10.11",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz",
"integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==",
"version": "1.10.13",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.13.tgz",
"integrity": "sha512-UmLwTKZwNmXYDAlRFhaEdgEg0dp9k5gfJXuO7uKvSqioN1M0+Mgf4V39IlVZMSuqGoCi6h5legkhNXvWy0rFSg==",
"dev": true,
"requires": {
"node-forge": "^0.10.0"
"node-forge": "^1.2.0"
}
},
"semver": {
@ -21265,6 +21274,11 @@
"has-flag": "^3.0.0"
}
},
"sweetalert2": {
"version": "11.3.4",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.3.4.tgz",
"integrity": "sha512-o7owEtVtpqnB8y3BDCRn2taYb7URcf6RIUm120LjFxt8IHfPIm0UeHuiS3nEp5r9RKm5GlSSJOh/yuapUQcYcQ=="
},
"symbol-observable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",

View File

@ -31,6 +31,7 @@
"ngx-bootstrap": "^7.1.2",
"ngx-toastr": "^14.2.1",
"rxjs": "~7.4.0",
"sweetalert2": "^11.3.4",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
},

View File

@ -0,0 +1 @@
<button (click)="locationBackService.goBack()" id="backBtn" title="Go back"><i class="fas fa-arrow-left"></i> Torna indietro</button>

View File

@ -0,0 +1,13 @@
#backBtn {
position: absolute;
z-index: 9999;
border: none;
outline: none;
background-color: #fbfbfb;
color: black;
padding: 10px;
border-radius: 5px;
font-size: 18px;
top: 60px;
left: 10px;
}

View File

@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { LocationBackService } from 'src/app/_services/locationBack.service';
@Component({
selector: 'back-btn',
templateUrl: './back-btn.component.html',
styleUrls: ['./back-btn.component.scss']
})
export class BackBtnComponent implements OnInit {
constructor(public locationBackService: LocationBackService) { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BackBtnComponent } from './back-btn.component';
@NgModule({
declarations: [
BackBtnComponent
],
imports: [
CommonModule
],
exports: [
BackBtnComponent
]
})
export class BackBtnModule { }

View File

@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { EditServiceComponent } from './edit-service.component';
const routes: Routes = [{ path: '', component: EditServiceComponent }];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class EditServiceRoutingModule { }

View File

@ -1,89 +1,107 @@
<form method="post">
<div class="container">
<div class="form-group">
<label for="date-picker">Giorno</label>
<input id="date-picker" type="text" placeholder="Premi per selezionare una data" class="form-control" bsDatepicker [bsConfig]="{ adaptivePosition: true }">
<back-btn></back-btn>
<br>
<form method="post" [formGroup]="serviceForm" (ngSubmit)="formSubmit()">
<div class="container">
<div class="form-group has-validation">
<label for="date-picker">Inizio</label>
<input formControlName="start" [class.is-invalid]="!isFieldValid('start')" type="text"
placeholder="Premi per selezionare data e ora" class="form-control" bsDatepicker
[bsConfig]="{ adaptivePosition: true, withTimepicker: true, dateInputFormat: 'DD/MM/YYYY, hh:mm' }">
<div class="invalid-feedback" *ngIf="start.errors?.['required']">
Seleziona data e ora di inizio dell'intervento
</div>
<div class="form-group">
<label for="progressivo">Progressivo</label>
<input id="progressivo" class="form-control" type="text" placeholder="1234/5" required>
</div>
<div class="form-group">
<label for="timePicker1">Ora inizio</label>
<input id="timePicker1" class="form-control" type="time" required>
</div>
<div class="form-group">
<label for="timePicker2">Ora fine</label>
<input id="timePicker2" class="form-control" type="time" required>
</div>
<div class="form-group">
<label>Caposquadra</label>
<br>
<ng-container *ngFor="let user of users">
<div class="form-check" *ngIf="user.chief">
<input class="form-check-input chief-{{ user.id }}" type="checkbox" value='{{ user.id }}'>
<label class="form-check-label" for="chief-{{ user.id }}">
{{ user.name }}
</label>
</div>
</ng-container>
</div>
<div class="form-group">
<label>Autisti</label>
<br>
<ng-container *ngFor="let user of users">
<div class="form-check" *ngIf="user.driver">
<input class="form-check-input driver-{{ user.id }}" type="checkbox" value='{{ user.id }}'>
<label class="form-check-label" for="driver-{{ user.id }}">
{{ user.name }}
</label>
</div>
</ng-container>
</div>
<div class="form-group">
<label>Altri membri della squadra</label>
<br>
<ng-container *ngFor="let user of users">
<div class="form-check" *ngIf="!user.chief && !user.driver">
<input class="form-check-input crew-{{ user.id }}" type="checkbox" value='{{ user.id }}'>
<label class="form-check-label" for="crew-{{ user.id }}">
{{ user.name }}
</label>
</div>
</ng-container>
</div>
<label>Luogo dell'intervento</label>
<div id="map" style="height: 300px;" leaflet [leafletOptions]="options" (leafletMapReady)="mapReady($event)"></div>
<div id="search" class="mt-2">
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Luogo">
<button class="btn btn-outline-secondary" type="button">Cerca</button>
</div>
<div id="results"></div>
</div>
<div class="form-group">
<label for="notes">Note (es. altre informazioni)</label><br>
<textarea class="form-control" id="notes"></textarea>
</div>
<br>
<div class="form-group">
<label>Tipologia</label>
<br>
<div class="input-group">
<select class="form-control mr-2">
<option selected disabled>Seleziona tipologia..</option>
<option *ngFor="let type of types" value="{{ type.id }}">{{ type.name }}</option>
</select>
<button class="btn btn-outline-secondary" type="button" tabindex="-1" (click)="addingType = true">
Aggiungi
</button>
</div>
<div class="input-group mb-2 mt-2" *ngIf="addingType">
<input type="text" class="form-control" placeholder="Nome della tipologia" [(ngModel)]="newType" [ngModelOptions]="{standalone: true}">
<button class="btn btn-secondary" type="button" (click)="addType()">Invia</button>
</div>
</div>
<br>
<button id="submit_button" type="submit" class="btn btn-primary">Invia</button>
</div>
</form>
<div class="form-group has-validation">
<label for="date-picker">Fine</label>
<input formControlName="end" [class.is-invalid]="!isFieldValid('end')" type="text"
placeholder="Premi per selezionare data e ora" class="form-control" bsDatepicker
[bsConfig]="{ adaptivePosition: true, withTimepicker: true, dateInputFormat: 'DD/MM/YYYY, hh:mm' }">
<div class="invalid-feedback" *ngIf="end.errors?.['required']">
Seleziona data e ora di fine dell'intervento
</div>
</div>
<div class="form-group has-validation">
<label for="progressivo">Progressivo</label>
<input formControlName="code" [class.is-invalid]="!isFieldValid('code')" id="progressivo" class="form-control"
type="text" placeholder="1234/5">
<div class="invalid-feedback" *ngIf="code.errors?.['required']">
Inserisci il progressivo dell'intervento
</div>
</div>
<div class="form-group has-validation" [class.is-invalid-div]="!isFieldValid('chief')">
<label>Caposquadra</label>
<br>
<ng-container *ngFor="let user of users">
<div class="form-check" *ngIf="user.chief">
<input formControlName="chief" [class.is-invalid]="!isFieldValid('chief')"
class="form-check-input chief-{{ user.id }}" type="radio" value='{{ user.id }}'>
<label class="form-check-label" for="chief-{{ user.id }}">
{{ user.name }}
</label>
</div>
</ng-container>
</div>
<div class="form-group has-validation" [class.is-invalid-div]="!isFieldValid('drivers')">
<label>Autisti</label>
<br>
<ng-container *ngFor="let user of users">
<div class="form-check" *ngIf="user.driver">
<input class="form-check-input driver-{{ user.id }}" [class.is-invalid]="!isFieldValid('drivers')"
(change)="onDriversCheckboxChange($event)" [checked]="isDriverSelected(user.id)" type="checkbox" value='{{ user.id }}'>
<label class="form-check-label" for="driver-{{ user.id }}">
{{ user.name }}
</label>
</div>
</ng-container>
</div>
<div class="form-group has-validation" [class.is-invalid-div]="!isFieldValid('crew')">
<label>Altri membri della squadra</label>
<br>
<ng-container *ngFor="let user of users">
<div class="form-check" *ngIf="!user.chief && !user.driver">
<input class="form-check-input crew-{{ user.id }}" [class.is-invalid]="!isFieldValid('crew')"
(change)="onCrewCheckboxChange($event)" [checked]="isCrewSelected(user.id)" type="checkbox" value='{{ user.id }}'>
<label class="form-check-label" for="crew-{{ user.id }}">
{{ user.name }}
</label>
</div>
</ng-container>
</div>
<div [class.is-invalid-div]="!isFieldValid('place')" class="mb-2">
<label>Luogo dell'intervento</label>
<map-picker (onMarkerSet)="setPlace($event.lat, $event.lng)"></map-picker>
</div>
<div class="form-group">
<label for="notes">Note (es. altre informazioni)</label><br>
<textarea formControlName="notes" class="form-control" id="notes"></textarea>
</div>
<br>
<div class="form-group">
<label>Tipologia</label>
<br>
<div class="input-group has-validation">
<select formControlName="type" [class.is-invalid]="!isFieldValid('type')" class="form-control mr-2">
<option selected disabled>Seleziona tipologia..</option>
<option *ngFor="let service_type of types" value="{{ service_type.id }}">{{ service_type.name }}</option>
</select>
<button class="btn btn-outline-secondary" type="button" tabindex="-1" (click)="addingType = true">
Aggiungi
</button>
<div class="invalid-feedback" *ngIf="type.errors?.['required']">
Seleziona una tipologia di intervento
</div>
</div>
<div class="input-group mb-2 mt-2" *ngIf="addingType">
<input type="text" class="form-control" placeholder="Nome della tipologia" [(ngModel)]="newType"
[ngModelOptions]="{standalone: true}">
<button class="btn btn-secondary" type="button" (click)="addType()">Invia</button>
</div>
</div>
<br>
<button id="submit_button" type="submit" class="btn btn-primary" [disabled]="submittingForm">Invia</button>
<button class="btn" type="button" (click)="formReset()" [disabled]="submittingForm">Reset</button>
<div class="d-flex justify-content-center mt-2 pt-2 mb-3" *ngIf="submittingForm">
<div class="spinner spinner-border"></div>
</div>
</div>
</form>

View File

@ -3,4 +3,7 @@
}
.form-check-input[type="checkbox"] {
margin-top: 0.5em;
}
.is-invalid-div {
border: 1px solid #dc3545;
}

View File

@ -1,9 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormBuilder, Validators } from '@angular/forms';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { ToastrService } from 'ngx-toastr';
import { latLng, tileLayer } from 'leaflet';
import "leaflet.locatecontrol";
@Component({
selector: 'app-edit-service',
@ -11,26 +10,78 @@ import "leaflet.locatecontrol";
styleUrls: ['./edit-service.component.scss']
})
export class EditServiceComponent implements OnInit {
public serviceId: string | undefined;
public users: any[] = [];
public types: any[] = [];
public addingType = false;
public newType = "";
public options = {
layers: [
tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: '...' })
],
zoom: 5,
center: latLng(46.879966, -121.726909)
addingService = false;
serviceId: string | undefined;
loadedService = {
start: '',
end: '',
code: '',
chief: '',
drivers: [],
crew: [],
place: '',
notes: '',
type: ''
};
users: any[] = [];
types: any[] = [];
place_lat: number = 0;
place_lng: number = 0;
addingType = false;
newType = "";
serviceForm: any;
private formSubmitAttempt: boolean = false;
submittingForm = false;
get start() { return this.serviceForm.get('start'); }
get end() { return this.serviceForm.get('end'); }
get code() { return this.serviceForm.get('code'); }
get chief() { return this.serviceForm.get('chief'); }
get drivers() { return this.serviceForm.get('drivers'); }
get crew() { return this.serviceForm.get('crew'); }
get place() { return this.serviceForm.get('place'); }
get type() { return this.serviceForm.get('type'); }
ngOnInit() {
this.serviceForm = this.fb.group({
start: [this.loadedService.start, [Validators.required]],
end: [this.loadedService.end, [Validators.required]],
code: [this.loadedService.code, [Validators.required, Validators.minLength(3)]],
chief: [this.loadedService.chief, [Validators.required]],
drivers: [this.loadedService.drivers, [Validators.required]],
crew: [this.loadedService.crew, [Validators.required]],
place: [this.loadedService.place, [Validators.required, Validators.minLength(3)]],
notes: [this.loadedService.notes],
type: [this.loadedService.type, [Validators.required, Validators.minLength(1)]]
});
}
constructor(
private route: ActivatedRoute,
private api: ApiClientService,
private toastr: ToastrService
private toastr: ToastrService,
private fb: FormBuilder
) {
this.route.paramMap.subscribe(params => {
this.serviceId = params.get('id') || undefined;
if(this.serviceId === "new") {
this.addingService = true;
} else {
this.api.get(`services/${this.serviceId}`).then((service) => {
this.loadedService = service;
let patch = Object.assign({}, service);
patch.start = new Date(parseInt(patch.start));
patch.end = new Date(parseInt(patch.end));
patch.drivers = patch.drivers.split(";");
patch.crew = patch.crew.split(";");
this.serviceForm.patchValue(patch);
});
}
console.log(this.serviceId);
});
this.api.get("users").then((users) => {
@ -47,8 +98,6 @@ export class EditServiceComponent implements OnInit {
});
}
ngOnInit(): void { }
addType() {
if(this.newType.length < 2) {
this.toastr.error("Il nome della tipologia deve essere lungo almeno 2 caratteri");
@ -69,9 +118,64 @@ export class EditServiceComponent implements OnInit {
});
}
mapReady(map: any) {
console.log(map);
(window as any).L.control.locate().addTo(map);
onDriversCheckboxChange(event: any) {
if (event.target.checked) {
this.drivers.setValue([...this.drivers.value, event.target.value]);
} else {
this.drivers.setValue(this.drivers.value.filter((x: number) => x !== event.target.value));
}
}
isDriverSelected(id: number) {
return this.drivers.value.find((x: number) => x == id);
}
onCrewCheckboxChange(event: any) {
if (event.target.checked) {
this.crew.setValue([...this.crew.value, event.target.value]);
} else {
this.crew.setValue(this.crew.value.filter((x: number) => x !== event.target.value));
}
}
isCrewSelected(id: number) {
return this.crew.value.find((x: number) => x == id);
}
setPlace(lat: number, lng: number) {
this.place_lat = lat;
this.place_lng = lng;
this.place.setValue(lat + ";" + lng);
console.log("Place selected", lat, lng);
}
//https://loiane.com/2017/08/angular-reactive-forms-trigger-validation-on-submit/
isFieldValid(field: string) {
return this.formSubmitAttempt ? this.serviceForm.get(field).valid : true;
}
formSubmit() {
this.formSubmitAttempt = true;
if(this.serviceForm.valid) {
this.submittingForm = true;
let values = Object.assign({}, this.serviceForm.value);
values.start = values.start.getTime();
values.end = values.end.getTime();
values.drivers = values.drivers.join(";");
values.crew = values.crew.join(";");
console.log(values);
this.api.post("services", values).then((res) => {
console.log(res);
this.toastr.success("Intervento aggiunto con successo.");
this.submittingForm = false;
}).catch((err) => {
console.error(err);
this.toastr.error("Errore durante l'aggiunta dell'intervento");
this.submittingForm = false;
});
}
}
formReset() {
this.formSubmitAttempt = false;
this.serviceForm.reset();
}
}

View File

@ -0,0 +1,26 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { MapPickerModule } from '../map-picker/map-picker.module';
import { BackBtnModule } from '../back-btn/back-btn.module';
import { EditServiceRoutingModule } from './edit-service-routing.module';
import { EditServiceComponent } from './edit-service.component';
@NgModule({
declarations: [
EditServiceComponent
],
imports: [
CommonModule,
EditServiceRoutingModule,
FormsModule,
ReactiveFormsModule,
BsDatepickerModule.forRoot(),
MapPickerModule,
BackBtnModule
]
})
export class EditServiceModule { }

View File

@ -1,2 +1,2 @@
<owner-image></owner-image>
<app-table [sourceType]="'logs'"></app-table>
<app-table [sourceType]="'logs'" [refreshInterval]="1200000"></app-table>

View File

@ -0,0 +1,12 @@
<div id="map" style="height: 300px;" leaflet [leafletOptions]="options" (leafletMapReady)="mapReady($event)" (leafletClick)="mapClick($event)">
<div *ngIf="isMarkerSet" [leafletLayer]="marker"></div>
</div>
<div id="search" class="mt-2 mb-3">
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Luogo" [(ngModel)]="placeName" (keyup.enter)="searchPlace()">
<button class="btn btn-outline-secondary" type="button" (click)="searchPlace()">Cerca</button>
</div>
<div id="results" *ngIf="isPlaceSearchResultsOpen">
<li *ngFor="let result of placeSearchResults" (click)="selectPlace(result)">{{ result.display_name }}</li>
</div>
</div>

View File

@ -0,0 +1,111 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { LatLng, latLng, tileLayer, Marker, Map } from 'leaflet';
import "leaflet.locatecontrol";
@Component({
selector: 'map-picker',
templateUrl: './map-picker.component.html',
styleUrls: ['./map-picker.component.scss']
})
export class MapPickerComponent implements OnInit {
@Input() lat = 45.88283872530;
@Input() lng = 10.18226623535;
@Output() onMarkerSet = new EventEmitter<any>();
options = {
layers: [
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' })
],
zoom: 10,
center: latLng(this.lat, this.lng)
};
isMarkerSet = false;
marker: Marker;
map: Map;
lc: any;
placeName = "";
isPlaceSearchResultsOpen = false;
placeSearchResults: any[] = [];
constructor(private toastr: ToastrService, private api: ApiClientService) {
this.marker = (window as any).L.marker(latLng(0,0));
this.map = undefined as unknown as Map;
}
ngOnInit(): void { }
setMarker(latLng: LatLng) {
this.onMarkerSet.emit({
lat: latLng.lat,
lng: latLng.lng
});
const iconRetinaUrl = "./assets/icons/marker-icon-2x.png";
const iconUrl = "./assets/icons/marker-icon.png";
const shadowUrl = "./assets/icons/marker-shadow.png";
const iconDefault = new (window as any).L.Icon({
iconRetinaUrl,
iconUrl,
shadowUrl,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
tooltipAnchor: [16, -28],
shadowSize: [41, 41]
});
this.marker.remove();
this.marker = (window as any).L.marker(latLng, { icon: iconDefault });
this.isMarkerSet = true;
}
mapReady(map: any) {
this.map = map;
const this_class = this;
(window as any).L.Control.CustomLocate = (window as any).L.Control.Locate.extend({
_drawMarker: function () {
this_class.setMarker(this._event.latlng);
},
_onDrag: function () {},
_onZoom: function () {},
_onZoomEnd: function () {}
});
this.lc = new (window as any).L.Control.CustomLocate({
cacheLocation: false, // disabled for privacy reasons
initialZoomLevel: 16
}).addTo(map);
}
mapClick(e: any) {
console.log(e);
this.setMarker(e.latlng);
}
searchPlace() {
if(this.placeName.length < 3) {
this.toastr.error("Il nome della località deve essere di almeno 3 caratteri");
return;
}
this.api.get("places/search", {
q: this.placeName
}).then((places) => {
this.isPlaceSearchResultsOpen = true;
this.placeSearchResults = places;
}).catch((err) => {
console.error(err);
this.toastr.error("Errore di caricamento dei risultati della ricerca. Riprovare più tardi");
});
}
selectPlace(place: any) {
console.log(place);
let latlng = latLng(place.lat, place.lon);
this.setMarker(latlng);
this.map.setView(latlng, 16);
}
}

View File

@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { LeafletModule } from '@asymmetrik/ngx-leaflet';
import { MapPickerComponent } from './map-picker.component';
@NgModule({
declarations: [
MapPickerComponent
],
imports: [
CommonModule,
FormsModule,
LeafletModule
],
exports: [
MapPickerComponent
]
})
export class MapPickerModule { }

View File

@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PlaceDetailsComponent } from './place-details.component';
const routes: Routes = [{ path: ':lat/:lng', component: PlaceDetailsComponent }];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class PlaceDetailsRoutingModule { }

View File

@ -0,0 +1,35 @@
<back-btn></back-btn>
<div class="d-flex justify-content-center mt-5 pt-5" *ngIf="!place_loaded">
<div class="spinner spinner-border"></div>
</div>
<br>
<div style="height: 300px;" leaflet [leafletOptions]="options" *ngIf="place_loaded">
<div [leafletLayers]="layers"></div>
</div>
<div class="place_info" *ngIf="place_loaded">
<h3>
<a href="https://www.google.com/maps/@?api=1&map_action=map&center={{ lat }},{{ lng }}&zoom=19&basemap=satellite" target="_blank">Apri il luogo in Google Maps</a>
</h3>
<br>
<h4 *ngIf="place_info.place_name">
Nome: <b>{{ place_info.place_name }}</b>
</h4>
<h4 *ngIf="place_info.building_service_name">
Nome del luogo: <b>{{ place_info.building_service_name }}</b>
</h4>
<h4 *ngIf="place_info.house_number">
Numero civico: <b>{{ place_info.house_number }}</b>
</h4>
<h4 *ngIf="place_info.road">
Strada: <b>{{ place_info.road }}</b>
</h4>
<h4 *ngIf="place_info.village">
Comune: <b>{{ place_info.village }}</b> (CAP <b>{{ place_info.postcode }}</b>)
</h4>
<h4 *ngIf="place_info.hamlet">
Frazione: <b>{{ place_info.hamlet }}</b>
</h4>
<h4 *ngIf="place_info.municipality">
Raggruppamento del Comune: <b>{{ place_info.municipality }}</b>
</h4>
</div>

View File

@ -0,0 +1,3 @@
.place_info {
margin: 2em;
}

View File

@ -0,0 +1,66 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { marker, latLng, tileLayer } from 'leaflet';
@Component({
selector: 'place-details',
templateUrl: './place-details.component.html',
styleUrls: ['./place-details.component.scss']
})
export class PlaceDetailsComponent implements OnInit {
lat: number = 0;
lng: number = 0;
place_info: any = {};
place_loaded = false;
options = {};
layers: any[] = [];
constructor(private route: ActivatedRoute, private api: ApiClientService){
this.route.paramMap.subscribe(params => {
this.lat = parseFloat(params.get('lat') || '');
this.lng = parseFloat(params.get('lng') || '');
console.log(this.lat, this.lng);
this.options = {
layers: [
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' })
],
zoom: 17,
center: latLng(this.lat, this.lng)
};
const iconRetinaUrl = "./assets/icons/marker-icon-2x.png";
const iconUrl = "./assets/icons/marker-icon.png";
const shadowUrl = "./assets/icons/marker-shadow.png";
const iconDefault = new (window as any).L.Icon({
iconRetinaUrl,
iconUrl,
shadowUrl,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
tooltipAnchor: [16, -28],
shadowSize: [41, 41]
});
this.layers = [
marker([this.lat, this.lng], {
icon: iconDefault
})
];
});
this.api.get("place_details", {
lat: this.lat,
lng: this.lng
}).then((place_info) => {
this.place_info = place_info;
console.log(this.place_info);
this.place_loaded = true;
});
}
ngOnInit() { }
}

View File

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LeafletModule } from '@asymmetrik/ngx-leaflet';
import { BackBtnModule } from '../back-btn/back-btn.module';
import { PlaceDetailsRoutingModule } from './place-details-routing.module';
import { PlaceDetailsComponent } from './place-details.component';
@NgModule({
declarations: [
PlaceDetailsComponent
],
imports: [
CommonModule,
PlaceDetailsRoutingModule,
LeafletModule,
BackBtnModule
]
})
export class PlaceDetailsModule { }

View File

@ -1,5 +1,5 @@
<owner-image></owner-image>
<div class="text-center mb-4">
<button type="button" class="btn btn-primary" (click)="addService()" disabled>Aggiungi intervento</button>
<button type="button" class="btn btn-primary" (click)="addService()">Aggiungi intervento</button>
</div>
<app-table [sourceType]="'services'"></app-table>
<app-table [sourceType]="'services'" [refreshInterval]="1200000"></app-table>

View File

@ -4,12 +4,12 @@
<tr>
<th>Nome</th>
<th>Disponibile</th>
<ng-container *ngIf="auth.profile.full_viewer">
<th>Autista</th>
<ng-container *ngIf="auth.profile.full_viewer">
<th>Chiama</th>
</ng-container>
<th>Interventi</th>
<th>Minuti disponibilità</th>
</ng-container>
</tr>
</thead>
<tbody id="table_body">
@ -24,16 +24,14 @@
<i class="fa fa-check" style="color:green" *ngIf="row.available"></i>
<i class="fa fa-times" style="color:red" *ngIf="!row.available"></i>
</td>
<ng-container *ngIf="auth.profile.full_viewer">
<td>
<img alt="driver" src="./assets/icons/wheel.png" width="20px" *ngIf="row.driver">
</td>
<td>
<td *ngIf="auth.profile.full_viewer">
<a href="tel:{{row.phone_number}}"><i class="fa fa-phone"></i></a>
</td>
<td>{{ row.services }}</td>
<td>{{ row.availability_minutes }}</td>
</ng-container>
</tr>
</tbody>
</table>
@ -58,34 +56,36 @@
<table *ngIf="sourceType === 'services'" id="table" class="table table-striped table-bordered dt-responsive nowrap">
<thead>
<tr>
<th>Data</th>
<th>Inizio</th>
<th>Fine</th>
<th>Codice</th>
<th>Tempo inizio</th>
<th>Tempo fine</th>
<th>Caposquadra</th>
<th>Autisti</th>
<th>Altre persone</th>
<th>Luogo</th>
<th>Note</th>
<th>Tipo</th>
<th>Modifica</th>
<th hidden>Modifica</th>
<th>Rimuovi</th>
</tr>
</thead>
<tbody id="table_body">
<tr *ngFor="let row of data">
<td>{{ row.date | date: 'MM/dd/yyyy HH:mm' }}</td>
<td>{{ row.start | date:'dd/MM/YYYY, hh:mm' }}</td>
<td>{{ row.end | date:'dd/MM/YYYY, hh:mm' }}</td>
<td>{{ row.code }}</td>
<td>{{ row.beginning }}</td>
<td>{{ row.end }}</td>
<td>{{ row.chief }}</td>
<td>{{ row.drivers }}</td>
<td>{{ row.crew }}</td>
<td>{{ row.place }}</td>
<td>
<img class="cursor-pointer" src="./api/place_image?lat={{ row.lat }}&lng={{ row.lng }}" (click)="openPlaceDetails(row.lat, row.lng)">
<br>
{{ row.place_name }}
</td>
<td>{{ row.notes }}</td>
<td>{{ row.type }}</td>
<td><i class="fa fa-edit"></i></td>
<td><i class="fa fa-trash"></i></td>
<td hidden><i class="fa fa-edit"></i></td>
<td (click)="deleteService(row.id)"><i class="fa fa-trash"></i></td>
</tr>
</tbody>
</table>
@ -100,8 +100,8 @@
<th>Altre persone</th>
<th>Luogo</th>
<th>Note</th>
<th>Modifica</th>
<th>Rimuovi</th>
<th hidden>Modifica</th>
<th hidden>Rimuovi</th>
</tr>
</thead>
<tbody id="table_body">
@ -114,8 +114,8 @@
<td>{{ row.crew }}</td>
<td>{{ row.place }}</td>
<td>{{ row.notes }}</td>
<td><i class="fa fa-edit"></i></td>
<td><i class="fa fa-trash"></i></td>
<td hidden><i class="fa fa-edit"></i></td>
<td hidden><i class="fa fa-trash"></i></td>
</tr>
</tbody>
</table>

View File

@ -19,3 +19,11 @@ th, td {
.table > :not(:first-child) {
border-top: none;
}
img {
max-width: 20em;
}
.cursor-pointer {
cursor: pointer;
}

View File

@ -1,7 +1,9 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
import { TableType } from 'src/app/_models/TableType';
import { Router } from '@angular/router';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { AuthService } from '../../_services/auth.service';
import { ToastrService } from 'ngx-toastr';
import Swal from 'sweetalert2';
@Component({
selector: 'app-table',
@ -19,14 +21,19 @@ export class TableComponent implements OnInit, OnDestroy {
public loadDataInterval: NodeJS.Timer | undefined = undefined;
constructor(public apiClient: ApiClientService, public auth: AuthService) {}
constructor(
private api: ApiClientService,
public auth: AuthService,
private router: Router,
private toastr: ToastrService
) { }
getTime() {
return Math.floor(Date.now() / 1000);
}
loadTableData() {
this.apiClient.get(this.sourceType || "list").then((data: any) => {
this.api.get(this.sourceType || "list").then((data: any) => {
console.log(data);
this.data = data.filter((row: any) => {
if(typeof row.hidden !== 'undefined') return !row.hidden;
@ -39,6 +46,9 @@ export class TableComponent implements OnInit, OnDestroy {
console.log(this.sourceType);
this.loadTableData();
this.loadDataInterval = setInterval(() => {
if(typeof (window as any).skipTableReload !== 'undefined' && (window as any).skipTableReload) {
return;
}
console.log("Refreshing data...");
this.loadTableData();
}, this.refreshInterval || 10000);
@ -55,4 +65,31 @@ export class TableComponent implements OnInit, OnDestroy {
this.changeAvailability.emit({user, newState});
}
}
openPlaceDetails(lat: number, lng: number) {
this.router.navigate(['/place-details', lat, lng]);
}
deleteService(id: number) {
console.log(id);
Swal.fire({
title: 'Sei del tutto sicuro di voler rimuovere l\'intervento?',
text: "Gli interventi eliminati non si possono recuperare.",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Si, rimuovilo',
cancelButtonText: 'Annulla'
}).then((result) => {
if (result.isConfirmed) {
this.api.delete(`services/${id}`).then((response) => {
this.toastr.success('Intervento rimosso con successo.');
this.loadTableData();
}).catch((e) => {
this.toastr.error('Errore durante la rimozione dell\'intervento.');
});
}
})
}
}

View File

@ -2,4 +2,4 @@
<div class="text-center mb-4">
<button type="button" class="btn btn-primary" disabled>Aggiungi esercitazione</button>
</div>
<app-table [sourceType]="'trainings'"></app-table>
<app-table [sourceType]="'trainings'" [refreshInterval]="1200000"></app-table>

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
@Injectable({
providedIn: 'root'
@ -26,6 +26,9 @@ export class ApiClientService {
}
public apiEndpoint(endpoint: string): string {
if(endpoint.startsWith('https')) {
return endpoint;
}
return this.apiRoot + endpoint;
}
@ -39,9 +42,12 @@ export class ApiClientService {
}, new URLSearchParams()).toString();
}
public get(endpoint: string) {
public get(endpoint: string, data: any = {}) {
return new Promise<any>((resolve, reject) => {
this.http.get(this.apiEndpoint(endpoint), this.requestOptions).subscribe((data: any) => {
this.http.get(this.apiEndpoint(endpoint), {
...this.requestOptions,
params: new HttpParams({ fromObject: data })
}).subscribe((data: any) => {
resolve(data);
}, (err) => {
reject(err);

View File

@ -0,0 +1,27 @@
import { Injectable } from '@angular/core'
import { Location } from '@angular/common'
import { Router, NavigationEnd } from '@angular/router'
/* Based on https://nils-mehlhorn.de/posts/angular-navigate-back-previous-page */
@Injectable({ providedIn: 'root' })
export class LocationBackService {
private history: string[] = [];
constructor(private router: Router, private location: Location) {
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd && !event.urlAfterRedirects.includes('login')) {
this.history.push(event.urlAfterRedirects);
}
})
}
goBack(): void {
this.history.pop();
if (this.history.length > 0) {
this.location.back();
} else {
this.router.navigateByUrl('/list');
}
}
}

View File

@ -4,7 +4,6 @@ import { RouterModule, Routes } from '@angular/router';
import { ListComponent } from './_components/list/list.component';
import { LogsComponent } from './_components/logs/logs.component';
import { ServicesComponent } from './_components/services/services.component';
//import { EditServiceComponent } from './_components/edit-service/edit-service.component';
import { TrainingsComponent } from './_components/trainings/trainings.component';
import { AuthorizeGuard } from './_guards/authorize.guard';
@ -14,7 +13,16 @@ const routes: Routes = [
{ path: 'list', component: ListComponent, canActivate: [AuthorizeGuard] },
{ path: 'logs', component: LogsComponent, canActivate: [AuthorizeGuard] },
{ path: 'services', component: ServicesComponent, canActivate: [AuthorizeGuard] },
//{ path: 'services/:id', component: EditServiceComponent, canActivate: [AuthorizeGuard] },
{
path: 'place-details',
loadChildren: () => import('./_components/place-details/place-details.module').then(m => m.PlaceDetailsModule),
canActivate: [AuthorizeGuard]
},
{
path: 'services/:id',
loadChildren: () => import('./_components/edit-service/edit-service.module').then(m => m.EditServiceModule),
canActivate: [AuthorizeGuard]
},
{ path: 'trainings', component: TrainingsComponent, canActivate: [AuthorizeGuard] },
{ path: "login/:redirect/:extraParam", component: LoginComponent },
{ path: "login/:redirect", component: LoginComponent },

View File

@ -7,6 +7,10 @@
<a class="icon" id="menuButton" (click)="menuButtonClicked = !menuButtonClicked"></a>
</div>
<div class="d-flex justify-content-center mt-4 pt-4 mb-3" *ngIf="loadingRoute">
<div class="spinner spinner-border"></div>
</div>
<router-outlet></router-outlet>
<div id="footer" class="footer text-center p-3">

View File

@ -1,6 +1,8 @@
import { Component } from '@angular/core';
import { AuthService } from './_services/auth.service';
import { LocationBackService } from 'src/app/_services/locationBack.service';
import { versions } from 'src/environments/versions';
import { Router, RouteConfigLoadStart, RouteConfigLoadEnd } from '@angular/router';
@Component({
selector: 'app-root',
@ -11,8 +13,23 @@ export class AppComponent {
public menuButtonClicked = false;
public revision_datetime_string;
public versions = versions;
public loadingRoute = false;
constructor(public auth: AuthService) {
constructor(
public auth: AuthService,
private locationBackService: LocationBackService,
private router: Router
) {
this.revision_datetime_string = new Date(versions.revision_timestamp).toLocaleString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' });
}
ngOnInit () {
this.router.events.subscribe((event) => {
if (event instanceof RouteConfigLoadStart) {
this.loadingRoute = true;
} else if (event instanceof RouteConfigLoadEnd) {
this.loadingRoute = false;
}
});
}
}

View File

@ -6,8 +6,6 @@ import { FormsModule } from '@angular/forms';
import { ToastrModule } from 'ngx-toastr';
import { ModalModule } from 'ngx-bootstrap/modal';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { LeafletModule } from '@asymmetrik/ngx-leaflet';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@ -23,7 +21,6 @@ import { LoginComponent } from './_components/login/login.component';
import { ListComponent } from './_components/list/list.component';
import { LogsComponent } from './_components/logs/logs.component';
import { ServicesComponent } from './_components/services/services.component';
//import { EditServiceComponent } from './_components/edit-service/edit-service.component';
import { TrainingsComponent } from './_components/trainings/trainings.component';
import { UnauthorizedInterceptor } from './_providers/unauthorized-interceptor.provider';
@ -41,7 +38,6 @@ import { UnauthorizedInterceptor } from './_providers/unauthorized-interceptor.p
ListComponent,
LogsComponent,
ServicesComponent,
//EditServiceComponent,
TrainingsComponent
],
imports: [
@ -58,8 +54,6 @@ import { UnauthorizedInterceptor } from './_providers/unauthorized-interceptor.p
}),
ModalModule.forRoot(),
TooltipModule.forRoot(),
BsDatepickerModule.forRoot(),
//LeafletModule,
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: false && environment.production,
// Register the ServiceWorker as soon as the app is stable

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

View File

@ -3,9 +3,11 @@
@import "~bootstrap/scss/bootstrap.scss";
@import "~@fortawesome/fontawesome-free/css/all.css";
@import '~ngx-toastr/toastr';
//Leaving this here because in component.scss it doesn't work really well
@import '~ngx-bootstrap/datepicker/bs-datepicker.scss';
//@import '~leaflet/dist/leaflet.css';
//@import '~leaflet.locatecontrol/dist/L.Control.Locate.min.css';
@import '~leaflet/dist/leaflet.css';
@import '~leaflet.locatecontrol/dist/L.Control.Locate.min.css';
.fa {
vertical-align: middle;

View File

@ -8,6 +8,33 @@ categories:
- data-analytics
- data-collection
- data-visualization
dependsOn:
open:
- name: MariaDB
optional: false
version: ''
versionMax: ''
versionMin: ''
- name: Apache2
optional: false
version: ''
versionMax: ''
versionMin: ''
- name: PHP
optional: false
version: ''
versionMax: ''
versionMin: 8.0.0
- name: Telegram
optional: true
version: ''
versionMax: ''
versionMin: ''
- name: Cron
optional: true
version: ''
versionMax: ''
versionMin: ''
description:
it:
features:
@ -18,6 +45,8 @@ description:
- Tabella di log di azioni effettuate dai diversi utenti
- Gestione della disponibilità per singolo utente
- Possibilità di registrare programmazioni orarie della disponibilità
- Rapida attivazione e disattivazione della programmazione oraria
- Integrazione del programma con bot Telegram personalizzato
genericName: software allertamento VVF Volontari
longDescription: |
Questa web-app viene utilizzata per gestire la disponibilità di squadre di
@ -29,7 +58,11 @@ description:
É possibile inoltre registrare interventi e esercitazioni svolte dai vigili, che saranno utilizzati per riordinare dinamicamente la tabella delle disponibilità.
Gli utenti hanno inoltre la possibilità di registrare delle fasce orarie
in cui la disponibilità deve essere automaticamente aggiornata.
in cui la disponibilità deve essere automaticamente aggiornata, attivabili rapidamente con un tasto apposito nella dashboard.
Il software include anche un modulo che permette l'attivazione (seguendo una procedura guidata) di un bot Telegram utilizzabile da tutti gli utenti,
attraverso il quale è possibile gestire la disponibilità e la programmazione oraria di questa, e ricevere notifiche.
Il bot è programmato per funzionare non sono con i comandi classici (preceduti da uno "/") ma anche con veri e propri messaggi testuali come "Attivo" "Non disponibile" "Elenco disponibili" e tanti altri.
shortDescription: |-
Software utilizzato per gestire la disponibilità di vigili del fuoco
volontari in caso di allertamento
@ -70,4 +103,4 @@ releaseDate: '2022-01-03'
roadmap: 'https://github.com/orgs/allerta-vvf/projects/1'
softwareType: standalone/web
softwareVersion: v0.0.1
url: 'https://github.com/allerta-vvf/allerta-vvf'
url: 'https://github.com/allerta-vvf/allerta-vvf'