WIP support for stats page
This commit is contained in:
parent
c5a144d2ff
commit
6d4db17e8f
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Place;
|
||||
use App\Models\Service;
|
||||
use App\Models\User;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class StatsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get all services with all data
|
||||
*/
|
||||
public function services(Request $request)
|
||||
{
|
||||
$query = Service::select('id','code','chief_id','type_id','place_id','notes','start','end','added_by_id','created_at')
|
||||
->with('place')
|
||||
->with('drivers:id')
|
||||
->with('crew:id')
|
||||
->orderBy('start', 'desc');
|
||||
if($request->has('from')) {
|
||||
try {
|
||||
$from = Carbon::parse($request->input('from'));
|
||||
$query->whereDate('start', '>=', $from->toDateString());
|
||||
} catch (\Carbon\Exceptions\InvalidFormatException $e) { }
|
||||
}
|
||||
if($request->has('to')) {
|
||||
try {
|
||||
$to = Carbon::parse($request->input('to'));
|
||||
$query->whereDate('start', '<=', $to->toDateString());
|
||||
} catch (\Carbon\Exceptions\InvalidFormatException $e) { }
|
||||
}
|
||||
return response()->json(
|
||||
$query->get()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ use App\Http\Controllers\AlertController;
|
|||
use App\Http\Controllers\LogsController;
|
||||
use App\Http\Controllers\TelegramController;
|
||||
use App\Http\Controllers\ServiceController;
|
||||
use App\Http\Controllers\StatsController;
|
||||
use App\Http\Controllers\PlacesController;
|
||||
use App\Http\Controllers\ServiceTypeController;
|
||||
use App\Http\Controllers\TrainingController;
|
||||
|
@ -29,6 +30,8 @@ use \Matthewbdaly\ETagMiddleware\ETag;
|
|||
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
|
||||
Route::get('/stats/services', [StatsController::class, 'services'])->middleware(ETag::class);
|
||||
|
||||
Route::middleware('auth:sanctum')->group( function () {
|
||||
//Route::post('/register', [AuthController::class, 'register']); //TODO: replace with admin only route
|
||||
|
||||
|
@ -72,6 +75,8 @@ Route::middleware('auth:sanctum')->group( function () {
|
|||
|
||||
Route::get('/logs', [LogsController::class, 'index'])->middleware(ETag::class);
|
||||
|
||||
//Route::get('/stats/services', [StatsController::class, 'services'])->middleware(ETag::class);
|
||||
|
||||
Route::post('/telegram_login_token', [TelegramController::class, 'loginToken']);
|
||||
|
||||
Route::post('/logout', [AuthController::class, 'logout']);
|
||||
|
|
|
@ -23,10 +23,14 @@
|
|||
"@ngx-translate/http-loader": "^7.0.0",
|
||||
"@sentry/angular-ivy": "^7.66.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"chart.js": "^3.3.2",
|
||||
"chartjs-plugin-colorschemes-v3": "^0.5.4",
|
||||
"leaflet": "^1.7.1",
|
||||
"leaflet.locatecontrol": "^0.76.0",
|
||||
"ngx-bootstrap": "^10.1.0",
|
||||
"ngx-toastr": "^18.0.0",
|
||||
"primeicons": "^6.0.1",
|
||||
"primeng": "17.2.0",
|
||||
"rxjs": "~7.4.0",
|
||||
"sweetalert2": "^11.3.4",
|
||||
"tslib": "^2.3.0",
|
||||
|
@ -36,6 +40,7 @@
|
|||
"@angular-devkit/build-angular": "^17.0.8",
|
||||
"@angular/cli": "^17.0.8",
|
||||
"@angular/compiler-cli": "^17.0.8",
|
||||
"@types/chartjs-plugin-colorschemes": "^0.4.5",
|
||||
"@types/jasmine": "~3.10.0",
|
||||
"@types/leaflet": "^1.7.8",
|
||||
"@types/node": "^20.10.5",
|
||||
|
@ -3849,6 +3854,24 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chart.js": {
|
||||
"version": "2.9.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.41.tgz",
|
||||
"integrity": "sha512-3dvkDvueckY83UyUXtJMalYoH6faOLkWQoaTlJgB4Djde3oORmNP0Jw85HtzTuXyliUHcdp704s0mZFQKio/KQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chartjs-plugin-colorschemes": {
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/chartjs-plugin-colorschemes/-/chartjs-plugin-colorschemes-0.4.5.tgz",
|
||||
"integrity": "sha512-KV41H9rYvGOWuIHAmam4t7tJCT6SBZEAbXrjd4nL65EBmMT8Ze85UCHU6sYO9FHGFqAIg3iGoRlJ0HONXNuOGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/chart.js": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
|
@ -5314,6 +5337,19 @@
|
|||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz",
|
||||
"integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w=="
|
||||
},
|
||||
"node_modules/chartjs-plugin-colorschemes-v3": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-plugin-colorschemes-v3/-/chartjs-plugin-colorschemes-v3-0.5.4.tgz",
|
||||
"integrity": "sha512-2YsZT7rkApsJBh1V2JrvtxWRCo6ouuIJqgok97ihYNjVEf5iVFfXYiD6sumaRWQ3elxF8yIywbvOjmpobp0dYg==",
|
||||
"peerDependencies": {
|
||||
"chart.js": ">= 3 < 4"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
|
@ -9349,6 +9385,15 @@
|
|||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",
|
||||
|
@ -10528,6 +10573,26 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/primeicons": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-6.0.1.tgz",
|
||||
"integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA=="
|
||||
},
|
||||
"node_modules/primeng": {
|
||||
"version": "17.2.0",
|
||||
"resolved": "https://registry.npmjs.org/primeng/-/primeng-17.2.0.tgz",
|
||||
"integrity": "sha512-pajexYLeJWE6+pmDy0gHrMiQ/zLq4il54EcRNWC0jTNgJvWhoz/K/7NL9kXf2H4eWexNcrEieLnO9ko5uj3cVg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^17.0.0",
|
||||
"@angular/core": "^17.0.0",
|
||||
"@angular/forms": "^17.0.0",
|
||||
"rxjs": "^6.0.0 || ^7.8.1",
|
||||
"zone.js": "~0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proc-log": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz",
|
||||
|
|
|
@ -28,10 +28,14 @@
|
|||
"@ngx-translate/http-loader": "^7.0.0",
|
||||
"@sentry/angular-ivy": "^7.66.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"chart.js": "^3.3.2",
|
||||
"chartjs-plugin-colorschemes-v3": "^0.5.4",
|
||||
"leaflet": "^1.7.1",
|
||||
"leaflet.locatecontrol": "^0.76.0",
|
||||
"ngx-bootstrap": "^10.1.0",
|
||||
"ngx-toastr": "^18.0.0",
|
||||
"primeicons": "^6.0.1",
|
||||
"primeng": "17.2.0",
|
||||
"rxjs": "~7.4.0",
|
||||
"sweetalert2": "^11.3.4",
|
||||
"tslib": "^2.3.0",
|
||||
|
@ -41,6 +45,7 @@
|
|||
"@angular-devkit/build-angular": "^17.0.8",
|
||||
"@angular/cli": "^17.0.8",
|
||||
"@angular/compiler-cli": "^17.0.8",
|
||||
"@types/chartjs-plugin-colorschemes": "^0.4.5",
|
||||
"@types/jasmine": "~3.10.0",
|
||||
"@types/leaflet": "^1.7.8",
|
||||
"@types/node": "^20.10.5",
|
||||
|
@ -52,4 +57,4 @@
|
|||
"karma-jasmine-html-reporter": "~1.7.0",
|
||||
"typescript": "~5.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
declare module 'chartjs-plugin-colorschemes-v3/src/colorschemes/colorschemes.office';
|
||||
declare module 'chartjs-plugin-colorschemes-v3/src/colorschemes/colorschemes.office'{
|
||||
export function Aspect6(): Function
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<p-chart [type]="type" [data]="data" [options]="options"></p-chart>
|
|
@ -0,0 +1,5 @@
|
|||
@media (max-width: 600px) {
|
||||
.chartjs-render-monitor {
|
||||
height: 700px !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import 'chartjs-plugin-colorschemes-v3/src/plugins/plugin.colorschemes';
|
||||
import { Aspect6 } from 'chartjs-plugin-colorschemes-v3/src/colorschemes/colorschemes.office';
|
||||
|
||||
@Component({
|
||||
selector: 'chart',
|
||||
templateUrl: './chart.component.html',
|
||||
styleUrls: ['./chart.component.scss']
|
||||
})
|
||||
export class ChartComponent implements OnInit {
|
||||
@Input() type = "";
|
||||
@Input() data: any = {};
|
||||
|
||||
options: any = {};
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const documentStyle = getComputedStyle(document.documentElement);
|
||||
const textColor = documentStyle.getPropertyValue('--text-color');
|
||||
|
||||
this.options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
},
|
||||
scales: { },
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
color: textColor
|
||||
}
|
||||
},
|
||||
colorschemes: {
|
||||
scheme: Aspect6
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (this.type === "bar") {
|
||||
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
|
||||
const surfaceBorder = documentStyle.getPropertyValue('--surface-border');
|
||||
this.options.scales = {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: textColorSecondary
|
||||
},
|
||||
grid: {
|
||||
color: surfaceBorder,
|
||||
drawBorder: false
|
||||
}
|
||||
},
|
||||
x: {
|
||||
ticks: {
|
||||
color: textColorSecondary
|
||||
},
|
||||
grid: {
|
||||
color: surfaceBorder,
|
||||
drawBorder: false
|
||||
}
|
||||
}
|
||||
};
|
||||
} else if (this.type === "horizontal-bar") {
|
||||
this.options.indexAxis = 'y';
|
||||
this.type = "bar";
|
||||
} else if (this.type === "doughnut") {
|
||||
this.options.cutout = '60%';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
|
||||
import { TranslationModule } from '../../translation.module';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { ChartModule as OrigChartModule } from 'primeng/chart';
|
||||
|
||||
import { ChartComponent } from './chart.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ChartComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
BsDatepickerModule.forRoot(),
|
||||
TranslationModule,
|
||||
ButtonModule,
|
||||
OrigChartModule
|
||||
],
|
||||
exports: [
|
||||
ChartComponent
|
||||
]
|
||||
})
|
||||
export class ChartModule { }
|
|
@ -0,0 +1,6 @@
|
|||
<div class="input-group flex-nowrap">
|
||||
<span class="input-group-text" id="addon-wrapping">Filtra interventi per data</span>
|
||||
<input type="text" [disabled]="disabled" placeholder="Seleziona un intervallo di date" class="form-control" placement="bottom"
|
||||
bsDaterangepicker [bsConfig]="dateRangePickerOptions" [maxDate]="maxDate" [(bsValue)]="range" (bsValueChange)="updateValue($event)">
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="resetRange()">Rimuovi filtri</button>
|
||||
</div>
|
|
@ -0,0 +1,3 @@
|
|||
.date-picker {
|
||||
width: 85%
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
import { Component, OnInit, forwardRef } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
|
||||
import { defineLocale } from 'ngx-bootstrap/chronos';
|
||||
import { BsLocaleService } from 'ngx-bootstrap/datepicker';
|
||||
import { itLocale } from 'ngx-bootstrap/locale';
|
||||
defineLocale('it', itLocale);
|
||||
|
||||
@Component({
|
||||
selector: 'daterange-picker',
|
||||
templateUrl: './daterange-picker.component.html',
|
||||
styleUrls: ['./daterange-picker.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
multi: true,
|
||||
useExisting: forwardRef(() => DaterangePickerComponent)
|
||||
}
|
||||
]
|
||||
})
|
||||
export class DaterangePickerComponent implements OnInit, ControlValueAccessor {
|
||||
disabled = false;
|
||||
|
||||
maxDate: Date = new Date();
|
||||
|
||||
dateRangePickerOptions = {
|
||||
ranges: [
|
||||
{
|
||||
value: [new Date(new Date().setDate(new Date().getDate() - 31)), new Date()],
|
||||
label: 'Ultimi 31 giorni'
|
||||
}, {
|
||||
value: [new Date(new Date().setDate(new Date().getDate() - 7)), new Date()],
|
||||
label: 'Ultimi 7 giorni'
|
||||
}, {
|
||||
value: [new Date(new Date().getFullYear(), 0, 1), new Date()],
|
||||
label: 'Anno corrente'
|
||||
}, {
|
||||
value: [new Date(new Date().getFullYear() - 1, 0, 1), new Date(new Date().getFullYear() - 1, 11, 31)],
|
||||
label: 'Anno precedente'
|
||||
}, {
|
||||
value: [new Date(new Date().setMonth(new Date().getMonth() - 6)), new Date()],
|
||||
label: 'Ultimi 6 mesi'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
range: (Date | undefined)[] | undefined = undefined;
|
||||
|
||||
constructor(private localeService: BsLocaleService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.localeService.use(window.navigator.language.split("-")[0]);
|
||||
}
|
||||
|
||||
get value(): (Date | undefined)[] | undefined {
|
||||
if(this.range === null) return undefined;
|
||||
return this.range;
|
||||
}
|
||||
|
||||
set value(range: (Date | undefined)[] | undefined) {
|
||||
console.log("new value", range, "old value", this.range);
|
||||
this.range = range;
|
||||
this.onChange(this.range);
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
updateValue($event: (Date | undefined)[] | undefined) {
|
||||
this.range = $event;
|
||||
console.log("updateValue", this.range);
|
||||
this.markAsTouched();
|
||||
this.onChange($event);
|
||||
}
|
||||
|
||||
resetRange() {
|
||||
this.updateValue(undefined);
|
||||
}
|
||||
|
||||
writeValue(range: (Date | undefined)[] | undefined): void {
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
onChange = (value: (Date | undefined)[] | undefined) => {};
|
||||
|
||||
onTouched = () => {};
|
||||
|
||||
registerOnChange(fn: (value: (Date | undefined)[] | undefined) => void): void {
|
||||
this.onChange = fn;
|
||||
console.log(fn);
|
||||
}
|
||||
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this.onTouched = fn;
|
||||
fn();
|
||||
}
|
||||
|
||||
markAsTouched() {
|
||||
this.onTouched();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
|
||||
import { TranslationModule } from '../../translation.module';
|
||||
|
||||
import { DaterangePickerComponent } from './daterange-picker.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DaterangePickerComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
BsDatepickerModule.forRoot(),
|
||||
TranslationModule
|
||||
],
|
||||
exports: [
|
||||
DaterangePickerComponent
|
||||
]
|
||||
})
|
||||
export class DaterangePickerModule { }
|
|
@ -17,7 +17,7 @@ export class MapPickerComponent implements OnInit {
|
|||
@Input() selectLat = "";
|
||||
@Input() selectLng = "";
|
||||
|
||||
@Output() onMarkerSet = new EventEmitter<any>();
|
||||
@Output() markerSet = new EventEmitter<any>();
|
||||
|
||||
options = {
|
||||
layers: [
|
||||
|
@ -48,7 +48,7 @@ export class MapPickerComponent implements OnInit {
|
|||
}
|
||||
|
||||
setMarker(latLng: LatLng) {
|
||||
this.onMarkerSet.emit({
|
||||
this.markerSet.emit({
|
||||
lat: latLng.lat,
|
||||
lng: latLng.lng
|
||||
});
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<div id="map" style="height: 300px;" leaflet [leafletOptions]="mapOptions" [leafletLayersControl]="mapLayersControl" [leafletLayers]="mapLayers" [leafletFitBounds]="mapFitBounds" (leafletClick)="_mapClick($event)">
|
||||
</div>
|
|
@ -0,0 +1,73 @@
|
|||
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
|
||||
import { LatLngTuple, LatLngBounds, latLng, tileLayer, Marker, Layer } from 'leaflet';
|
||||
|
||||
export declare class LeafletControlLayersConfig {
|
||||
baseLayers: {
|
||||
[name: string]: Layer;
|
||||
};
|
||||
overlays: {
|
||||
[name: string]: Layer;
|
||||
};
|
||||
}
|
||||
interface IRange {
|
||||
value: Date[];
|
||||
label: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'map',
|
||||
templateUrl: './map.component.html',
|
||||
styleUrls: ['./map.component.scss']
|
||||
})
|
||||
export class MapComponent implements OnInit {
|
||||
@Output() mapClick = new EventEmitter<any>();
|
||||
|
||||
defaultLat = 45.88283872530;
|
||||
defaultLng = 10.18226623535;
|
||||
|
||||
mapOptions = {
|
||||
zoom: 10,
|
||||
center: latLng(this.defaultLat, this.defaultLng)
|
||||
}
|
||||
mapLayersControl: LeafletControlLayersConfig = {
|
||||
baseLayers: {
|
||||
'Open Street Map': tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }),
|
||||
'ESRI WorldImagery': tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' }),
|
||||
'ESRI WorldTopoMap': tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles © Esri — Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community' }),
|
||||
'Open Topo Map': tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: © <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)' }),
|
||||
'Open Street Map Hot': tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', { maxZoom: 20, attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Tiles style by <a href="https://www.hotosm.org/" target="_blank">Humanitarian OpenStreetMap Team</a> hosted by <a href="https://openstreetmap.fr/" target="_blank">OpenStreetMap France</a>' }),
|
||||
'Stadia Dark': tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png', { maxZoom: 20, attribution: '© <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> © <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }),
|
||||
},
|
||||
overlays: {
|
||||
'Open Railway Map': tileLayer('https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', { maxZoom: 19, attribution: 'Map data: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors | Map style: © <a href="https://www.OpenRailwayMap.org">OpenRailwayMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)' }),
|
||||
'Open Fire Map': tileLayer('http://openfiremap.org/hytiles/{z}/{x}/{y}.png', { maxZoom: 19, attribution: 'Map data: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors | Map style: © <a href="http://www.openfiremap.org">OpenFireMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)' }),
|
||||
'SafeCast': tileLayer('https://s3.amazonaws.com/te512.safecast.org/{z}/{x}/{y}.png', { maxZoom: 16, attribution: 'Map data: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors | Map style: © <a href="https://blog.safecast.org/about/">SafeCast</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)' })
|
||||
}
|
||||
};
|
||||
mapLayers: Layer[] = [this.mapLayersControl.baseLayers['Open Street Map']];
|
||||
mapFitBounds: LatLngBounds = latLng(this.defaultLat, this.defaultLng).toBounds(3000);
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void { }
|
||||
|
||||
_mapClick(event: any) {
|
||||
this.mapClick.emit(event);
|
||||
}
|
||||
|
||||
setBounds(boundsTuple: LatLngTuple[] | undefined) {
|
||||
if(boundsTuple && boundsTuple.length > 0) {
|
||||
this.mapFitBounds = new LatLngBounds(boundsTuple);
|
||||
} else {
|
||||
this.mapFitBounds = latLng(this.defaultLat, this.defaultLng).toBounds(3000);
|
||||
}
|
||||
}
|
||||
|
||||
addMarker(marker: Marker) {
|
||||
this.mapLayers.push(marker);
|
||||
}
|
||||
|
||||
removeAllMarkers() {
|
||||
this.mapLayers.splice(1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { LeafletModule } from '@asymmetrik/ngx-leaflet';
|
||||
import { TranslationModule } from '../../translation.module';
|
||||
|
||||
import { MapComponent } from './map.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
MapComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
LeafletModule,
|
||||
TranslationModule
|
||||
],
|
||||
exports: [
|
||||
MapComponent
|
||||
]
|
||||
})
|
||||
export class MapModule { }
|
|
@ -65,8 +65,8 @@
|
|||
</div>
|
||||
<div [class.is-invalid-div]="!isFieldValid('lat')" class="mb-2">
|
||||
<label>{{ 'place'|translate|titlecase }}</label>
|
||||
<map-picker *ngIf="addingService" (onMarkerSet)="setPlace($event.lat, $event.lng)"></map-picker>
|
||||
<map-picker *ngIf="!addingService && loadedServiceLat !== ''" (onMarkerSet)="setPlace($event.lat, $event.lng)" [selectLat]="loadedServiceLat" [selectLng]="loadedServiceLng"></map-picker>
|
||||
<map-picker *ngIf="addingService" (markerSet)="setPlace($event.lat, $event.lng)"></map-picker>
|
||||
<map-picker *ngIf="!addingService && loadedServiceLat !== ''" (markerSet)="setPlace($event.lat, $event.lng)" [selectLat]="loadedServiceLat" [selectLng]="loadedServiceLng"></map-picker>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="notes">{{ 'notes'|translate|titlecase }}</label><br>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { StatsServicesComponent } from './stats-services/stats-services.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'services', component: StatsServicesComponent }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class StatsRoutingModule { }
|
|
@ -0,0 +1,7 @@
|
|||
<daterange-picker [(ngModel)]="range" (ngModelChange)="filterDateRangeChanged($event)"></daterange-picker>
|
||||
|
||||
<h3>Mappa degli interventi</h3>
|
||||
<map #servicesMap (mapClick)="serviceMapClick($event)"></map>
|
||||
|
||||
<h3 class="mt-3">Interventi per vigile</h3>
|
||||
<chart type="pie" [data]="data"></chart>
|
|
@ -0,0 +1,81 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ApiClientService } from 'src/app/_services/api-client.service';
|
||||
import { MapComponent } from 'src/app/_components/map/map.component';
|
||||
import { LatLngTuple, LatLngBounds, Marker } from 'leaflet';
|
||||
|
||||
@Component({
|
||||
selector: 'app-stats',
|
||||
templateUrl: './stats-services.component.html',
|
||||
styleUrls: ['./stats-services.component.scss']
|
||||
})
|
||||
export class StatsServicesComponent implements OnInit {
|
||||
services: any[] = [];
|
||||
servicesFilterStart: Date | undefined;
|
||||
servicesFilterEnd: Date | undefined;
|
||||
|
||||
data: any;
|
||||
|
||||
@ViewChild("servicesMap") servicesMap!: MapComponent;
|
||||
|
||||
range: (Date | undefined)[] | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
private toastr: ToastrService,
|
||||
private api: ApiClientService,
|
||||
private translate: TranslateService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadServices();
|
||||
|
||||
this.data = {
|
||||
labels: ['A', 'B', 'C'],
|
||||
datasets: [
|
||||
{
|
||||
data: [540, 325, 702]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
loadServices() {
|
||||
this.api.get("stats/services", {
|
||||
from: this.servicesFilterStart ? this.servicesFilterStart.toISOString() : undefined,
|
||||
to: this.servicesFilterEnd ? this.servicesFilterEnd.toISOString() : undefined
|
||||
}).then((response: any) => {
|
||||
this.services = response;
|
||||
console.log(this.services);
|
||||
|
||||
let serviceMapFitBoundsTuple = [];
|
||||
this.servicesMap.removeAllMarkers();
|
||||
for (let service of this.services) {
|
||||
const pos: LatLngTuple = [service.place.lat, service.place.lon];
|
||||
serviceMapFitBoundsTuple.push(pos);
|
||||
let marker = new Marker(pos);
|
||||
this.servicesMap.addMarker(marker);
|
||||
}
|
||||
this.servicesMap.setBounds(serviceMapFitBoundsTuple);
|
||||
}).catch((error: any) => {
|
||||
console.error(error);
|
||||
this.toastr.error(this.translate.instant("Error while loading services"));
|
||||
});
|
||||
}
|
||||
|
||||
serviceMapClick(e: any) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
filterDateRangeChanged($event: Date[]) {
|
||||
console.log($event);
|
||||
if ($event === undefined) {
|
||||
this.servicesFilterStart = undefined;
|
||||
this.servicesFilterEnd = undefined;
|
||||
} else {
|
||||
this.servicesFilterStart = $event[0];
|
||||
this.servicesFilterEnd = $event[1];
|
||||
}
|
||||
this.loadServices();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
|
||||
import { DatetimePickerModule } from '../../_components/datetime-picker/datetime-picker.module';
|
||||
import { DaterangePickerModule } from '../../_components/daterange-picker/daterange-picker.module';
|
||||
import { BackBtnModule } from '../../_components/back-btn/back-btn.module';
|
||||
import { MapModule } from 'src/app/_components/map/map.module';
|
||||
import { ChartModule } from '../../_components/chart/chart.module';
|
||||
import { TranslationModule } from '../../translation.module';
|
||||
|
||||
import { StatsRoutingModule } from './stats-routing.module';
|
||||
import { StatsServicesComponent } from './stats-services/stats-services.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
StatsServicesComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
StatsRoutingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
BsDatepickerModule.forRoot(),
|
||||
DatetimePickerModule,
|
||||
DaterangePickerModule,
|
||||
BackBtnModule,
|
||||
MapModule,
|
||||
ChartModule,
|
||||
TranslationModule
|
||||
]
|
||||
})
|
||||
export class StatsModule { }
|
|
@ -29,6 +29,11 @@ const routes: Routes = [
|
|||
loadChildren: () => import('./_routes/edit-training/edit-training.module').then(m => m.EditTrainingModule),
|
||||
canActivate: [AuthorizeGuard]
|
||||
},
|
||||
{
|
||||
path: 'stats',
|
||||
loadChildren: () => import('./_routes/stats/stats.module').then(m => m.StatsModule),
|
||||
canActivate: [AuthorizeGuard]
|
||||
},
|
||||
{ path: "login/:redirect/:extraParam", component: LoginComponent },
|
||||
{ path: "login/:redirect", component: LoginComponent },
|
||||
//
|
||||
|
|
|
@ -18,3 +18,7 @@
|
|||
color: green;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.leaflet-pane.leaflet-shadow-pane {
|
||||
display: none;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue