commit
16c277d8fc
|
@ -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 : []);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -294,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',
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ function job_schedule_availability() {
|
|||
}
|
||||
}
|
||||
$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);
|
||||
|
@ -166,7 +166,7 @@ function job_send_notification_if_manual_mode() {
|
|||
foreach ($profiles as $profile) {
|
||||
$notified_users[] = $profiles["id"];
|
||||
$stato = $profile["available"] ? "disponibile" : "non disponibile";
|
||||
sendTelegramNotificationToUser("⚠️ Attenzione! La tua disponibilità non segue la programmazione oraria.\nAttualmente sei {$stato}.\nScrivi \"/programma\" se vuoi ripristinare la programmazione.", $profile["id"]);
|
||||
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";
|
||||
|
@ -203,4 +203,4 @@ function cronRouter (FastRoute\RouteCollector $r) {
|
|||
apiResponse(["excuted_actions" => $executed_actions]);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,14 +58,20 @@ 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"]]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -146,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"
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -194,7 +202,8 @@ function telegramBotRouter() {
|
|||
global $Bot, $availability;
|
||||
requireBotLogin($message);
|
||||
if(count(explode(" ", $message->text)) > 3) return;
|
||||
$availability->change_manual_mode(1);
|
||||
$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\"");
|
||||
});
|
||||
|
||||
|
@ -203,11 +212,11 @@ function telegramBotRouter() {
|
|||
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) {
|
||||
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 === 1) {
|
||||
} else if($available_users_count >= 0) {
|
||||
$message->reply("⚠️ Distaccamento non operativo");
|
||||
}
|
||||
});
|
||||
|
@ -216,7 +225,7 @@ function telegramBotRouter() {
|
|||
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) {
|
||||
|
|
|
@ -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,15 +259,16 @@ class Availability {
|
|||
$this->users = $users;
|
||||
}
|
||||
|
||||
public function change_manual_mode($manual_mode) {
|
||||
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" => $users->auth->getUserId()
|
||||
"id" => $user_id
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -286,25 +285,83 @@ class Availability {
|
|||
$change_values,
|
||||
["id" => $user_id]
|
||||
);
|
||||
|
||||
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");
|
||||
} else if($available_users_count === 2) {
|
||||
sendTelegramNotification("🧯 Distaccamento operativo per supporto");
|
||||
} else if($available_users_count === 1 && !$availability) {
|
||||
sendTelegramNotification("⚠️ Distaccamento non operativo");
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
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)"
|
||||
);
|
||||
|
@ -312,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)"
|
||||
);
|
||||
|
@ -319,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -380,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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<button (click)="locationBackService.goBack()" id="backBtn" title="Go back"><i class="fas fa-arrow-left"></i> Torna indietro</button>
|
|
@ -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;
|
||||
}
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
|
@ -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 { }
|
|
@ -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 { }
|
|
@ -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>
|
|
@ -3,4 +3,7 @@
|
|||
}
|
||||
.form-check-input[type="checkbox"] {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.is-invalid-div {
|
||||
border: 1px solid #dc3545;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { }
|
|
@ -1,2 +1,2 @@
|
|||
<owner-image></owner-image>
|
||||
<app-table [sourceType]="'logs'"></app-table>
|
||||
<app-table [sourceType]="'logs'" [refreshInterval]="1200000"></app-table>
|
|
@ -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>
|
|
@ -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: '© <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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 { }
|
|
@ -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 { }
|
|
@ -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¢er={{ 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>
|
|
@ -0,0 +1,3 @@
|
|||
.place_info {
|
||||
margin: 2em;
|
||||
}
|
|
@ -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: '© <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() { }
|
||||
}
|
|
@ -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 { }
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -19,3 +19,11 @@ th, td {
|
|||
.table > :not(:first-child) {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 20em;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -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.');
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 },
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue