Merge pull request #405 from allerta-vvf/master

New features
This commit is contained in:
Matteo Gheza 2021-12-29 13:20:19 +01:00 committed by GitHub
commit 7d554bdb65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1581 additions and 16332 deletions

View File

@ -1,6 +1,12 @@
<?php
require_once 'utils.php';
require_once 'cronRouter.php';
function apiRouter (FastRoute\RouteCollector $r) {
$r->addGroup('/cron', function (FastRoute\RouteCollector $r) {
cronRouter($r);
});
$r->addRoute(
'GET',
'/healthcheck',
@ -48,7 +54,11 @@ function apiRouter (FastRoute\RouteCollector $r) {
global $db, $users;
requireLogin() || accessDenied();
$users->online_time_update();
$response = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` ORDER BY available DESC, chief DESC, services ASC, availability_minutes ASC, name ASC");
if($users->hasRole(Role::FULL_VIEWER)) {
$response = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` ORDER BY available DESC, chief DESC, services ASC, availability_minutes ASC, name ASC");
} else {
$response = $db->select("SELECT `id`, `chief`, `online_time`, `available`, `name` FROM `".DB_PREFIX."_profiles` ORDER BY available DESC, chief DESC, services ASC, availability_minutes ASC, name ASC");
}
apiResponse(
!is_null($response) ? $response : []
);
@ -126,6 +136,9 @@ function apiRouter (FastRoute\RouteCollector $r) {
function ($vars) {
global $users;
requireLogin() || accessDenied();
if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){
exit;
}
apiResponse(["userId" => $users->add_user($_POST["email"], $_POST["name"], $_POST["username"], $_POST["password"], $_POST["phone_number"], $_POST["birthday"], $_POST["chief"], $_POST["driver"], $_POST["hidden"], $_POST["disabled"], "unknown")]);
}
);
@ -135,6 +148,9 @@ function apiRouter (FastRoute\RouteCollector $r) {
function ($vars) {
global $users;
requireLogin() || accessDenied();
if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){
exit;
}
apiResponse($users->get_user($vars["userId"]));
}
);
@ -144,6 +160,9 @@ function apiRouter (FastRoute\RouteCollector $r) {
function ($vars) {
global $users;
requireLogin() || accessDenied();
if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){
exit;
}
$users->remove_user($vars["userId"], "unknown");
apiResponse(["status" => "success"]);
}
@ -171,12 +190,15 @@ function apiRouter (FastRoute\RouteCollector $r) {
global $users, $db;
requireLogin() || accessDenied();
$users->online_time_update();
logger("Disponibilità cambiata in ".($_POST["available"] ? '"disponibile"' : '"non disponibile"'), is_numeric($_POST["id"]) ? $_POST["id"] : $users->auth->getUserId());
if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){
exit;
}
logger("Disponibilità cambiata in ".($_POST["available"] ? '"disponibile"' : '"non disponibile"'), is_numeric($_POST["id"]) ? $_POST["id"] : $users->auth->getUserId(), $users->auth->getUserId());
apiResponse([
"response" => $db->update(
DB_PREFIX.'_profiles',
[
'available' => $_POST['available'],
'available' => $_POST['available'], 'availability_last_change' => 'manual'
],
[
'id' => is_numeric($_POST["id"]) ? $_POST["id"] : $users->auth->getUserId()
@ -186,6 +208,30 @@ function apiRouter (FastRoute\RouteCollector $r) {
}
);
$r->addRoute(
['GET'],
'/schedules',
function ($vars) {
global $users, $schedules;
requireLogin() || accessDenied();
$users->online_time_update();
apiResponse($schedules->get());
}
);
$r->addRoute(
['POST'],
'/schedules',
function ($vars) {
global $users, $schedules;
requireLogin() || accessDenied();
$users->online_time_update();
$new_schedules = !is_string($_POST["schedules"]) ? json_encode($_POST["schedules"]) : $_POST["schedules"];
apiResponse([
"response" => $schedules->update($new_schedules)
]);
}
);
$r->addRoute(
['POST'],
'/login',

View File

@ -13,14 +13,14 @@
"delight-im/auth": "dev-master",
"ulrichsg/getopt-php": "4.0.0",
"nikic/fast-route": "^2.0@dev",
"spatie/array-to-xml": "2.16.0",
"ezyang/htmlpurifier": "4.13.0",
"spatie/array-to-xml": "3.1.0",
"ezyang/htmlpurifier": "4.14.0",
"brick/phonenumber": "0.4.0",
"sentry/sdk": "3.1.1",
"azuyalabs/yasumi": "2.4.0",
"ministryofweb/php-osm-tiles": "2.0.0",
"delight-im/db": "1.3.1",
"phpfastcache/phpfastcache": "8.0.8",
"phpfastcache/phpfastcache": "9.0.1",
"skrtdev/novagram": "1.10",
"league/mime-type-detection": "1.9.0"
},

144
backend/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "122d9ea31cd8f0cdc173a0d102dc599e",
"content-hash": "c24f492bcd977f01ae44c4ee0c7ae1e4",
"packages": [
{
"name": "azuyalabs/yasumi",
@ -207,12 +207,12 @@
"source": {
"type": "git",
"url": "https://github.com/allerta-vvf/PHP-Auth-JWT",
"reference": "ddb3236ae79fcd0e706d108332dbad9dcdffc2c6"
"reference": "3ea0aa3d7e74528c57932872bbda339e995a9d9a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/allerta-vvf/PHP-Auth-JWT/zipball/ddb3236ae79fcd0e706d108332dbad9dcdffc2c6",
"reference": "ddb3236ae79fcd0e706d108332dbad9dcdffc2c6",
"url": "https://api.github.com/repos/allerta-vvf/PHP-Auth-JWT/zipball/3ea0aa3d7e74528c57932872bbda339e995a9d9a",
"reference": "3ea0aa3d7e74528c57932872bbda339e995a9d9a",
"shasum": ""
},
"require": {
@ -240,7 +240,7 @@
"login",
"security"
],
"time": "2021-12-24T14:07:13+00:00"
"time": "2021-12-27T18:35:45+00:00"
},
{
"name": "delight-im/base64",
@ -334,24 +334,21 @@
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.13.0",
"version": "v4.14.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75"
"reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
"reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/12ab42bd6e742c70c0a52f7b82477fcd44e64b75",
"reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75",
"shasum": ""
},
"require": {
"php": ">=5.2"
},
"require-dev": {
"simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd"
},
"type": "library",
"autoload": {
"psr-0": {
@ -382,9 +379,9 @@
],
"support": {
"issues": "https://github.com/ezyang/htmlpurifier/issues",
"source": "https://github.com/ezyang/htmlpurifier/tree/master"
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.14.0"
},
"time": "2020-06-29T00:56:53+00:00"
"time": "2021-12-25T01:21:49+00:00"
},
{
"name": "giggsey/libphonenumber-for-php",
@ -1631,48 +1628,56 @@
},
{
"name": "phpfastcache/phpfastcache",
"version": "8.0.8",
"version": "9.0.1",
"source": {
"type": "git",
"url": "https://github.com/PHPSocialNetwork/phpfastcache.git",
"reference": "c413ffd8185564db3d670e20f9135497be9ebe85"
"reference": "bcd068ff1a78cd1475a4d8c13ed371711465ef40"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPSocialNetwork/phpfastcache/zipball/c413ffd8185564db3d670e20f9135497be9ebe85",
"reference": "c413ffd8185564db3d670e20f9135497be9ebe85",
"url": "https://api.github.com/repos/PHPSocialNetwork/phpfastcache/zipball/bcd068ff1a78cd1475a4d8c13ed371711465ef40",
"reference": "bcd068ff1a78cd1475a4d8c13ed371711465ef40",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": ">=7.3",
"psr/cache": "~1.0.0",
"psr/simple-cache": "~1.0.0"
"php": ">=8.0",
"psr/cache": "^2.0||^3.0",
"psr/simple-cache": "^2.0||^3.0"
},
"conflict": {
"doctrine/couchdb": "*"
"doctrine/couchdb": "<dev-master#9eeb9e5"
},
"require-dev": {
"ext-gettext": "*",
"league/climate": "^3.5"
"jetbrains/phpstorm-stubs": "dev-master",
"league/climate": "^3.5",
"phpmd/phpmd": "@stable",
"phpstan/phpstan": "^0.12.98",
"squizlabs/php_codesniffer": "~3.0"
},
"suggest": {
"ext-apc": "*",
"aws/aws-sdk-php": "~3.0",
"doctrine/couchdb": "^dev-master#9eeb9e5",
"ext-apcu": "*",
"ext-couchbase": "*",
"ext-couchbase_v3": "*",
"ext-intl": "*",
"ext-leveldb": "*",
"ext-memcache": "*",
"ext-memcached": "*",
"ext-mongodb": "*",
"ext-redis": "*",
"ext-sqlite": "*",
"ext-wincache": "*",
"ext-xcache": "*",
"google/cloud-firestore": "^1.20",
"mongodb/mongodb": "^1.9",
"phpfastcache/couchdb": "~1.0.0",
"phpfastcache/phpssdb": "~1.0.0",
"predis/predis": "^1.1"
"phpfastcache/phpssdb": "~1.1.0",
"predis/predis": "^1.1",
"triagens/arangodb": "^3.8"
},
"type": "library",
"autoload": {
@ -1693,15 +1698,14 @@
"role": "Actual Project Manager/Developer"
},
{
"name": "Khoa Bui",
"email": "khoaofgod@gmail.com",
"homepage": "https://www.phpfastcache.com",
"role": "Former Project Developer/Original Creator"
"name": "Contributors",
"homepage": "https://github.com/PHPSocialNetwork/phpfastcache/graphs/contributors"
}
],
"description": "PHP Abstract Cache Class - Reduce your database call using cache system. PhpFastCache handles a lot of drivers such as Apc(u), Cassandra, CouchBase, Couchdb, Mongodb, Files, (P)redis, Leveldb, Memcache(d), Ssdb, Sqlite, Wincache, Xcache, Zend Data Cache.",
"description": "PHP Abstract Cache Class - Reduce your database call using cache system. Phpfastcache handles a lot of drivers such as Apc(u), Cassandra, CouchBase, Couchdb, Mongodb, Files, (P)redis, Leveldb, Memcache(d), Ssdb, Sqlite, Wincache, Xcache, Zend Data Cache.",
"homepage": "https://www.phpfastcache.com",
"keywords": [
"ArangoDb",
"LevelDb",
"abstract",
"apc",
@ -1713,6 +1717,7 @@
"cookie",
"couchbase",
"couchdb",
"dynamodb",
"files cache",
"memcache",
"memcached",
@ -1733,36 +1738,33 @@
],
"support": {
"issues": "https://github.com/PHPSocialNetwork/phpfastcache/issues",
"source": "https://github.com/PHPSocialNetwork/phpfastcache/tree/8.0.8"
"source": "https://github.com/PHPSocialNetwork/phpfastcache",
"wiki": "https://github.com/PHPSocialNetwork/phpfastcache/wiki"
},
"funding": [
{
"url": "https://github.com/geolim4",
"type": "github"
},
{
"url": "https://www.patreon.com/geolim4",
"type": "patreon"
}
],
"time": "2021-08-18T01:26:03+00:00"
"time": "2021-11-13T23:59:22+00:00"
},
{
"name": "psr/cache",
"version": "1.0.1",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/cache.git",
"reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
"reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
"php": ">=8.0.0"
},
"type": "library",
"extra": {
@ -1782,7 +1784,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for caching libraries",
@ -1792,9 +1794,9 @@
"psr-6"
],
"support": {
"source": "https://github.com/php-fig/cache/tree/master"
"source": "https://github.com/php-fig/cache/tree/3.0.0"
},
"time": "2016-08-06T20:24:11+00:00"
"time": "2021-02-03T23:26:27+00:00"
},
{
"name": "psr/container",
@ -2061,25 +2063,25 @@
},
{
"name": "psr/simple-cache",
"version": "1.0.1",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/simple-cache.git",
"reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
"reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
"url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
"dev-master": "3.0.x-dev"
}
},
"autoload": {
@ -2094,7 +2096,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interfaces for simple caching",
@ -2106,9 +2108,9 @@
"simple-cache"
],
"support": {
"source": "https://github.com/php-fig/simple-cache/tree/master"
"source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
},
"time": "2017-10-23T01:57:42+00:00"
"time": "2021-10-29T13:26:27+00:00"
},
{
"name": "ralouphie/getallheaders",
@ -2212,16 +2214,16 @@
},
{
"name": "sentry/sentry",
"version": "3.3.4",
"version": "3.3.5",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-php.git",
"reference": "ecbd09ea5d053a202cf773cb24ab28af820831bd"
"reference": "c186c44c32899ad0cf5b4e942d71035f01b87b64"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getsentry/sentry-php/zipball/ecbd09ea5d053a202cf773cb24ab28af820831bd",
"reference": "ecbd09ea5d053a202cf773cb24ab28af820831bd",
"url": "https://api.github.com/repos/getsentry/sentry-php/zipball/c186c44c32899ad0cf5b4e942d71035f01b87b64",
"reference": "c186c44c32899ad0cf5b4e942d71035f01b87b64",
"shasum": ""
},
"require": {
@ -2229,7 +2231,7 @@
"ext-mbstring": "*",
"guzzlehttp/promises": "^1.4",
"guzzlehttp/psr7": "^1.7|^2.0",
"jean85/pretty-package-versions": "^1.5|^2.0.1",
"jean85/pretty-package-versions": "^1.5|^2.0.4",
"php": "^7.2|^8.0",
"php-http/async-client-implementation": "^1.0",
"php-http/client-common": "^1.5|^2.0",
@ -2300,7 +2302,7 @@
],
"support": {
"issues": "https://github.com/getsentry/sentry-php/issues",
"source": "https://github.com/getsentry/sentry-php/tree/3.3.4"
"source": "https://github.com/getsentry/sentry-php/tree/3.3.5"
},
"funding": [
{
@ -2312,7 +2314,7 @@
"type": "custom"
}
],
"time": "2021-11-08T08:44:00+00:00"
"time": "2021-12-27T12:31:24+00:00"
},
{
"name": "skrtdev/async",
@ -2421,25 +2423,25 @@
},
{
"name": "spatie/array-to-xml",
"version": "2.16.0",
"version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/array-to-xml.git",
"reference": "db39308c5236b69b89cadc3f44f191704814eae2"
"reference": "3090918cb441ad707660dd8bccc6dc46beb34380"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/array-to-xml/zipball/db39308c5236b69b89cadc3f44f191704814eae2",
"reference": "db39308c5236b69b89cadc3f44f191704814eae2",
"url": "https://api.github.com/repos/spatie/array-to-xml/zipball/3090918cb441ad707660dd8bccc6dc46beb34380",
"reference": "3090918cb441ad707660dd8bccc6dc46beb34380",
"shasum": ""
},
"require": {
"ext-dom": "*",
"php": "^7.4|^8.0"
"php": "^8.0"
},
"require-dev": {
"mockery/mockery": "^1.2",
"phpunit/phpunit": "^9.0",
"phpunit/phpunit": "^9.5",
"spatie/phpunit-snapshot-assertions": "^4.2"
},
"type": "library",
@ -2469,7 +2471,7 @@
],
"support": {
"issues": "https://github.com/spatie/array-to-xml/issues",
"source": "https://github.com/spatie/array-to-xml/tree/2.16.0"
"source": "https://github.com/spatie/array-to-xml/tree/3.1.0"
},
"funding": [
{
@ -2481,7 +2483,7 @@
"type": "github"
}
],
"time": "2020-11-18T22:03:17+00:00"
"time": "2021-09-12T17:08:24+00:00"
},
{
"name": "symfony/deprecation-contracts",
@ -3263,5 +3265,5 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.2.0"
"plugin-api-version": "2.1.0"
}

186
backend/cronRouter.php Normal file
View File

@ -0,0 +1,186 @@
<?php
require_once 'utils.php';
$executed_actions = [];
function job_reset_availability() {
global $db, $executed_actions;
$profiles = $db->select("SELECT * FROM `".DB_PREFIX."_profiles`");
if(!is_null($profiles) && count($profiles) > 0) {
$list = [];
foreach($profiles as $profile){
$list[] = [$profile["id"] => $profile["availability_minutes"]];
}
$db->insert(
DB_PREFIX."_minutes",
["month" => date("m"), "year" => date("Y"), "list"=> json_encode($list)]
);
$db->exec("UPDATE `".DB_PREFIX."_profiles` SET `availability_minutes` = 0");
$output = $list;
$output_status = "ok";
} else {
$output = ["profiles array empty"];
$output_status = "error";
}
$executed_actions[] = [
"title" => "Reset availability minutes",
"description" => "Reset availability minutes for all profiles",
"output" => $output,
"output_status" => $output_status
];
}
function job_increment_availability() {
global $db, $executed_actions;
$profiles = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `available` = 1");
if(!is_null($profiles) && count($profiles) > 0) {
$output = [];
$output[] = $profiles;
$output_status = "ok";
$queries = [];
foreach ($profiles as $row) {
$value = (int)$row["availability_minutes"]+5;
$id = $row["id"];
$increment[$id] = $value;
$count = $db->update(
DB_PREFIX."_profiles",
["availability_minutes" => $value],
["id" => $id]
);
$tmp = $id . " - " . $value . " ";
$tmp .= $count == 1 ? "success" : "fail";
$queries[] = $tmp;
}
$output[] = $queries;
} else {
$output = ["profiles array empty"];
$output_status = "ok";
}
$executed_actions[] = [
"title" => "Increment availability minutes",
"description" => "Increment availability minutes for all available profiles",
"output" => $output,
"output_status" => $output_status
];
}
function job_schedule_availability() {
global $db, $executed_actions;
$result = $db->select("SELECT * FROM `".DB_PREFIX."_schedules`;");
$schedules_check = [];
$schedules_users = [];
$schedules_check["schedules"] = [];
$schedules_check["users"] = [];
if(!empty($result)){
foreach ($result as $key => $value) {
$result[$key]["schedules"] = json_decode($result[$key]["schedules"], true);
}
$schedules_check["table"] = $result;
foreach ($result as $row) {
if(!is_null($row["last_exec"])){
$last_exec = [
"day" => (int) explode(";",$row["last_exec"])[0],
"hour" => (int) explode(":",explode(";",$row["last_exec"])[1])[0],
"minutes" => (int) explode(":",$row["last_exec"])[1]
];
} else {
$last_exec = null;
}
$id = $row["id"];
$user_id = $row["user"];
/*
$selected_holidays = json_decode($row["holidays"]);
$selected_holidays_dates = [];
foreach ($selected_holidays as $holiday){
$selected_holidays_dates[] = $user->holidays->getHoliday($holiday)->format('Y-m-d');
}
*/
foreach ($row["schedules"] as $value) {
$schedule = [
"day" => (int) $value["day"]+1,
"hour" => (int) explode(":",$value["hour"])[0],
"minutes" => (int) explode(":",$value["hour"])[1]
];
$now = [
"day" => (int) date("N"),
"hour" => (int) date("H"),
"minutes" => (int) date("i")
];
if(
$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]
);
$db->update(
DB_PREFIX."_profiles",
["available" => '1', "availability_last_change" => "cron"],
["id" => $user_id]
);
$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`");
foreach ($profiles as $profile) {
if(!in_array($profile["id"],$schedules_users)){
$db->update(
DB_PREFIX."_profiles",
["available" => 0],
["availability_last_change" => "cron", "id" => $profile["id"]]
);
}
}
$output = $schedules_check;
$output_status = "ok";
} else {
$output = ["schedules array empty"];
$output_status = "ok";
}
$executed_actions[] = [
"title" => "Schedule availability",
"description" => "Update availability for all users based on schedules",
"output" => $output,
"output_status" => $output_status
];
}
function cronRouter (FastRoute\RouteCollector $r) {
$r->addRoute(
'GET',
'/execute',
function ($vars) {
global $db, $executed_actions;
$cron_job_allowed = true;
if(!$cron_job_allowed) {
statusCode(403);
exit();
}
job_schedule_availability();
//job_reset_availability();
job_increment_availability();
apiResponse(["excuted_actions" => $executed_actions]);
}
);
}

View File

@ -155,21 +155,31 @@ class Users
public function loginAndReturnToken($username, $password)
{
$this->auth->loginWithUsername($username, $password);
$token = $this->auth->generateJWTtoken();
$token = $this->auth->generateJWTtoken([
"chief" => $this->auth->hasRole(Role::FULL_VIEWER),
"name" => $this->getName(),
]);
return $token;
}
public function isHidden($id)
public function isHidden($id=null)
{
if(is_null($id)) $id = $this->auth->getUserId();
$user = $this->db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$id]);
return $user["hidden"];
}
public function getName($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"];
}
public function hasRole($role, $adminGranted=true)
{
return $this->auth->hasRole($role) || $adminGranted && $role !== Role::DEVELOPER && $this->auth->hasRole(Role::ADMIN) || $role !== Role::DEVELOPER && $this->auth->hasRole(Role::SUPER_ADMIN);
}
}
class Services {
@ -222,5 +232,44 @@ class Services {
}
}
class Schedules {
private $db = null;
private $users = null;
public function __construct($db, $users)
{
$this->db = $db;
$this->users = $users;
}
public function get($profile="default") {
$response = $this->db->selectRow("SELECT * FROM `".DB_PREFIX."_schedules` WHERE `user` = ? AND `profile_name` = ?", [$this->users->auth->getUserId(), $profile]);
if(!is_null($response)) {
$response["schedules"] = json_decode($response["schedules"], true);
return $response;
}
return [];
}
public function update($schedules, $profile="default") {
//TODO implement multiple profiles
//TODO implement holidays
logger("Aggiornata programmazione orari disponibilità");
if(empty($this->get($profile))) {
return $this->db->insert(
DB_PREFIX."_schedules",
["user" => $this->users->auth->getUserId(), "schedules" => $schedules, "profile_name" => $profile]
);
} else {
return $this->db->update(
DB_PREFIX."_schedules",
["schedules" => $schedules, "last_update" => null],
["user" => $this->users->auth->getUserId(), "profile_name" => $profile]
);
}
}
}
$users = new Users($db, $auth);
$services = new Services($db);
$services = new Services($db);
$schedules = new Schedules($db, $users);

File diff suppressed because it is too large Load Diff

View File

@ -20,9 +20,10 @@
"@angular/router": "~13.0.0",
"@angular/service-worker": "~13.0.0",
"@fortawesome/fontawesome-free": "^5.15.4",
"@ng-bootstrap/ng-bootstrap": "^10.0.0",
"@ng-bootstrap/ng-bootstrap": "11.0.0",
"bootstrap": "^5.1.3",
"jwt-decode": "^3.1.2",
"ngx-bootstrap": "^7.1.2",
"rxjs": "~7.4.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"

View File

@ -1,10 +1,10 @@
<div class="text-center">
<p>Sei disponibile in caso di allerta?</p>
<div id="availability-btn-group">
<button (click)="changeAvailibility(1)" id="activate-btn" class="btn btn-lg btn-success me-1">Attiva</button>
<button (click)="changeAvailibility(0)" id="deactivate-btn" class="btn btn-lg btn-danger">Disattiva</button>
<button (click)="changeAvailibility(1)" type="button" [delay]="1000" tooltip="Cambia la tua disponibilità in 'attivo'" id="activate-btn" class="btn btn-lg btn-success me-1">Attiva</button>
<button (click)="changeAvailibility(0)" type="button" [delay]="1000" tooltip="Cambia la tua disponibilità in 'non attivo'" id="deactivate-btn" class="btn btn-lg btn-danger">Disattiva</button>
</div>
<button type="button" class="btn btn-lg">
<button type="button" class="btn btn-lg" (click)="openScheduleModal()">
Modifica orari disponibilità
</button>
</div>

View File

@ -1,5 +1,8 @@
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { TableComponent } from '../table/table.component';
import { ModalAvailabilityScheduleComponent } from '../modal-availability-schedule/modal-availability-schedule.component';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal';
@Component({
selector: 'app-list',
@ -7,11 +10,10 @@ import { ApiClientService } from 'src/app/_services/api-client.service';
styleUrls: ['./list.component.scss']
})
export class ListComponent implements OnInit {
scheduleModalRef?: BsModalRef;
@ViewChild('table') table!: TableComponent;
@ViewChild('table') table!: any;
constructor(private api: ApiClientService) {}
constructor(private api: ApiClientService, private modalService: BsModalService) {}
changeAvailibility(available: 0|1, id?: number|undefined) {
this.api.post("availability", {
@ -22,6 +24,10 @@ export class ListComponent implements OnInit {
});
}
openScheduleModal() {
this.scheduleModalRef = this.modalService.show(ModalAvailabilityScheduleComponent, Object.assign({}, { class: 'modal-custom' }));
}
ngOnInit(): void {
}

View File

@ -0,0 +1,49 @@
<div class="modal-header">
<h4 class="modal-title pull-left">Modifica orari disponibilità</h4>
<button type="button" class="btn-close close pull-right" aria-label="Close" (click)="bsModalRef.hide()">
<span aria-hidden="true" class="visually-hidden">&times;</span>
</button>
</div>
<div class="modal-body" style="overflow-x: auto">
<table cellpadding="0" cellspacing="0" id="scheduler_table">
<thead>
<tr>
<td style="background-color: white;"></td>
<ng-container *ngIf="orientation === 'portrait'">
<ng-container *ngFor="let day of days; let i = index">
<td class="day" (click)="selectDay(i)">{{ day.short }}</td>
</ng-container>
</ng-container>
<ng-container *ngIf="orientation === 'landscape'">
<ng-container *ngFor="let hour of hours">
<td class="hour" (click)="selectHour(hour)">{{ hour }}</td>
</ng-container>
</ng-container>
</tr>
</thead>
<tbody id="scheduler_body" *ngIf="orientation === 'portrait'">
<ng-container *ngFor="let hour of hours">
<tr>
<td class="hour" (click)="selectHour(hour)">{{ hour }}</td>
<ng-container *ngFor="let day of days; let i = index">
<td class="hour-cell" [class.highlighted] = "isCellSelected(i, hour)" (mousedown)="mouseDownCell(i, hour)" (mouseup)="mouseUpCell()" (mouseover)="mouseOverCell(i, hour)"></td>
</ng-container>
</tr>
</ng-container>
</tbody>
<tbody id="scheduler_body" *ngIf="orientation === 'landscape'">
<ng-container *ngFor="let day of days; let i = index">
<tr>
<td class="day" (click)="selectDay(i)">{{ day.short }}</td>
<ng-container *ngFor="let hour of hours">
<td class="hour-cell" [class.highlighted] = "isCellSelected(i, hour)" (mousedown)="mouseDownCell(i, hour)" (mouseup)="mouseUpCell()" (mouseover)="mouseOverCell(i, hour)"></td>
</ng-container>
</tr>
</ng-container>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="saveChanges()">Salva le modifiche</button>
<button type="button" class="btn btn-secondary" (click)="bsModalRef.hide()">Chiudi</button>
</div>

View File

@ -0,0 +1,21 @@
.hour-cell {
width: 100px;
height: 100px;
text-align: center;
vertical-align: middle;
background-color: #ccc;
border: 1px solid #fff;
}
.hour-cell.highlighted {
background-color: #999;
}
#scheduler_body td {
min-width: 40px;
}
.modal-custom {
overflow-x: auto;
max-width: 99%;
margin-bottom: 5em;
}

View File

@ -0,0 +1,197 @@
import { Component, OnInit, ViewEncapsulation, HostListener } from '@angular/core';
import { BsModalRef } from 'ngx-bootstrap/modal';
import { ApiClientService } from 'src/app/_services/api-client.service';
@Component({
selector: 'modal-availability-schedule',
templateUrl: './modal-availability-schedule.component.html',
styleUrls: ['./modal-availability-schedule.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ModalAvailabilityScheduleComponent implements OnInit {
public orientation = "portrait";
public days = [
{
name: 'Lunedì',
short: 'Lun'
},
{
name: 'Martedì',
short: 'Mar'
},
{
name: 'Mercoledì',
short: 'Mer'
},
{
name: 'Giovedì',
short: 'Gio'
},
{
name: 'Venerdì',
short: 'Ven'
},
{
name: 'Sabato',
short: 'Sab'
},
{
name: 'Domenica',
short: 'Dom'
}
];
public hours = [
"0:00", "0:30",
"1:00", "1:30",
"2:00", "2:30",
"3:00", "3:30",
"4:00", "4:30",
"5:00", "5:30",
"6:00", "6:30",
"7:00", "7:30",
"8:00", "8:30",
"9:00", "9:30",
"10:00", "10:30",
"11:00", "11:30",
"12:00", "12:30",
"13:00", "13:30",
"14:00", "14:30",
"15:00", "15:30",
"16:00", "16:30",
"17:00", "17:30",
"18:00", "18:30",
"19:00", "19:30",
"20:00", "20:30",
"21:00", "21:30",
"22:00", "22:30",
"23:00", "23:30",
];
public selectedCells: any = [];
//Used for "select all"
public selectedHours: string[] = [];
public selectedDays: number[] = [];
public isSelecting = false;
constructor(public bsModalRef: BsModalRef, private api: ApiClientService) { }
loadSchedules(schedules: any) {
console.log("Loaded schedules", schedules);
if(typeof schedules === "undefined") {
schedules = [];
}
if(typeof schedules === "string") {
schedules = JSON.parse(schedules);
}
this.selectedCells = schedules;
}
ngOnInit(): void {
this.orientation = window.innerHeight > window.innerWidth ? "portrait" : "landscape";
if(localStorage.getItem('schedules') === null) {
this.api.get("schedules").then((response: any) => {
this.loadSchedules(response.schedules);
});
} else {
this.loadSchedules(JSON.parse((localStorage.getItem('schedules') as string)));
}
}
saveChanges() {
console.log("Selected cells", this.selectedCells);
this.api.post("schedules", {
schedules: this.selectedCells
});
localStorage.removeItem('schedules');
this.bsModalRef.hide();
}
saveChangesInLocal() {
localStorage.setItem('schedules', JSON.stringify(this.selectedCells));
}
@HostListener('window:resize', ['$event'])
onResize(event: Event) {
this.orientation = window.innerHeight > window.innerWidth ? "portrait" : "landscape";
}
isCellSelected(day: number, hour: string) {
return this.selectedCells.find((cell: any) => cell.day === day && cell.hour === hour);
}
toggleCell(day: number, hour: string) {
if(!this.isCellSelected(day, hour)) {
this.selectedCells.push({
day, hour
});
} else {
this.selectedCells = this.selectedCells.filter((cell: any) => cell.day !== day || cell.hour !== hour);
}
this.saveChangesInLocal();
}
selectHour(hour: string) {
console.log("Hour selected", hour);
if(this.selectedHours.includes(hour)) {
this.days.forEach((day: any, i: number) => {
this.selectedCells = this.selectedCells.filter((cell: any) => cell.day !== i || cell.hour !== hour);
});
this.selectedHours = this.selectedHours.filter((h: string) => h !== hour);
} else {
this.days.forEach((day: any, i: number) => {
if(!this.isCellSelected(i, hour)) {
this.selectedCells.push({
day: i, hour
});
}
});
this.selectedHours.push(hour);
}
this.saveChangesInLocal();
}
selectDay(day: number) {
console.log("Day selected", day);
if(this.selectedDays.includes(day)) {
this.hours.forEach((hour: string) => {
this.selectedCells = this.selectedCells.filter((cell: any) => cell.day !== day || cell.hour !== hour);
});
this.selectedDays = this.selectedDays.filter((i: number) => i !== day);
} else {
this.hours.forEach((hour: string) => {
if(!this.isCellSelected(day, hour)) {
this.selectedCells.push({
day, hour
});
}
});
this.selectedDays.push(day);
}
this.saveChangesInLocal();
}
mouseDownCell(day: number, hour: string) {
this.isSelecting = true;
console.log("Mouse down");
console.log("Hour cell selected", day, hour);
this.toggleCell(day, hour);
return false;
}
mouseUpCell() {
this.isSelecting = false;
console.log("Mouse up");
}
mouseOverCell(day: number, hour: string) {
if (this.isSelecting) {
console.log("Mouse over", day, hour);
console.log("Hour cell selected", day, hour);
this.toggleCell(day, hour);
}
}
}

View File

@ -4,27 +4,30 @@
<tr>
<th>Nome</th>
<th>Disponibile</th>
<ng-container *ngIf="auth.profile.chief">
<th>Autista</th>
<th>Chiama</th>
<th>Scrivi</th>
<th>Interventi</th>
<th>Minuti disponibilità</th>
</ng-container>
</tr>
</thead>
<tbody id="table_body">
<tr *ngFor="let row of data">
<td>
<img alt="red helmet" src="./assets/img/red_helmet.png" width="20px" *ngIf="row.chief">
<img alt="red helmet" src="./assets/img/black_helmet.png" width="20px" *ngIf="!row.chief">
<img alt="red helmet" src="./assets/icons/red_helmet.png" width="20px" *ngIf="row.chief">
<img alt="red helmet" src="./assets/icons/black_helmet.png" width="20px" *ngIf="!row.chief">
<ng-container *ngIf="(getTime() - row.online_time) < 30"><u>{{ row.name }}</u></ng-container>
<ng-container *ngIf="(getTime() - row.online_time) > 30">{{ row.name }}</ng-container>
</td>
<td (click)="changeAvailability.emit({user: row.id, newState: row.available ? 0 : 1})">
<td (click)="onChangeAvailability(row.id, row.available ? 0 : 1)">
<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.chief">
<td>
<img alt="driver" src="./assets/img/wheel.png" width="20px" *ngIf="row.driver">
<img alt="driver" src="./assets/icons/wheel.png" width="20px" *ngIf="row.driver">
</td>
<td>
<a href="tel:{{row.phone_number}}"><i class="fa fa-phone"></i></a>
@ -34,6 +37,7 @@
</td>
<td>{{ row.services }}</td>
<td>{{ row.availability_minutes }}</td>
</ng-container>
</tr>
</tbody>
</table>

View File

@ -1,6 +1,7 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { TableType } from 'src/app/_models/TableType';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { AuthService } from '../../_services/auth.service';
@Component({
selector: 'app-table',
@ -10,12 +11,15 @@ import { ApiClientService } from 'src/app/_services/api-client.service';
export class TableComponent implements OnInit {
@Input() sourceType?: string;
@Input() refreshInterval?: number;
@Output() changeAvailability: EventEmitter<{user: number, newState: 0|1}> = new EventEmitter<{user: number, newState: 0|1}>();
public data: any = [];
constructor(public apiClient: ApiClientService) {}
public loadDataInterval: NodeJS.Timer | number = 0;
constructor(public apiClient: ApiClientService, public auth: AuthService) {}
getTime() {
return Math.floor(Date.now() / 1000);
@ -34,6 +38,15 @@ export class TableComponent implements OnInit {
ngOnInit(): void {
console.log(this.sourceType);
this.loadTableData();
this.loadDataInterval = setInterval(() => {
console.log("Refreshing data...");
this.loadTableData();
}, this.refreshInterval || 10000);
}
onChangeAvailability(user: number, newState: 0|1) {
if(this.auth.profile.chief) {
this.changeAvailability.emit({user, newState});
}
}
}

View File

@ -40,9 +40,10 @@ export class ApiClientService {
}
public post(endpoint: string, data: any) {
let params = new HttpParams({
fromObject: data,
});
let params = Object.keys(data).reduce(function (params, key) {
params.set(key, JSON.stringify(data[key]));
return params;
}, new URLSearchParams());
return new Promise<any>((resolve, reject) => {
this.http.post(this.apiEndpoint(endpoint), params.toString(), this.requestOptions).subscribe((data: any) => {
resolve(data);

View File

@ -3,7 +3,7 @@
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/services">Interventi</a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/trainings">Esercitazioni</a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/logs">Logs</a>
<a style="float: right;" id="logout">Ciao, {{ auth.profile.auth_username }}. <b id="logout-text" (click)="auth.logout()">Logout</b></a>
<a style="float: right;" id="logout">Ciao, {{ auth.profile.name }}. <b id="logout-text" (click)="auth.logout()">Logout</b></a>
<a class="icon" id="menuButton" (click)="menuButtonClicked = !menuButtonClicked"></a>
</div>

View File

@ -2,6 +2,8 @@ import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { ModalModule } from 'ngx-bootstrap/modal';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@ -9,6 +11,7 @@ import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { TableComponent } from './_components/table/table.component';
import { ModalAvailabilityScheduleComponent } from './_components/modal-availability-schedule/modal-availability-schedule.component';
import { OwnerImageComponent } from './_components/owner-image/owner-image.component';
import { LoginComponent } from './_components/login/login.component';
@ -23,6 +26,7 @@ import { TrainingsComponent } from './_components/trainings/trainings.component'
AppComponent,
//
TableComponent,
ModalAvailabilityScheduleComponent,
OwnerImageComponent,
//
LoginComponent,
@ -37,6 +41,8 @@ import { TrainingsComponent } from './_components/trainings/trainings.component'
AppRoutingModule,
HttpClientModule,
FormsModule,
ModalModule.forRoot(),
TooltipModule.forRoot(),
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: false && environment.production,
// Register the ServiceWorker as soon as the app is stable

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 792 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 958 B

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -7,53 +7,15 @@
"scope": "./",
"start_url": "./",
"icons": [
{
"src": "assets/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
"type": "image/png"
},
{
"src": "assets/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
"type": "image/png"
}
]
}

View File

@ -3,7 +3,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
"types": ["node"]
},
"files": [
"src/main.ts",

View File

@ -1,46 +0,0 @@
{
"name": "allerta-vvf/allerta-vvf",
"description": "Un software di allertamento per i vvf",
"type": "project",
"repositories": [
{
"type": "vcs",
"no-api": true,
"url": "https://github.com/allerta-vvf/tiny-html-minifier"
},
{
"type": "vcs",
"no-api": true,
"url": "https://github.com/allerta-vvf/php-debugbar"
}
],
"require": {
"twig/twig": "3.3.4",
"delight-im/auth": "8.3.0",
"ulrichsg/getopt-php": "4.0.0",
"nikic/fast-route": "^2.0@dev",
"spatie/array-to-xml": "2.16.0",
"ezyang/htmlpurifier": "4.13.0",
"brick/phonenumber": "0.4.0",
"sentry/sdk": "3.1.1",
"maximebf/debugbar": "dev-master",
"azuyalabs/yasumi": "2.4.0",
"ministryofweb/php-osm-tiles": "2.0.0",
"jenstornell/tiny-html-minifier": "dev-master",
"delight-im/db": "1.3.1",
"webonyx/graphql-php": "14.11.3",
"phpfastcache/phpfastcache": "8.0.8",
"skrtdev/novagram": "1.10"
},
"license": "GPL-3.0-or-later",
"authors": [
{
"name": "Matteo Gheza",
"email": "matteo@matteogheza.it"
}
],
"minimum-stability": "stable",
"require-dev": {
"pheromone/phpcs-security-audit": "2.0.1"
}
}

3573
server/composer.lock generated

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,68 +0,0 @@
{
"browserslist": [
"ie 11"
],
"name": "allertavvf_webpack",
"version": "1.0.0",
"description": "",
"scripts": {
"prod": "webpack --config webpack.prod.js",
"dev": "webpack --config webpack.dev.js",
"dev_watch": "webpack --config webpack.dev.js --watch",
"debug_bundle": "webpack --config webpack.debug_bundle.js"
},
"author": "",
"license": "GPL3",
"dependencies": {
"@sentry/browser": "6.12.0",
"@sentry/tracing": "6.12.0",
"bootstrap": "4.6.0",
"bootstrap-datepicker": "1.9.0",
"bootstrap-toggle": "2.2.2",
"datatables.net-bs4": "1.11.1",
"datatables.net-buttons-bs4": "1.7.1",
"datatables.net-plugins": "1.10.24",
"datatables.net-responsive-bs4": "2.2.9",
"font-awesome": "4.7.0",
"howler": "2.2.3",
"jquery": "3.6.0",
"jquery-pjax": "2.0.1",
"jszip": "3.7.1",
"leaflet": "1.7.1",
"leaflet.locatecontrol": "0.74.0",
"pdfmake": "0.2.2",
"popper.js": "1.16.1",
"promise-polyfill": "8.2.0",
"time-input-polyfill": "1.0.10",
"toastr": "2.1.4",
"whatwg-fetch": "3.6.2"
},
"devDependencies": {
"@babel/core": "7.15.5",
"@babel/plugin-transform-runtime": "7.15.0",
"@babel/preset-env": "7.15.4",
"@fiverr/afterbuild-webpack-plugin": "1.0.0",
"@fortawesome/fontawesome-free": "5.15.4",
"@sentry/webpack-plugin": "1.17.1",
"babel-loader": "8.2.2",
"clean-webpack-plugin": "4.0.0",
"colors": "1.4.0",
"copy-webpack-plugin": "9.0.1",
"core-js": "3.17.2",
"css-loader": "6.2.0",
"expose-loader": "3.0.0",
"file-loader": "6.2.0",
"glob": "7.1.7",
"sass": "1.39.0",
"sass-loader": "12.1.0",
"speed-measure-webpack-plugin": "1.5.0",
"style-loader": "3.2.1",
"terser-webpack-plugin": "5.2.3",
"webpack": "5.52.0",
"webpack-assets-manifest": "5.0.6",
"webpack-bundle-analyzer": "4.4.2",
"webpack-cli": "4.8.0",
"webpack-inject-plugin": "1.5.5",
"webpack-merge": "5.8.0"
}
}