Merge pull request #424 from allerta-vvf/master

Add manual mode
This commit is contained in:
Matteo Gheza 2022-01-06 22:18:34 +01:00 committed by GitHub
commit 299daf9ccb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 256 additions and 43 deletions

View File

@ -207,11 +207,13 @@ function apiRouter (FastRoute\RouteCollector $r) {
global $users, $db;
requireLogin() || accessDenied();
$users->online_time_update();
$row = $db->selectRow(
"SELECT `available`, `manual_mode` FROM `".DB_PREFIX."_profiles` WHERE `id` = ?",
[$users->auth->getUserId()]
);
apiResponse([
"available" => $db->selectValue(
"SELECT `available` FROM `".DB_PREFIX."_profiles` WHERE `id` = ?",
[$users->auth->getUserId()]
)
"available" => $row["available"],
"manual_mode" => $row["manual_mode"]
]);
}
);
@ -227,12 +229,31 @@ function apiRouter (FastRoute\RouteCollector $r) {
}
$user_id = is_numeric($_POST["id"]) ? $_POST["id"] : $users->auth->getUserId();
apiResponse([
"response" => $availability->change($_POST["available"], $user_id),
"response" => $availability->change($_POST["available"], $user_id, true),
"updated_user" => $user_id,
"updated_user_name" => $users->getName($user_id)
]);
}
);
$r->addRoute(
"POST",
"/manual_mode",
function ($vars) {
global $users, $db;
requireLogin() || accessDenied();
$users->online_time_update();
$db->update(
DB_PREFIX."_profiles",
[
"manual_mode" => $_POST["manual_mode"]
],
[
"id" => $users->auth->getUserId()
]
);
apiResponse(["status" => "success"]);
}
);
$r->addRoute(
['GET'],
@ -258,6 +279,29 @@ function apiRouter (FastRoute\RouteCollector $r) {
}
);
$r->addRoute(
['GET'],
'/service_types',
function ($vars) {
global $users, $db;
requireLogin() || accessDenied();
$users->online_time_update();
$response = $db->select("SELECT * FROM `".DB_PREFIX."_type`");
apiResponse(is_null($response) ? [] : $response);
}
);
$r->addRoute(
['POST'],
'/service_types',
function ($vars) {
global $users, $db;
requireLogin() || accessDenied();
$users->online_time_update();
$response = $db->insert(DB_PREFIX."_type", ["name" => $_POST["name"]]);
apiResponse($response);
}
);
$r->addRoute(
['POST'],
'/telegram_login_token',

View File

@ -108,17 +108,14 @@ function job_schedule_availability() {
"minutes" => (int) date("i")
];
$manual_mode = $db->select("SELECT manual_mode FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$user_id]);
if(
!$manual_mode &&
$schedule["day"] == $now["day"] &&
$schedule["hour"] == $now["hour"] &&
$schedule["minutes"] <= $now["minutes"] &&
$now["minutes"] - $schedule["minutes"] <= 30
){
$availability_last_change = $db->select("SELECT availability_last_change FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$user_id]);
if($availability_last_change === "manual" && $last_exec["day"] === $now["day"]){
break;
}
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"]);
@ -127,7 +124,7 @@ function job_schedule_availability() {
["last_exec" => $last_exec_new],
["id" => $id]
);
$availability->change(1, $user_id, "cron");
$availability->change(1, $user_id, false);
$schedules_check["schedules"][] = [
"schedule" => $schedule,
"now" => $now,
@ -143,7 +140,7 @@ function job_schedule_availability() {
$profiles = $db->select("SELECT id FROM `".DB_PREFIX."_profiles`");
foreach ($profiles as $profile) {
if(!in_array($profile["id"],$schedules_users)){
$availability->change(0, $profile["id"], "cron");
$availability->change(0, $profile["id"], false);
}
}
$output = $schedules_check;

View File

@ -162,7 +162,7 @@ function telegramBotRouter() {
requireBotLogin($message);
if(count(explode(" ", $message->text)) > 3) return;
$user_id = getUserIdByMessage($message);
$availability->change(1, $user_id);
$availability->change(1, $user_id, true);
$Bot->sendMessage($message->from->id, "Disponibilità aggiorata con successo.\nOra sei <b>operativo</b>.");
});
@ -171,7 +171,7 @@ function telegramBotRouter() {
requireBotLogin($message);
if(count(explode(" ", $message->text)) > 4) return;
$user_id = getUserIdByMessage($message);
$availability->change(0, $user_id);
$availability->change(0, $user_id, true);
$Bot->sendMessage($message->from->id, "Disponibilità aggiorata con successo.\nOra sei <b>non operativo</b>.");
});

View File

@ -261,13 +261,16 @@ class Availability {
$this->users = $users;
}
public function change($availability, $user_id, $change_type="manual")
public function change($availability, $user_id, $is_manual_mode=true)
{
if($change_type === "manual") logger("Disponibilità cambiata in ".($availability ? '"disponibile"' : '"non disponibile"'), $user_id, $this->users->auth->getUserId());
if($is_manual_mode) logger("Disponibilità cambiata in ".($availability ? '"disponibile"' : '"non disponibile"'), $user_id, $this->users->auth->getUserId());
$change_values = ["available" => $availability];
if($is_manual_mode) $change_values["manual_mode"] = 1;
$response = $this->db->update(
DB_PREFIX."_profiles",
["available" => $availability, 'availability_last_change' => $change_type],
$change_values,
["id" => $user_id]
);

View File

@ -17,10 +17,13 @@
"@angular/platform-browser-dynamic": "~13.0.0",
"@angular/router": "~13.0.0",
"@angular/service-worker": "~13.0.0",
"@asymmetrik/ngx-leaflet": "^8.1.0",
"@fortawesome/fontawesome-free": "^5.15.4",
"@ng-bootstrap/ng-bootstrap": "11.0.0",
"bootstrap": "^5.1.3",
"jwt-decode": "^3.1.2",
"leaflet": "^1.7.1",
"leaflet.locatecontrol": "^0.76.0",
"ngx-bootstrap": "^7.1.2",
"ngx-toastr": "^14.2.1",
"rxjs": "~7.4.0",
@ -32,6 +35,7 @@
"@angular/cli": "~13.0.3",
"@angular/compiler-cli": "~13.0.0",
"@types/jasmine": "~3.10.0",
"@types/leaflet": "^1.7.8",
"@types/node": "16.11.17",
"jasmine-core": "4.0.0",
"karma": "~6.3.0",
@ -622,6 +626,17 @@
"integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==",
"dev": true
},
"node_modules/@asymmetrik/ngx-leaflet": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@asymmetrik/ngx-leaflet/-/ngx-leaflet-8.1.0.tgz",
"integrity": "sha512-lq7LduBP/vXcaSEmKnx7mzCR8WsoYqh9pB6BNnq53yeCwsqRbG3GdKye1/i8VvoRzjDsmQBPQsIFZ9uclXrtgg==",
"peerDependencies": {
"@angular/common": ">=10",
"@angular/core": ">=10",
"leaflet": "1",
"tslib": "2"
}
},
"node_modules/@babel/code-frame": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz",
@ -2665,6 +2680,12 @@
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
"dev": true
},
"node_modules/@types/geojson": {
"version": "7946.0.8",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
"integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==",
"dev": true
},
"node_modules/@types/http-proxy": {
"version": "1.17.8",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz",
@ -2686,6 +2707,15 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
"node_modules/@types/leaflet": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.7.8.tgz",
"integrity": "sha512-prZwhmUznkwTYCTZVGTR4U1GzgPP3PAWYYQ3wDgVkIoiuQTheWoycsXx4Rz9ARYhlDTl0Ycd8lvhH2/rNSkqIg==",
"dev": true,
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/node": {
"version": "16.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.17.tgz",
@ -7119,6 +7149,16 @@
"node": ">= 8"
}
},
"node_modules/leaflet": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.7.1.tgz",
"integrity": "sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw=="
},
"node_modules/leaflet.locatecontrol": {
"version": "0.76.0",
"resolved": "https://registry.npmjs.org/leaflet.locatecontrol/-/leaflet.locatecontrol-0.76.0.tgz",
"integrity": "sha512-Mx8uiihBi8KrrW3LgblsNL/pS8HR0gj60m8VFDFrnhSvDuitChazc095XcMSscf/XqZW+TSqQMCTe+AUy/4/eA=="
},
"node_modules/less": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/less/-/less-4.1.2.tgz",
@ -12979,6 +13019,12 @@
"integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==",
"dev": true
},
"@asymmetrik/ngx-leaflet": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@asymmetrik/ngx-leaflet/-/ngx-leaflet-8.1.0.tgz",
"integrity": "sha512-lq7LduBP/vXcaSEmKnx7mzCR8WsoYqh9pB6BNnq53yeCwsqRbG3GdKye1/i8VvoRzjDsmQBPQsIFZ9uclXrtgg==",
"requires": {}
},
"@babel/code-frame": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz",
@ -14453,6 +14499,12 @@
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
"dev": true
},
"@types/geojson": {
"version": "7946.0.8",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
"integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==",
"dev": true
},
"@types/http-proxy": {
"version": "1.17.8",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz",
@ -14474,6 +14526,15 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
"@types/leaflet": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.7.8.tgz",
"integrity": "sha512-prZwhmUznkwTYCTZVGTR4U1GzgPP3PAWYYQ3wDgVkIoiuQTheWoycsXx4Rz9ARYhlDTl0Ycd8lvhH2/rNSkqIg==",
"dev": true,
"requires": {
"@types/geojson": "*"
}
},
"@types/node": {
"version": "16.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.17.tgz",
@ -17838,6 +17899,16 @@
"integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==",
"dev": true
},
"leaflet": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.7.1.tgz",
"integrity": "sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw=="
},
"leaflet.locatecontrol": {
"version": "0.76.0",
"resolved": "https://registry.npmjs.org/leaflet.locatecontrol/-/leaflet.locatecontrol-0.76.0.tgz",
"integrity": "sha512-Mx8uiihBi8KrrW3LgblsNL/pS8HR0gj60m8VFDFrnhSvDuitChazc095XcMSscf/XqZW+TSqQMCTe+AUy/4/eA=="
},
"less": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/less/-/less-4.1.2.tgz",

View File

@ -21,10 +21,13 @@
"@angular/platform-browser-dynamic": "~13.0.0",
"@angular/router": "~13.0.0",
"@angular/service-worker": "~13.0.0",
"@asymmetrik/ngx-leaflet": "^8.1.0",
"@fortawesome/fontawesome-free": "^5.15.4",
"@ng-bootstrap/ng-bootstrap": "11.0.0",
"bootstrap": "^5.1.3",
"jwt-decode": "^3.1.2",
"leaflet": "^1.7.1",
"leaflet.locatecontrol": "^0.76.0",
"ngx-bootstrap": "^7.1.2",
"ngx-toastr": "^14.2.1",
"rxjs": "~7.4.0",
@ -36,6 +39,7 @@
"@angular/cli": "~13.0.3",
"@angular/compiler-cli": "~13.0.0",
"@types/jasmine": "~3.10.0",
"@types/leaflet": "^1.7.8",
"@types/node": "16.11.17",
"jasmine-core": "4.0.0",
"karma": "~6.3.0",

View File

@ -53,17 +53,11 @@
</ng-container>
</div>
<label>Luogo dell'intervento</label>
<div id="map"></div>
<div id="search">
<div class="form-inline">
<div class="form-group mx-sm-3 mb-2">
<input type="text" class="form-control" value="" id="addr" size="50" />
</div>
<div class="form-group mx-sm-3 mb-2">
<button id="search_button" type="button" class="btn btn-primary mb-2">
Cerca
</button>
</div>
<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>
@ -75,17 +69,19 @@
<div class="form-group">
<label>Tipologia</label>
<br>
<!--
<select id="types" class="form-control" class="types">
{% for type in service.types %}
<option value='{{ type.name }}'>{{ type.name }}</option>
{% endfor %}
{% if service.types is empty %}
<option id="empty_option" value=''></option>
{% endif %}
<option value='add_new'>{{ 'Add type...'|t }}</option>
</select>
-->
<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>

View File

@ -1,6 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
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',
@ -10,8 +13,22 @@ import { ApiClientService } from 'src/app/_services/api-client.service';
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)
};
constructor(private route: ActivatedRoute, private api: ApiClientService) {
constructor(
private route: ActivatedRoute,
private api: ApiClientService,
private toastr: ToastrService
) {
this.route.paramMap.subscribe(params => {
this.serviceId = params.get('id') || undefined;
console.log(this.serviceId);
@ -20,8 +37,41 @@ export class EditServiceComponent implements OnInit {
this.users = users;
console.log(this.users);
});
this.loadTypes();
}
loadTypes() {
this.api.get("service_types").then((types) => {
console.log(types);
this.types = types;
});
}
ngOnInit(): void { }
addType() {
if(this.newType.length < 2) {
this.toastr.error("Il nome della tipologia deve essere lungo almeno 2 caratteri");
return;
}
if(this.types.find(t => t.name == this.newType)) {
this.toastr.error("Il nome della tipologia è già in uso");
return;
}
this.api.post("service_types", {
name: this.newType
}).then((type) => {
this.addingType = false;
this.newType = "";
console.log(type);
if(type === 1) this.toastr.success("Tipologia di servizio aggiunta con successo.");
this.loadTypes();
});
}
mapReady(map: any) {
console.log(map);
(window as any).L.control.locate().addTo(map);
}
}

View File

@ -1,9 +1,18 @@
<div class="text-center">
<p>Sei disponibile in caso di allerta?</p>
<h3 *ngIf="available !== undefined">Attualmente sei: <b>{{ available ? "Disponibile" : "Non disponibile" }}{{ manual_mode ? "" : " (programmato)" }}</b></h3>
<div id="availability-btn-group">
<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>
<ng-container *ngIf="manual_mode !== undefined">
<button type="button" class="btn btn-secondary" *ngIf="manual_mode" (click)="updateManualMode(0)">
Attiva programmazione oraria
</button>
<button type="button" class="btn btn-secondary" *ngIf="!manual_mode" (click)="updateManualMode(1)">
Disattiva programmazione oraria
</button>
<br>
</ng-container>
<button type="button" class="btn btn-lg" (click)="openScheduleModal()">
Modifica orari disponibilità
</button>

View File

@ -1,4 +1,4 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Component, OnInit, OnDestroy, 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';
@ -15,12 +15,27 @@ export class ListComponent implements OnInit {
scheduleModalRef?: BsModalRef;
@ViewChild('table') table!: TableComponent;
public loadAvailabilityInterval: NodeJS.Timer | undefined = undefined;
public available: boolean | undefined = undefined;
public manual_mode: boolean | undefined = undefined;
constructor(
private api: ApiClientService,
private auth: AuthService,
private toastr: ToastrService,
private modalService: BsModalService
) {}
) {
this.loadAvailability();
}
loadAvailability() {
this.api.get("availability").then((response) => {
this.available = response.available;
this.manual_mode = response.manual_mode;
console.log(this.available, this.manual_mode);
});
}
changeAvailibility(available: 0|1, id?: number|undefined) {
this.api.post("availability", {
@ -30,15 +45,35 @@ export class ListComponent implements OnInit {
let changed_user_msg = parseInt(response.updated_user) === parseInt(this.auth.profile.auth_user_id) ? "La tua disponibilità" : `La disponibilità di ${response.updated_user_name}`;
let msg = available === 1 ? `${changed_user_msg} è stata impostata con successo.` : `${changed_user_msg} è stata rimossa con successo.`;
this.toastr.success(msg);
this.loadAvailability();
this.table.loadTableData();
});
}
updateManualMode(manual_mode: 0|1) {
this.api.post("manual_mode", {
manual_mode: manual_mode
}).then((response) => {
this.toastr.success("Modalità manuale aggiornata con successo.");
this.loadAvailability();
});
}
openScheduleModal() {
this.scheduleModalRef = this.modalService.show(ModalAvailabilityScheduleComponent, Object.assign({}, { class: 'modal-custom' }));
}
ngOnInit(): void {
this.loadAvailabilityInterval = setInterval(() => {
console.log("Refreshing availability...");
this.loadAvailability();
}, 10000);
}
ngOnDestroy(): void {
if(typeof this.loadAvailabilityInterval !== 'undefined') {
clearInterval(this.loadAvailabilityInterval);
}
}
requestTelegramToken() {

View File

@ -7,6 +7,7 @@ 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';
@ -58,6 +59,7 @@ 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

View File

@ -4,6 +4,8 @@
@import "~@fortawesome/fontawesome-free/css/all.css";
@import '~ngx-toastr/toastr';
@import '~ngx-bootstrap/datepicker/bs-datepicker.scss';
@import '~leaflet/dist/leaflet.css';
@import '~leaflet.locatecontrol/dist/L.Control.Locate.min.css';
.fa {
vertical-align: middle;