diff --git a/backend/app/Http/Controllers/StatsController.php b/backend/app/Http/Controllers/StatsController.php
new file mode 100644
index 0000000..e7cd51e
--- /dev/null
+++ b/backend/app/Http/Controllers/StatsController.php
@@ -0,0 +1,40 @@
+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()
+ );
+ }
+}
diff --git a/backend/routes/api.php b/backend/routes/api.php
index af9c5b4..92908e4 100644
--- a/backend/routes/api.php
+++ b/backend/routes/api.php
@@ -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']);
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 2601ed0..3db458e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -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",
diff --git a/frontend/package.json b/frontend/package.json
index b543f5c..b9644c2 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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"
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/app/_components/chart/chart.component.d.ts b/frontend/src/app/_components/chart/chart.component.d.ts
new file mode 100644
index 0000000..92121e1
--- /dev/null
+++ b/frontend/src/app/_components/chart/chart.component.d.ts
@@ -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
+}
diff --git a/frontend/src/app/_components/chart/chart.component.html b/frontend/src/app/_components/chart/chart.component.html
new file mode 100644
index 0000000..fe0baac
--- /dev/null
+++ b/frontend/src/app/_components/chart/chart.component.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/app/_components/chart/chart.component.scss b/frontend/src/app/_components/chart/chart.component.scss
new file mode 100644
index 0000000..58e31b3
--- /dev/null
+++ b/frontend/src/app/_components/chart/chart.component.scss
@@ -0,0 +1,5 @@
+@media (max-width: 600px) {
+ .chartjs-render-monitor {
+ height: 700px !important;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/_components/chart/chart.component.ts b/frontend/src/app/_components/chart/chart.component.ts
new file mode 100644
index 0000000..5b1fe1b
--- /dev/null
+++ b/frontend/src/app/_components/chart/chart.component.ts
@@ -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%';
+ }
+ }
+
+}
diff --git a/frontend/src/app/_components/chart/chart.module.ts b/frontend/src/app/_components/chart/chart.module.ts
new file mode 100644
index 0000000..79e7d90
--- /dev/null
+++ b/frontend/src/app/_components/chart/chart.module.ts
@@ -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 { }
diff --git a/frontend/src/app/_components/daterange-picker/daterange-picker.component.html b/frontend/src/app/_components/daterange-picker/daterange-picker.component.html
new file mode 100644
index 0000000..f5191a2
--- /dev/null
+++ b/frontend/src/app/_components/daterange-picker/daterange-picker.component.html
@@ -0,0 +1,6 @@
+
+ Filtra interventi per data
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/_components/daterange-picker/daterange-picker.component.scss b/frontend/src/app/_components/daterange-picker/daterange-picker.component.scss
new file mode 100644
index 0000000..0c8a794
--- /dev/null
+++ b/frontend/src/app/_components/daterange-picker/daterange-picker.component.scss
@@ -0,0 +1,3 @@
+.date-picker {
+ width: 85%
+}
\ No newline at end of file
diff --git a/frontend/src/app/_components/daterange-picker/daterange-picker.component.ts b/frontend/src/app/_components/daterange-picker/daterange-picker.component.ts
new file mode 100644
index 0000000..1738f07
--- /dev/null
+++ b/frontend/src/app/_components/daterange-picker/daterange-picker.component.ts
@@ -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();
+ }
+}
diff --git a/frontend/src/app/_components/daterange-picker/daterange-picker.module.ts b/frontend/src/app/_components/daterange-picker/daterange-picker.module.ts
new file mode 100644
index 0000000..6919c24
--- /dev/null
+++ b/frontend/src/app/_components/daterange-picker/daterange-picker.module.ts
@@ -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 { }
diff --git a/frontend/src/app/_components/map-picker/map-picker.component.ts b/frontend/src/app/_components/map-picker/map-picker.component.ts
index 4b56b10..c857f06 100644
--- a/frontend/src/app/_components/map-picker/map-picker.component.ts
+++ b/frontend/src/app/_components/map-picker/map-picker.component.ts
@@ -17,7 +17,7 @@ export class MapPickerComponent implements OnInit {
@Input() selectLat = "";
@Input() selectLng = "";
- @Output() onMarkerSet = new EventEmitter();
+ @Output() markerSet = new EventEmitter();
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
});
diff --git a/frontend/src/app/_components/map/map.component.html b/frontend/src/app/_components/map/map.component.html
new file mode 100644
index 0000000..1395654
--- /dev/null
+++ b/frontend/src/app/_components/map/map.component.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/app/_components/map/map.component.scss b/frontend/src/app/_components/map/map.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/app/_components/map/map.component.ts b/frontend/src/app/_components/map/map.component.ts
new file mode 100644
index 0000000..158a6c6
--- /dev/null
+++ b/frontend/src/app/_components/map/map.component.ts
@@ -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();
+
+ 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: '© OpenStreetMap 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: '© © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap (CC-BY-SA)' }),
+ 'Open Street Map Hot': tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', { maxZoom: 20, attribution: '© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France' }),
+ 'Stadia Dark': tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png', { maxZoom: 20, attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors' }),
+ },
+ overlays: {
+ 'Open Railway Map': tileLayer('https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', { maxZoom: 19, attribution: 'Map data: © OpenStreetMap contributors | Map style: © OpenRailwayMap (CC-BY-SA)' }),
+ 'Open Fire Map': tileLayer('http://openfiremap.org/hytiles/{z}/{x}/{y}.png', { maxZoom: 19, attribution: 'Map data: © OpenStreetMap contributors | Map style: © OpenFireMap (CC-BY-SA)' }),
+ 'SafeCast': tileLayer('https://s3.amazonaws.com/te512.safecast.org/{z}/{x}/{y}.png', { maxZoom: 16, attribution: 'Map data: © OpenStreetMap contributors | Map style: © SafeCast (CC-BY-SA)' })
+ }
+ };
+ 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);
+ }
+}
diff --git a/frontend/src/app/_components/map/map.module.ts b/frontend/src/app/_components/map/map.module.ts
new file mode 100644
index 0000000..d56b31c
--- /dev/null
+++ b/frontend/src/app/_components/map/map.module.ts
@@ -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 { }
diff --git a/frontend/src/app/_routes/edit-service/edit-service.component.html b/frontend/src/app/_routes/edit-service/edit-service.component.html
index 00d3753..1d6faa9 100644
--- a/frontend/src/app/_routes/edit-service/edit-service.component.html
+++ b/frontend/src/app/_routes/edit-service/edit-service.component.html
@@ -65,8 +65,8 @@
-
-
+
+
diff --git a/frontend/src/app/_routes/stats/stats-routing.module.ts b/frontend/src/app/_routes/stats/stats-routing.module.ts
new file mode 100644
index 0000000..234f40c
--- /dev/null
+++ b/frontend/src/app/_routes/stats/stats-routing.module.ts
@@ -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 { }
diff --git a/frontend/src/app/_routes/stats/stats-services/stats-services.component.html b/frontend/src/app/_routes/stats/stats-services/stats-services.component.html
new file mode 100644
index 0000000..6afb7b2
--- /dev/null
+++ b/frontend/src/app/_routes/stats/stats-services/stats-services.component.html
@@ -0,0 +1,7 @@
+
+
+
Mappa degli interventi
+
+
+Interventi per vigile
+
diff --git a/frontend/src/app/_routes/stats/stats-services/stats-services.component.scss b/frontend/src/app/_routes/stats/stats-services/stats-services.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/app/_routes/stats/stats-services/stats-services.component.ts b/frontend/src/app/_routes/stats/stats-services/stats-services.component.ts
new file mode 100644
index 0000000..69420fd
--- /dev/null
+++ b/frontend/src/app/_routes/stats/stats-services/stats-services.component.ts
@@ -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();
+ }
+}
diff --git a/frontend/src/app/_routes/stats/stats.module.ts b/frontend/src/app/_routes/stats/stats.module.ts
new file mode 100644
index 0000000..09bd034
--- /dev/null
+++ b/frontend/src/app/_routes/stats/stats.module.ts
@@ -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 { }
diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts
index a774dd3..648ab58 100644
--- a/frontend/src/app/app-routing.module.ts
+++ b/frontend/src/app/app-routing.module.ts
@@ -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 },
//
diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss
index cf9a3b6..19eb119 100644
--- a/frontend/src/styles.scss
+++ b/frontend/src/styles.scss
@@ -18,3 +18,7 @@
color: green;
font-size: 1.5em;
}
+
+.leaflet-pane.leaflet-shadow-pane {
+ display: none;
+}