From 6d4db17e8f26775efffa9748ac5085167e4e861f Mon Sep 17 00:00:00 2001 From: Matteo Gheza Date: Sun, 31 Dec 2023 15:30:39 +0100 Subject: [PATCH] WIP support for stats page --- .../app/Http/Controllers/StatsController.php | 40 +++++++ backend/routes/api.php | 5 + frontend/package-lock.json | 65 +++++++++++ frontend/package.json | 7 +- .../_components/chart/chart.component.d.ts | 4 + .../_components/chart/chart.component.html | 1 + .../_components/chart/chart.component.scss | 5 + .../app/_components/chart/chart.component.ts | 74 +++++++++++++ .../src/app/_components/chart/chart.module.ts | 28 +++++ .../daterange-picker.component.html | 6 ++ .../daterange-picker.component.scss | 3 + .../daterange-picker.component.ts | 102 ++++++++++++++++++ .../daterange-picker.module.ts | 24 +++++ .../map-picker/map-picker.component.ts | 4 +- .../app/_components/map/map.component.html | 2 + .../app/_components/map/map.component.scss | 0 .../src/app/_components/map/map.component.ts | 73 +++++++++++++ .../src/app/_components/map/map.module.ts | 24 +++++ .../edit-service/edit-service.component.html | 4 +- .../app/_routes/stats/stats-routing.module.ts | 13 +++ .../stats-services.component.html | 7 ++ .../stats-services.component.scss | 0 .../stats-services.component.ts | 81 ++++++++++++++ .../src/app/_routes/stats/stats.module.ts | 34 ++++++ frontend/src/app/app-routing.module.ts | 5 + frontend/src/styles.scss | 4 + 26 files changed, 610 insertions(+), 5 deletions(-) create mode 100644 backend/app/Http/Controllers/StatsController.php create mode 100644 frontend/src/app/_components/chart/chart.component.d.ts create mode 100644 frontend/src/app/_components/chart/chart.component.html create mode 100644 frontend/src/app/_components/chart/chart.component.scss create mode 100644 frontend/src/app/_components/chart/chart.component.ts create mode 100644 frontend/src/app/_components/chart/chart.module.ts create mode 100644 frontend/src/app/_components/daterange-picker/daterange-picker.component.html create mode 100644 frontend/src/app/_components/daterange-picker/daterange-picker.component.scss create mode 100644 frontend/src/app/_components/daterange-picker/daterange-picker.component.ts create mode 100644 frontend/src/app/_components/daterange-picker/daterange-picker.module.ts create mode 100644 frontend/src/app/_components/map/map.component.html create mode 100644 frontend/src/app/_components/map/map.component.scss create mode 100644 frontend/src/app/_components/map/map.component.ts create mode 100644 frontend/src/app/_components/map/map.module.ts create mode 100644 frontend/src/app/_routes/stats/stats-routing.module.ts create mode 100644 frontend/src/app/_routes/stats/stats-services/stats-services.component.html create mode 100644 frontend/src/app/_routes/stats/stats-services/stats-services.component.scss create mode 100644 frontend/src/app/_routes/stats/stats-services/stats-services.component.ts create mode 100644 frontend/src/app/_routes/stats/stats.module.ts 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; +}