Add new service place selection procedure

This commit is contained in:
Matteo Gheza 2024-02-23 00:27:22 +01:00
parent 12fdfb3058
commit ee310a3155
No known key found for this signature in database
GPG Key ID: A7019AD593CEF319
25 changed files with 1366 additions and 983 deletions

View File

@ -3,7 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Auth; use App\Models\Option;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -94,13 +94,26 @@ class AuthController extends Controller
public function me(Request $request) public function me(Request $request)
{ {
$impersonateManager = app('impersonate'); $impersonateManager = app('impersonate');
$options = Option::all(["name", "value", "type"]);
//Cast the value to the correct type and remove type
foreach($options as $option) {
if($option->type == "boolean") {
$option->value = boolval($option->value);
} else if($option->type == "number") {
$option->value = floatval($option->value);
}
unset($option->type);
}
return [ return [
...$request->user()->toArray(), ...$request->user()->toArray(),
"permissions" => array_map(function($p) { "permissions" => array_map(function($p) {
return $p["name"]; return $p["name"];
}, $request->user()->allPermissions()->toArray()), }, $request->user()->allPermissions()->toArray()),
"impersonating_user" => $impersonateManager->isImpersonating(), "impersonating_user" => $impersonateManager->isImpersonating(),
"impersonator_id" => $impersonateManager->getImpersonatorId() "impersonator_id" => $impersonateManager->getImpersonatorId(),
"options" => $options
]; ];
} }

View File

@ -64,7 +64,9 @@ class PlacesController extends Controller
User::where('id', $request->user()->id)->update(['last_access' => now()]); User::where('id', $request->user()->id)->update(['last_access' => now()]);
return response()->json( return response()->json(
Place::find($id) Place::where('id', $id)
->with('municipality', 'municipality.province')
->firstOrFail()
); );
} }
} }

View File

@ -3,13 +3,17 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Place; use App\Models\Place;
use App\Models\PlaceMunicipality;
use App\Models\PlaceProvince;
use App\Models\Service; use App\Models\Service;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use App\Utils\Logger; use App\Utils\Logger;
use App\Utils\DBTricks; use App\Utils\DBTricks;
use App\Utils\Helpers;
class ServiceController extends Controller class ServiceController extends Controller
{ {
@ -18,31 +22,48 @@ class ServiceController extends Controller
*/ */
public function index(Request $request) public function index(Request $request)
{ {
if(!$request->user()->hasPermission("services-read")) abort(401); if (!$request->user()->hasPermission("services-read")) abort(401);
User::where('id', $request->user()->id)->update(['last_access' => now()]); User::where('id', $request->user()->id)->update(['last_access' => now()]);
$query = Service::join('users', 'users.id', '=', 'chief_id') $query = Service::join('users', 'users.id', '=', 'chief_id')
->join('services_types', 'services_types.id', '=', 'type_id') ->join('services_types', 'services_types.id', '=', 'type_id')
->select('services.*', DBTricks::nameSelect("chief", "users"), 'services_types.name as type') ->select(
'services.*', DBTricks::nameSelect("chief", "users"),
'services_types.name as type'
)
->with('drivers:name,surname') ->with('drivers:name,surname')
->with('crew:name,surname') ->with('crew:name,surname')
->with('place') ->with('place.municipality.province')
->orderBy('start', 'desc'); ->orderBy('start', 'desc');
if($request->has('from')) { if ($request->has('from')) {
try { try {
$from = Carbon::parse($request->input('from')); $from = Carbon::parse($request->input('from'));
$query->whereDate('start', '>=', $from->toDateString()); $query->whereDate('start', '>=', $from->toDateString());
} catch (\Carbon\Exceptions\InvalidFormatException $e) { } } catch (\Carbon\Exceptions\InvalidFormatException $e) {
}
} }
if($request->has('to')) { if ($request->has('to')) {
try { try {
$to = Carbon::parse($request->input('to')); $to = Carbon::parse($request->input('to'));
$query->whereDate('start', '<=', $to->toDateString()); $query->whereDate('start', '<=', $to->toDateString());
} catch (\Carbon\Exceptions\InvalidFormatException $e) { } } catch (\Carbon\Exceptions\InvalidFormatException $e) {
}
} }
return response()->json(
$query->get() $result = $query->get();
); foreach ($result as $service) {
if($service->place->municipality) {
$m = $service->place->municipality;
unset(
$m->cadastral_code, $m->email, $m->fax, $m->latitude, $m->longitude,
$m->phone, $m->pec, $m->prefix, $m->foreign_name, $m->province_id
);
}
$p = $service->place;
unset($p->lat, $p->lon, $p->place_id, $p->osm_id, $p->osm_type, $p->licence, $p->addresstype, $p->country, $p->country_code, $p->display_name, $p->road, $p->house_number, $p->postcode, $p->state, $p->suburb, $p->city, $p->municipality_id);
}
return response()->json($result);
} }
/** /**
@ -50,7 +71,7 @@ class ServiceController extends Controller
*/ */
public function show(Request $request, $id) public function show(Request $request, $id)
{ {
if(!$request->user()->hasPermission("services-read")) abort(401); if (!$request->user()->hasPermission("services-read")) abort(401);
User::where('id', $request->user()->id)->update(['last_access' => now()]); User::where('id', $request->user()->id)->update(['last_access' => now()]);
return response()->json( return response()->json(
@ -59,7 +80,7 @@ class ServiceController extends Controller
->select('services.*', DBTricks::nameSelect("chief", "users"), 'services_types.name as type') ->select('services.*', DBTricks::nameSelect("chief", "users"), 'services_types.name as type')
->with('drivers:name,surname') ->with('drivers:name,surname')
->with('crew:name,surname') ->with('crew:name,surname')
->with('place') ->with('place.municipality.province')
->find($id) ->find($id)
); );
} }
@ -67,10 +88,10 @@ class ServiceController extends Controller
private function extractServiceUsers($service) private function extractServiceUsers($service)
{ {
$usersList = [$service->chief_id]; $usersList = [$service->chief_id];
foreach($service->drivers as $driver) { foreach ($service->drivers as $driver) {
$usersList[] = $driver->id; $usersList[] = $driver->id;
} }
foreach($service->crew as $crew) { foreach ($service->crew as $crew) {
$usersList[] = $crew->id; $usersList[] = $crew->id;
} }
return array_unique($usersList); return array_unique($usersList);
@ -83,14 +104,14 @@ class ServiceController extends Controller
{ {
$adding = !isset($request->id) || is_null($request->id); $adding = !isset($request->id) || is_null($request->id);
if(!$adding && !$request->user()->hasPermission("services-update")) abort(401); if (!$adding && !$request->user()->hasPermission("services-update")) abort(401);
if($adding && !$request->user()->hasPermission("services-create")) abort(401); if ($adding && !$request->user()->hasPermission("services-create")) abort(401);
$service = $adding ? new Service() : Service::where("id",$request->id)->with('drivers')->with('crew')->first(); $service = $adding ? new Service() : Service::where("id", $request->id)->with('drivers')->with('crew')->first();
if(is_null($service)) abort(404); if (is_null($service)) abort(404);
if(!$adding) { if (!$adding) {
$usersToDecrement = $this->extractServiceUsers($service); $usersToDecrement = $this->extractServiceUsers($service);
User::whereIn('id', $usersToDecrement)->decrement('services'); User::whereIn('id', $usersToDecrement)->decrement('services');
@ -99,36 +120,111 @@ class ServiceController extends Controller
$service->save(); $service->save();
} }
//Find Place by lat lon $is_map_picker = Helpers::get_option('service_place_selection_use_map_picker', false);
$place = Place::where('lat', $request->lat)->where('lon', $request->lon)->first();
if(!$place) { if ($is_map_picker) {
//Find Place by lat lon
$place = Place::where('lat', $request->place->lat)->where('lon', $request->place->lon)->first();
if (!$place) {
$place = new Place();
$place->lat = $request->place->lat;
$place->lon = $request->place->lon;
$response = Http::withUrlParameters([
'lat' => $request->place->lat,
'lon' => $request->place->lon,
])->get('https://nominatim.openstreetmap.org/reverse?format=json&lat={lat}&lon={lon}');
if (!$response->ok()) abort(500);
$place->place_id = isset($response["place_id"]) ? $response["place_id"] : null;
$place->osm_id = isset($response["osm_id"]) ? $response["osm_id"] : null;
$place->osm_type = isset($response["osm_type"]) ? $response["osm_type"] : null;
$place->licence = isset($response["licence"]) ? $response["licence"] : null;
$place->addresstype = isset($response["addresstype"]) ? $response["addresstype"] : null;
$place->country = isset($response["address"]["country"]) ? $response["address"]["country"] : null;
$place->country_code = isset($response["address"]["country_code"]) ? $response["address"]["country_code"] : null;
$place->name = isset($response["name"]) ? $response["name"] : null;
$place->display_name = isset($response["display_name"]) ? $response["display_name"] : null;
$place->road = isset($response["address"]["road"]) ? $response["address"]["road"] : null;
$place->house_number = isset($response["address"]["house_number"]) ? $response["address"]["house_number"] : null;
$place->postcode = isset($response["address"]["postcode"]) ? $response["address"]["postcode"] : null;
$place->state = isset($response["address"]["state"]) ? $response["address"]["state"] : null;
$place->village = isset($response["address"]["village"]) ? $response["address"]["village"] : null;
$place->suburb = isset($response["address"]["suburb"]) ? $response["address"]["suburb"] : null;
$place->city = isset($response["address"]["city"]) ? $response["address"]["city"] : null;
$place->save();
}
} else {
if (!$adding) {
//Delete old place
$place = $service->place;
$service->place()->dissociate();
$service->save();
$place->delete();
}
$place = new Place(); $place = new Place();
$place->lat = $request->lat; $place->name = $request->place["address"];
$place->lon = $request->lon;
$response = Http::withUrlParameters([ //Check if municipality exists
'lat' => $request->lat, $municipality = PlaceMunicipality::where('code', $request->place["municipalityCode"])->first();
'lon' => $request->lon, if (!$municipality) {
])->get('https://nominatim.openstreetmap.org/reverse?format=json&lat={lat}&lon={lon}'); //Check if province exists
if(!$response->ok()) abort(500); $province = PlaceProvince::where('code', $request->place["provinceCode"])->first();
if (!$province) {
$provinces = Cache::remember('italy_provinces_all', 60 * 60 * 24 * 365, function () {
return Http::get('https://axqvoqvbfjpaamphztgd.functions.supabase.co/province/')->object();
});
$place->place_id = isset($response["place_id"]) ? $response["place_id"] : null; //Find province
$place->osm_id = isset($response["osm_id"]) ? $response["osm_id"] : null; foreach ($provinces as $p) {
$place->osm_type = isset($response["osm_type"]) ? $response["osm_type"] : null; if ($p->codice == $request->place["provinceCode"]) {
$place->licence = isset($response["licence"]) ? $response["licence"] : null; $province = new PlaceProvince();
$place->addresstype = isset($response["addresstype"]) ? $response["addresstype"] : null; $province->code = $p->codice;
$place->country = isset($response["address"]["country"]) ? $response["address"]["country"] : null; $province->name = $p->nome;
$place->country_code = isset($response["address"]["country_code"]) ? $response["address"]["country_code"] : null; $province->short_name = $p->sigla;
$place->name = isset($response["name"]) ? $response["name"] : null; $province->region = $p->regione;
$place->display_name = isset($response["display_name"]) ? $response["display_name"] : null; $province->save();
$place->road = isset($response["address"]["road"]) ? $response["address"]["road"] : null; break;
$place->house_number = isset($response["address"]["house_number"]) ? $response["address"]["house_number"] : null; }
$place->postcode = isset($response["address"]["postcode"]) ? $response["address"]["postcode"] : null; }
$place->state = isset($response["address"]["state"]) ? $response["address"]["state"] : null; if (!$province) {
$place->village = isset($response["address"]["village"]) ? $response["address"]["village"] : null; abort(400);
$place->suburb = isset($response["address"]["suburb"]) ? $response["address"]["suburb"] : null; }
$place->city = isset($response["address"]["city"]) ? $response["address"]["city"] : null; }
$place->municipality = isset($response["address"]["municipality"]) ? $response["address"]["municipality"] : null;
$province_name = $province->name;
$municipalities = Cache::remember('italy_municipalities_' . $province_name, 60 * 60 * 24 * 365, function () use ($province_name) {
return Http::get('https://axqvoqvbfjpaamphztgd.functions.supabase.co/comuni/provincia/' . $province_name)->object();
});
//Find municipality
foreach ($municipalities as $m) {
if ($m->codice == $request->place["municipalityCode"]) {
$municipality = new PlaceMunicipality();
$municipality->code = $m->codice;
$municipality->name = $m->nome;
$municipality->foreign_name = $m->nomeStraniero;
$municipality->cadastral_code = $m->codiceCatastale;
$municipality->postal_code = $m->cap;
$municipality->prefix = $m->prefisso;
$municipality->email = $m->email;
$municipality->pec = $m->pec;
$municipality->phone = $m->telefono;
$municipality->fax = $m->fax;
$municipality->latitude = $m->coordinate->lat;
$municipality->longitude = $m->coordinate->lng;
$municipality->province()->associate($province);
$municipality->save();
break;
}
}
if (!$municipality) {
abort(400);
}
}
$place->municipality()->associate($municipality);
$place->save(); $place->save();
} }
@ -137,8 +233,8 @@ class ServiceController extends Controller
$service->chief()->associate($request->chief); $service->chief()->associate($request->chief);
$service->type()->associate($request->type); $service->type()->associate($request->type);
$service->notes = $request->notes; $service->notes = $request->notes;
$service->start = $request->start/1000; $service->start = $request->start / 1000;
$service->end = $request->end/1000; $service->end = $request->end / 1000;
$service->place()->associate($place); $service->place()->associate($place);
$service->addedBy()->associate($request->user()); $service->addedBy()->associate($request->user());
$service->updatedBy()->associate($request->user()); $service->updatedBy()->associate($request->user());
@ -163,7 +259,7 @@ class ServiceController extends Controller
*/ */
public function destroy(Request $request, $id) public function destroy(Request $request, $id)
{ {
if(!$request->user()->hasPermission("services-delete")) abort(401); if (!$request->user()->hasPermission("services-delete")) abort(401);
$service = Service::find($id); $service = Service::find($id);
$usersToDecrement = $this->extractServiceUsers($service); $usersToDecrement = $this->extractServiceUsers($service);
User::whereIn('id', $usersToDecrement)->decrement('services'); User::whereIn('id', $usersToDecrement)->decrement('services');

View File

@ -251,12 +251,4 @@ class UserController extends Controller
return response()->json($user); return response()->json($user);
} }
/**
* Remove the specified resource from storage.
*/
public function destroy(User $user)
{
//
}
} }

View File

@ -4,6 +4,8 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Models\PlaceMunicipality;
class Place extends Model class Place extends Model
{ {
@ -32,7 +34,13 @@ class Place extends Model
'state', 'state',
'village', 'village',
'suburb', 'suburb',
'city', 'city'
'municipality'
]; ];
/**
* Get the municipality
*/
public function municipality(): BelongsTo {
return $this->belongsTo(PlaceMunicipality::class);
}
} }

View File

@ -0,0 +1,23 @@
<?php
namespace App\Utils;
use App\Models\Option;
class Helpers {
public static function get_option($key, $default = null) {
$option = Option::where('name', $key)->first();
if($option) {
// Cast to correct type
if($option->type == "boolean") {
return $option->value == "true";
} else if($option->type == "number") {
return floatval($option->value);
} else {
return $option->value;
}
}
return $default;
}
}

View File

@ -11,14 +11,14 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('PlaceProvince', function (Blueprint $table) { Schema::create('place_provinces', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('code', 2)->unique(); $table->string('code', 20)->unique();
$table->string('name', 100); $table->string('name', 100);
$table->string('short_name', 2); $table->string('short_name', 2);
$table->string('region', 25); $table->string('region', 25);
}); });
Schema::create('PlaceMunicipality', function (Blueprint $table) { Schema::create('place_municipalities', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('code', 6)->unique(); $table->string('code', 6)->unique();
$table->string('name', 200); $table->string('name', 200);
@ -32,7 +32,7 @@ return new class extends Migration
$table->string('fax', 30)->nullable(); $table->string('fax', 30)->nullable();
$table->decimal('latitude', 10, 8)->nullable(); $table->decimal('latitude', 10, 8)->nullable();
$table->decimal('longitude', 11, 8)->nullable(); $table->decimal('longitude', 11, 8)->nullable();
$table->foreignId('province_id')->constrained('PlaceProvince'); $table->foreignId('province_id')->constrained('place_provinces');
}); });
} }
@ -41,7 +41,7 @@ return new class extends Migration
*/ */
public function down(): void public function down(): void
{ {
Schema::dropIfExists('PlaceMunicipality'); Schema::dropIfExists('place_municipalities');
Schema::dropIfExists('PlaceProvince'); Schema::dropIfExists('place_provinces');
} }
}; };

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('places', function (Blueprint $table) {
$table->dropColumn('municipality');
$table->foreignId('municipality_id')->constrained('place_municipalities')->nullable();
$table->float('lat', 10, 6)->nullable()->change();
$table->float('lon', 10, 6)->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('places', function (Blueprint $table) {
$table->dropColumn('municipality_id');
$table->string('municipality')->nullable();
$table->float('lat', 10, 6)->nullable(false)->change();
$table->float('lon', 10, 6)->nullable(false)->change();
});
}
};

View File

@ -14,7 +14,7 @@ class OptionsSeeder extends Seeder
{ {
$options = [ $options = [
[ [
'name' => 'service_place_selection_manual', 'name' => 'service_place_selection_use_map_picker',
'value' => true, 'value' => true,
'type' => 'boolean' 'type' => 'boolean'
] ]

View File

@ -0,0 +1,22 @@
<div class="border border-2 p-2 pb-0">
<div class="input-group mb-3">
<span class="input-group-text" id="region-label">{{ 'region'|translate|titlecase }}</span>
<input [(ngModel)]="selectedRegion" [typeahead]="regions" (typeaheadOnSelect)="onRegionSelected()"
class="form-control" aria-describedby="region-label">
</div>
<div class="input-group mb-3" *ngIf="regionSelected">
<span class="input-group-text" id="province-label">{{ 'province'|translate|titlecase }}</span>
<input [(ngModel)]="selectedProvince" [typeahead]="provinces" typeaheadOptionField="nome" (typeaheadOnSelect)="onProvinceSelected($event)"
class="form-control" aria-describedby="province-label">
</div>
<div class="input-group mb-3" *ngIf="provinceSelected">
<span class="input-group-text" id="municipality-label">{{ 'place_details.municipality'|translate|titlecase }}</span>
<input [(ngModel)]="selectedMunicipality" [typeahead]="municipalities" typeaheadOptionField="nome" (typeaheadOnSelect)="onMunicipalitySelected($event)"
class="form-control" aria-describedby="municipality-label">
</div>
<div class="input-group mb-3" *ngIf="municipalitySelected">
<span class="input-group-text" id="address-label">{{ 'address'|translate|titlecase }}</span>
<input [(ngModel)]="selectedAddress" (change)="onAddressChanged()"
class="form-control" aria-describedby="address-label" type="text">
</div>
</div>

View File

@ -0,0 +1,99 @@
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { TranslateService } from '@ngx-translate/core';
import { ApiClientService } from 'src/app/_services/api-client.service';
interface Test {
codice: string;
}
@Component({
selector: 'place-picker',
templateUrl: './place-picker.component.html',
styleUrls: ['./place-picker.component.scss']
})
export class PlacePickerComponent implements OnInit {
selectedRegion?: string;
regions: string[] = [];
selectedProvince?: string;
selectedProvinceCodice?: string;
provinces: string[] = [];
selectedMunicipality?: string;
selectedMunicipalityCodice?: string;
municipalities: string[] = [];
selectedAddress?: string;
regionSelected = false;
provinceSelected = false;
municipalitySelected = false;
addressSelected = false;
@Output() addrSel = new EventEmitter<any>();
constructor(private toastr: ToastrService, private api: ApiClientService, private translate: TranslateService) {
this.api.get('places/italy/regions').then((res: any) => {
this.regions = res;
console.log(this.regions);
}).catch((err: any) => {
console.error(err);
this.toastr.error(this.translate.instant("error_loading_regions"));
});
}
ngOnInit() {
}
onRegionSelected() {
this.selectedProvince = "";
this.selectedMunicipality = "";
this.selectedAddress = "";
this.provinceSelected = false;
this.municipalitySelected = false;
this.api.get('places/italy/provinces/' + this.selectedRegion).then((res: any) => {
this.provinces = res;
console.log(this.provinces);
this.regionSelected = true;
}).catch((err: any) => {
console.error(err);
this.toastr.error(this.translate.instant("error_loading_provinces"));
});
}
onProvinceSelected(event: any) {
this.selectedMunicipality = "";
this.selectedAddress = "";
this.municipalitySelected = false;
this.api.get('places/italy/municipalities/' + this.selectedProvince).then((res: any) => {
this.municipalities = res;
console.log(this.municipalities);
this.selectedProvinceCodice = event.item.codice;
this.provinceSelected = true;
}).catch((err: any) => {
console.error(err);
this.toastr.error(this.translate.instant("error_loading_municipalities"));
});
}
onMunicipalitySelected(event: any) {
this.selectedAddress = "";
this.selectedMunicipalityCodice = event.item.codice;
this.municipalitySelected = true;
}
onAddressChanged() {
this.addrSel.emit({
region: this.selectedRegion,
province: this.selectedProvinceCodice,
municipality: this.selectedMunicipalityCodice,
address: this.selectedAddress
});
}
}

View File

@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TypeaheadModule } from 'ngx-bootstrap/typeahead';
import { TranslationModule } from '../../translation.module';
import { PlacePickerComponent } from './place-picker.component';
@NgModule({
declarations: [
PlacePickerComponent
],
imports: [
CommonModule,
FormsModule,
TypeaheadModule,
TranslationModule
],
exports: [
PlacePickerComponent
]
})
export class PlacePickerModule { }

View File

@ -138,6 +138,7 @@
<td> <td>
<ng-container *ngIf="row.place.name"><i>{{ row.place.name }}</i></ng-container><br> <ng-container *ngIf="row.place.name"><i>{{ row.place.name }}</i></ng-container><br>
<ng-container *ngIf="row.place.village">{{ row.place.village }}</ng-container><br> <ng-container *ngIf="row.place.village">{{ row.place.village }}</ng-container><br>
<ng-container *ngIf="row.place.municipality">{{ row.place.municipality.name }} {{ row.place.municipality.province.short_name }}</ng-container><br>
<a class="place_details_link cursor-pointer" (click)="openPlaceDetails(row.place.id)">{{ 'more details'|translate|ftitlecase }}</a> <a class="place_details_link cursor-pointer" (click)="openPlaceDetails(row.place.id)">{{ 'more details'|translate|ftitlecase }}</a>
</td> </td>
<td>{{ row.notes }}</td> <td>{{ row.notes }}</td>

View File

@ -63,10 +63,15 @@
</div> </div>
</ng-container> </ng-container>
</div> </div>
<div [class.is-invalid-div]="!isFieldValid('lat')" class="mb-2"> <div [class.is-invalid-div]="!isFieldValid('lat')" class="mb-2" *ngIf="usingMapSelector">
<label>{{ 'place'|translate|ftitlecase }}</label> <label>{{ 'place'|translate|ftitlecase }}</label>
<map-picker *ngIf="addingService" (markerSet)="setPlace($event.lat, $event.lng)"></map-picker> <map-picker *ngIf="addingService" (markerSet)="setPlaceMap($event.lat, $event.lng)"></map-picker>
<map-picker *ngIf="!addingService && loadedServiceLat !== ''" (markerSet)="setPlace($event.lat, $event.lng)" [selectLat]="loadedServiceLat" [selectLng]="loadedServiceLng"></map-picker> <map-picker *ngIf="!addingService && loadedServiceLat !== ''" (markerSet)="setPlaceMap($event.lat, $event.lng)" [selectLat]="loadedServiceLat" [selectLng]="loadedServiceLng"></map-picker>
</div>
<div [class.is-invalid-div]="!isFieldValid('address')" class="mb-2" *ngIf="!usingMapSelector">
<label>{{ 'place'|translate|ftitlecase }}</label>
<place-picker *ngIf="addingService" (addrSel)="setPlace($event.province, $event.municipality, $event.address)"></place-picker>
<place-picker *ngIf="!addingService && loadedServiceLat !== ''" (addrSel)="setPlace($event.province, $event.municipality, $event.address)"></place-picker>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="notes">{{ 'notes'|translate|ftitlecase }}</label><br> <label for="notes">{{ 'notes'|translate|ftitlecase }}</label><br>

View File

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { AbstractControl, UntypedFormBuilder, ValidationErrors, Validators } from '@angular/forms'; import { AbstractControl, UntypedFormBuilder, ValidationErrors, Validators } from '@angular/forms';
import { ApiClientService } from 'src/app/_services/api-client.service'; import { ApiClientService } from 'src/app/_services/api-client.service';
import { AuthService } from 'src/app/_services/auth.service';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -22,11 +23,15 @@ export class EditServiceComponent implements OnInit {
crew: [], crew: [],
lat: -1, lat: -1,
lon: -1, lon: -1,
provinceCode: '',
municipalityCode: '',
address: '',
notes: '', notes: '',
type: '' type: ''
}; };
loadedServiceLat = ""; loadedServiceLat = "";
loadedServiceLng = ""; loadedServiceLng = "";
usingMapSelector = true;
users: any[] = []; users: any[] = [];
types: any[] = []; types: any[] = [];
@ -44,8 +49,9 @@ export class EditServiceComponent implements OnInit {
get chief() { return this.serviceForm.get('chief'); } get chief() { return this.serviceForm.get('chief'); }
get drivers() { return this.serviceForm.get('drivers'); } get drivers() { return this.serviceForm.get('drivers'); }
get crew() { return this.serviceForm.get('crew'); } get crew() { return this.serviceForm.get('crew'); }
get lat() { return this.serviceForm.get('lat'); } get lat() { return this.serviceForm.get('place.lat'); }
get lon() { return this.serviceForm.get('lon'); } get lon() { return this.serviceForm.get('place.lon'); }
get address() { return this.serviceForm.get('place.address'); }
get type() { return this.serviceForm.get('type'); } get type() { return this.serviceForm.get('type'); }
ngOnInit() { ngOnInit() {
@ -56,14 +62,26 @@ export class EditServiceComponent implements OnInit {
chief: [this.loadedService.chief, [Validators.required]], chief: [this.loadedService.chief, [Validators.required]],
drivers: [this.loadedService.drivers, []], drivers: [this.loadedService.drivers, []],
crew: [this.loadedService.crew, [Validators.required]], crew: [this.loadedService.crew, [Validators.required]],
lat: [this.loadedService.lat, [Validators.required, (control: AbstractControl): ValidationErrors | null => { place: this.fb.group({
const valid = control.value >= -90 && control.value <= 90; lat: [this.loadedService.lat, this.usingMapSelector ?
return valid ? null : { 'invalidLatitude': { value: control.value } }; [Validators.required, (control: AbstractControl): ValidationErrors | null => {
}]], const valid = control.value >= -90 && control.value <= 90;
lon: [this.loadedService.lon, [Validators.required, (control: AbstractControl): ValidationErrors | null => { return valid ? null : { 'invalidLatitude': { value: control.value } };
const valid = control.value >= -180 && control.value <= 180; }] : []
return valid ? null : { 'invalidLongitude': { value: control.value } }; ],
}]], lon: [this.loadedService.lon, this.usingMapSelector ?
[Validators.required, (control: AbstractControl): ValidationErrors | null => {
const valid = control.value >= -180 && control.value <= 180;
return valid ? null : { 'invalidLongitude': { value: control.value } };
}] : []
],
provinceCode: [this.loadedService.provinceCode, this.usingMapSelector ?
[] : [Validators.required, Validators.minLength(3)]],
municipalityCode: [this.loadedService.municipalityCode, this.usingMapSelector ?
[] : [Validators.required, Validators.minLength(3)]],
address: [this.loadedService.address, this.usingMapSelector ?
[] : [Validators.required, Validators.minLength(3)]]
}),
notes: [this.loadedService.notes], notes: [this.loadedService.notes],
type: [this.loadedService.type, [Validators.required, Validators.minLength(1)]] type: [this.loadedService.type, [Validators.required, Validators.minLength(1)]]
}); });
@ -72,10 +90,12 @@ export class EditServiceComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private api: ApiClientService, private api: ApiClientService,
public auth: AuthService,
private toastr: ToastrService, private toastr: ToastrService,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private translate: TranslateService private translate: TranslateService
) { ) {
this.usingMapSelector = this.auth.profile.getOption("service_place_selection_use_map_picker", true);
this.route.paramMap.subscribe(params => { this.route.paramMap.subscribe(params => {
this.serviceId = params.get('id') || undefined; this.serviceId = params.get('id') || undefined;
if (this.serviceId === "new") { if (this.serviceId === "new") {
@ -182,15 +202,24 @@ export class EditServiceComponent implements OnInit {
return this.crew.value.find((x: number) => x == id); return this.crew.value.find((x: number) => x == id);
} }
setPlace(lat: number, lng: number) { setPlaceMap(lat: number, lng: number) {
this.lat.setValue(lat); this.lat.setValue(lat);
this.lon.setValue(lng); this.lon.setValue(lng);
console.log("Place selected", lat, lng); console.log("Place selected", lat, lng);
} }
setPlace(provinceCode: string, municipalityCode: string, address: string) {
this.serviceForm.get('place.provinceCode').setValue(provinceCode);
this.serviceForm.get('place.municipalityCode').setValue(municipalityCode);
this.address.setValue(address);
console.log("Place selected", provinceCode, municipalityCode, address);
}
//https://loiane.com/2017/08/angular-reactive-forms-trigger-validation-on-submit/ //https://loiane.com/2017/08/angular-reactive-forms-trigger-validation-on-submit/
isFieldValid(field: string) { isFieldValid(field: string) {
return this.formSubmitAttempt ? this.serviceForm.get(field).valid : true; if(!this.formSubmitAttempt) return true;
if(this.serviceForm.get(field) == null) return false;
return this.serviceForm.get(field).valid;
} }
formSubmit() { formSubmit() {

View File

@ -4,6 +4,7 @@ import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { MapPickerModule } from '../../_components/map-picker/map-picker.module'; import { MapPickerModule } from '../../_components/map-picker/map-picker.module';
import { PlacePickerModule } from 'src/app/_components/place-picker/place-picker.module';
import { DatetimePickerModule } from '../../_components/datetime-picker/datetime-picker.module'; import { DatetimePickerModule } from '../../_components/datetime-picker/datetime-picker.module';
import { BackBtnModule } from '../../_components/back-btn/back-btn.module'; import { BackBtnModule } from '../../_components/back-btn/back-btn.module';
import { TranslationModule } from '../../translation.module'; import { TranslationModule } from '../../translation.module';
@ -23,6 +24,7 @@ import { EditServiceComponent } from './edit-service.component';
ReactiveFormsModule, ReactiveFormsModule,
BsDatepickerModule.forRoot(), BsDatepickerModule.forRoot(),
MapPickerModule, MapPickerModule,
PlacePickerModule,
DatetimePickerModule, DatetimePickerModule,
BackBtnModule, BackBtnModule,
TranslationModule, TranslationModule,

View File

@ -3,10 +3,11 @@
<div class="spinner spinner-border"></div> <div class="spinner spinner-border"></div>
</div> </div>
<br> <br>
<div style="height: 300px;" leaflet [leafletOptions]="options" *ngIf="place_loaded"> <div style="height: 300px;" leaflet [leafletOptions]="options" *ngIf="place_loaded && place_info.lat && place_info.lon">
<div [leafletLayers]="layers"></div> <div [leafletLayers]="layers"></div>
</div> </div>
<div class="place_info" *ngIf="place_loaded">
<div class="place_info" *ngIf="place_loaded && place_info.lat && place_info.lon">
<h3> <h3>
<a href="https://www.google.com/maps/@?api=1&map_action=map&center={{ place_info.lat }},{{ place_info.lon }}&zoom=19&basemap=satellite" target="_blank">{{ 'place_details.open_in_google_maps'|translate }}</a> <a href="https://www.google.com/maps/@?api=1&map_action=map&center={{ place_info.lat }},{{ place_info.lon }}&zoom=19&basemap=satellite" target="_blank">{{ 'place_details.open_in_google_maps'|translate }}</a>
</h3> </h3>
@ -26,9 +27,6 @@
<ng-container *ngIf="place_info.suburb">- {{ place_info.suburb }}</ng-container> <ng-container *ngIf="place_info.suburb">- {{ place_info.suburb }}</ng-container>
</b> ({{ 'place_details.postcode'|translate }} <b>{{ place_info.postcode }}</b>) </b> ({{ 'place_details.postcode'|translate }} <b>{{ place_info.postcode }}</b>)
</h4> </h4>
<h4 *ngIf="place_info.municipality">
{{ 'place_details.municipality'|translate|ftitlecase }}: <b>{{ place_info.municipality }}</b>
</h4>
<h4 *ngIf="place_info.road"> <h4 *ngIf="place_info.road">
{{ 'place_details.road'|translate|ftitlecase }}: <b>{{ place_info.road }}</b> {{ 'place_details.road'|translate|ftitlecase }}: <b>{{ place_info.road }}</b>
</h4> </h4>
@ -36,3 +34,36 @@
{{ 'place_details.house_number'|translate|ftitlecase }}: <b>{{ place_info.house_number }}</b> {{ 'place_details.house_number'|translate|ftitlecase }}: <b>{{ place_info.house_number }}</b>
</h4> </h4>
</div> </div>
<div class="place_info" *ngIf="place_loaded && place_info.municipality">
<h3>
<a href="https://www.google.com/maps/search/?api=1&query={{ place_query }}&zoom=19&basemap=satellite" target="_blank">{{ 'place_details.open_in_google_maps'|translate }}</a>
</h3>
<br>
<h4>
{{ 'name'|translate|ftitlecase }}: <b>{{ place_info.name }} - {{ place_info.municipality.name }}</b><br>
{{ 'province'|translate|ftitlecase }}: <b>{{ place_info.municipality.province.name }} {{ place_info.municipality.province.short_name }}</b><br>
{{ 'region'|translate|ftitlecase }}: <b>{{ place_info.municipality.province.region }}</b>
</h4>
<br>
<h4>
{{ 'cadastral_code'|translate|ftitlecase }}: <b>{{ place_info.municipality.cadastral_code }}</b><br>
{{ 'zip_code'|translate|ftitlecase }}: <b>{{ place_info.municipality.postal_code }}</b><br>
{{ 'prefix'|translate|ftitlecase }}: <b>{{ place_info.municipality.prefix }}</b>
</h4>
<br>
<h4>
{{ 'email'|translate|ftitlecase }}: <a href="mailto:{{ place_info.municipality.email }}">
{{ place_info.municipality.email }}
</a><br>
{{ 'pec'|translate|ftitlecase }}: <a href="mailto:{{ place_info.municipality.pec }}">
{{ place_info.municipality.pec }}
</a><br>
{{ 'phone_number'|translate|ftitlecase }}: <a href="tel:{{ place_info.municipality.phone }}">
{{ place_info.municipality.phone }}
</a><br>
{{ 'fax'|translate|ftitlecase }}: <a href="tel:{{ place_info.municipality.fax }}">
{{ place_info.municipality.fax }}
</a>
</h4>
</div>

View File

@ -14,6 +14,7 @@ export class PlaceDetailsComponent implements OnInit {
id: number = 0; id: number = 0;
lat: number = 0; lat: number = 0;
lon: number = 0; lon: number = 0;
place_query: string = '';
place_info: any = {}; place_info: any = {};
place_loaded = false; place_loaded = false;
@ -37,32 +38,36 @@ export class PlaceDetailsComponent implements OnInit {
this.lon = parseFloat(place_info.lon || ''); this.lon = parseFloat(place_info.lon || '');
console.log(this.lat, this.lon); console.log(this.lat, this.lon);
this.options = { if(Number.isNaN(this.lat) || Number.isNaN(this.lon)) {
layers: [ this.place_query = encodeURIComponent(place_info.name + ", " + place_info.municipality.name + " " + place_info.municipality.province.short_name);
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }) } else {
], this.options = {
zoom: 17, layers: [
center: latLng(this.lat, this.lon) tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' })
}; ],
zoom: 17,
center: latLng(this.lat, this.lon)
};
const iconRetinaUrl = "./assets/icons/marker-icon-2x.png"; const iconRetinaUrl = "./assets/icons/marker-icon-2x.png";
const iconUrl = "./assets/icons/marker-icon.png"; const iconUrl = "./assets/icons/marker-icon.png";
const shadowUrl = "./assets/icons/marker-shadow.png"; const shadowUrl = "./assets/icons/marker-shadow.png";
const iconDefault = new Icon({ const iconDefault = new Icon({
iconRetinaUrl, iconRetinaUrl,
iconUrl, iconUrl,
shadowUrl, shadowUrl,
iconSize: [25, 41], iconSize: [25, 41],
iconAnchor: [12, 41], iconAnchor: [12, 41],
popupAnchor: [1, -34], popupAnchor: [1, -34],
tooltipAnchor: [16, -28], tooltipAnchor: [16, -28],
shadowSize: [41, 41] shadowSize: [41, 41]
}); });
this.layers = [ this.layers = [
marker([this.lat, this.lon], { marker([this.lat, this.lon], {
icon: iconDefault icon: iconDefault
}) })
]; ];
}
this.place_loaded = true; this.place_loaded = true;
}).catch((err) => { }).catch((err) => {

View File

@ -21,6 +21,3 @@
<h3 class="mt-5 text-center">Interventi per paese</h3> <h3 class="mt-5 text-center">Interventi per paese</h3>
<chart type="pie" [data]="chartServicesByVillageData"></chart> <chart type="pie" [data]="chartServicesByVillageData"></chart>
<h3 class="mt-5 text-center">Interventi per area di competenza</h3>
<chart type="pie" [data]="chartServicesByMunicipalityData"></chart>

View File

@ -21,7 +21,6 @@ export class StatsServicesComponent implements OnInit {
chartServicesByDriverData: any; chartServicesByDriverData: any;
chartServicesByTypeData: any; chartServicesByTypeData: any;
chartServicesByVillageData: any; chartServicesByVillageData: any;
chartServicesByMunicipalityData: any;
@ViewChild("servicesMap") servicesMap!: MapComponent; @ViewChild("servicesMap") servicesMap!: MapComponent;
@ -109,14 +108,6 @@ export class StatsServicesComponent implements OnInit {
villages[service.place.village] = 0; villages[service.place.village] = 0;
} }
villages[service.place.village]++; villages[service.place.village]++;
if (service.place.municipality === null) service.place.municipality = this.translate.instant("unknown");
// Capitalize first letter
service.place.municipality = service.place.municipality.charAt(0).toUpperCase() + service.place.municipality.slice(1);
if (!municipalities[service.place.municipality]) {
municipalities[service.place.municipality] = 0;
}
municipalities[service.place.municipality]++;
} }
console.log(people, chiefs, drivers, types, villages, municipalities); console.log(people, chiefs, drivers, types, villages, municipalities);
@ -190,20 +181,6 @@ export class StatsServicesComponent implements OnInit {
} }
] ]
}; };
let municipalitiesLabels: string[] = [], municipalitiesValues: number[] = [];
Object.entries(municipalities).sort(([,a],[,b]) => b-a).forEach(([key, value]) => {
municipalitiesLabels.push(key);
municipalitiesValues.push(value);
});
this.chartServicesByMunicipalityData = {
labels: municipalitiesLabels,
datasets: [
{
data: municipalitiesValues,
}
]
};
} }
loadServices() { loadServices() {

View File

@ -14,175 +14,190 @@ export interface LoginResponse {
providedIn: 'root' providedIn: 'root'
}) })
export class AuthService { export class AuthService {
private defaultPlaceholderProfile: any = { private defaultPlaceholderProfile: any = {
id: undefined, id: undefined,
impersonating: false, impersonating: false,
can: (permission: string) => false options: {},
}; can: (permission: string) => false,
public profile: any = this.defaultPlaceholderProfile; getOption: (option: string, defaultValue: string) => defaultValue,
public authChanged = new Subject<void>(); };
public _authLoaded = false; public profile: any = this.defaultPlaceholderProfile;
public authChanged = new Subject<void>();
public _authLoaded = false;
public loadProfile() { public loadProfile() {
console.log("Loading profile data..."); console.log("Loading profile data...");
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
this.api.post("me").then((data: any) => { this.api.post("me").then((data: any) => {
this.profile = data; this.profile = data;
this.profile.options = this.profile.options.reduce((acc: any, val: any) => {
acc[val.name] = val.value;
return acc;
}, {});
this.profile.can = (permission: string) => { this.profile.can = (permission: string) => {
return this.profile.permissions.includes(permission); return this.profile.permissions.includes(permission);
}
this.profile.profilePageLink = "/users/" + this.profile.id;
Sentry.setUser({
id: this.profile.id,
name: this.profile.name
});
resolve();
}).catch((e) => {
console.error(e);
this.profile = this.defaultPlaceholderProfile;
reject();
}).finally(() => {
this.authChanged.next();
});
});
}
authLoaded() {
return this._authLoaded;
}
constructor(
private api: ApiClientService,
private authToken: AuthTokenService,
private router: Router
) {
this.loadProfile().then(() => {
console.log("User is authenticated");
}).catch(() => {
console.log("User is not logged in");
}).finally(() => {
this._authLoaded = true;
});
}
public isAuthenticated() {
return this.profile.id !== undefined;
}
public login(username: string, password: string) {
return new Promise<LoginResponse>((resolve) => {
this.api.get("csrf-cookie").then((data: any) => {
this.api.post("login", {
username: username,
password: password,
// use_sessions: true //Disabled because on cheap hosting it can cause problems
}).then((data: any) => {
this.authToken.updateToken(data.access_token);
this.loadProfile().then(() => {
resolve({
loginOk: true,
message: data.message
});
}).catch(() => {
resolve({
loginOk: false,
message: "Unknown error"
});
});
}).catch((err) => {
let error_message = "";
if(err.status === 401 || err.status === 422) {
error_message = err.error.message;
} else if (err.status === 400) {
let error_messages = err.error.errors;
error_message = error_messages.map((val: any) => {
return `${val.msg} in ${val.param}`;
}).join(" & ");
} else if (err.status === 500) {
error_message = "Server error";
} else {
error_message = "Unknown error";
}
resolve({
loginOk: false,
message: error_message
});
});
}).catch((err) => {
if(err.status = 500) {
resolve({
loginOk: false,
message: "Server error"
});
} else {
resolve({
loginOk: false,
message: "Unknown error"
});
}
});
})
}
public impersonate(user_id: number): Promise<void|string> {
return new Promise((resolve, reject) => {
this.api.post(`impersonate/${user_id}`).then((data) => {
this.authToken.updateToken(data.access_token);
this.loadProfile().then(() => {
resolve();
}).catch((err) => {
console.error(err);
this.logout();
this.profile.impersonating_user = false;
this.logout();
});
}).catch((err) => {
console.error(err);
reject(err.error.message);
});
});
}
public stop_impersonating(): Promise<void> {
return new Promise((resolve, reject) => {
this.api.post("stop_impersonating").then((data) => {
this.authToken.updateToken(data.access_token);
this.api.post("refresh_token").then((data) => {
this.authToken.updateToken(data.access_token);
Sentry.setUser(null);
resolve();
}).catch((err) => {
this.logout(undefined, true);
reject();
});
}).catch((err) => {
this.logout(undefined, true);
reject();
});
});
}
public logout(routerDestination?: string[] | undefined, forceLogout: boolean = false) {
if(!forceLogout && this.profile.impersonating_user) {
this.stop_impersonating().then(() => {
this.loadProfile();
}).catch((err) => {
console.error(err);
});
} else {
this.api.post("logout").then((data: any) => {
this.profile = this.defaultPlaceholderProfile;
if(routerDestination === undefined) {
routerDestination = ["login", "list"];
}
this.authToken.clearToken();
Sentry.setUser(null);
this.router.navigate(routerDestination);
});
} }
this.profile.getOption = (option: string, defaultValue: any) => {
let value = this.profile.options[option];
if (value === undefined) {
return defaultValue;
} else {
return value;
}
}
this.profile.profilePageLink = "/users/" + this.profile.id;
Sentry.setUser({
id: this.profile.id,
name: this.profile.name
});
resolve();
}).catch((e) => {
console.error(e);
this.profile = this.defaultPlaceholderProfile;
reject();
}).finally(() => {
this.authChanged.next();
});
});
}
authLoaded() {
return this._authLoaded;
}
constructor(
private api: ApiClientService,
private authToken: AuthTokenService,
private router: Router
) {
this.loadProfile().then(() => {
console.log("User is authenticated");
}).catch(() => {
console.log("User is not logged in");
}).finally(() => {
this._authLoaded = true;
});
}
public isAuthenticated() {
return this.profile.id !== undefined;
}
public login(username: string, password: string) {
return new Promise<LoginResponse>((resolve) => {
this.api.get("csrf-cookie").then((data: any) => {
this.api.post("login", {
username: username,
password: password,
// use_sessions: true //Disabled because on cheap hosting it can cause problems
}).then((data: any) => {
this.authToken.updateToken(data.access_token);
this.loadProfile().then(() => {
resolve({
loginOk: true,
message: data.message
});
}).catch(() => {
resolve({
loginOk: false,
message: "Unknown error"
});
});
}).catch((err) => {
let error_message = "";
if (err.status === 401 || err.status === 422) {
error_message = err.error.message;
} else if (err.status === 400) {
let error_messages = err.error.errors;
error_message = error_messages.map((val: any) => {
return `${val.msg} in ${val.param}`;
}).join(" & ");
} else if (err.status === 500) {
error_message = "Server error";
} else {
error_message = "Unknown error";
}
resolve({
loginOk: false,
message: error_message
});
});
}).catch((err) => {
if (err.status = 500) {
resolve({
loginOk: false,
message: "Server error"
});
} else {
resolve({
loginOk: false,
message: "Unknown error"
});
}
});
})
}
public impersonate(user_id: number): Promise<void | string> {
return new Promise((resolve, reject) => {
this.api.post(`impersonate/${user_id}`).then((data) => {
this.authToken.updateToken(data.access_token);
this.loadProfile().then(() => {
resolve();
}).catch((err) => {
console.error(err);
this.logout();
this.profile.impersonating_user = false;
this.logout();
});
}).catch((err) => {
console.error(err);
reject(err.error.message);
});
});
}
public stop_impersonating(): Promise<void> {
return new Promise((resolve, reject) => {
this.api.post("stop_impersonating").then((data) => {
this.authToken.updateToken(data.access_token);
this.api.post("refresh_token").then((data) => {
this.authToken.updateToken(data.access_token);
Sentry.setUser(null);
resolve();
}).catch((err) => {
this.logout(undefined, true);
reject();
});
}).catch((err) => {
this.logout(undefined, true);
reject();
});
});
}
public logout(routerDestination?: string[] | undefined, forceLogout: boolean = false) {
if (!forceLogout && this.profile.impersonating_user) {
this.stop_impersonating().then(() => {
this.loadProfile();
}).catch((err) => {
console.error(err);
});
} else {
this.api.post("logout").then((data: any) => {
this.profile = this.defaultPlaceholderProfile;
if (routerDestination === undefined) {
routerDestination = ["login", "list"];
}
this.authToken.clearToken();
Sentry.setUser(null);
this.router.navigate(routerDestination);
});
} }
}
} }

View File

@ -1,333 +1,337 @@
{ {
"menu": { "menu": {
"list": "List", "list": "List",
"services": "Services", "services": "Services",
"trainings": "Trainings", "trainings": "Trainings",
"logs": "Logs", "logs": "Logs",
"stats": "Stats", "stats": "Stats",
"admin": "Admin", "admin": "Admin",
"logout": "Logout", "logout": "Logout",
"stop_impersonating": "Stop impersonating", "stop_impersonating": "Stop impersonating",
"hi": "hi" "hi": "hi"
}, },
"admin": { "admin": {
"info": "info", "info": "info",
"maintenance": "maintenance", "maintenance": "maintenance",
"roles": "roles", "roles": "roles",
"installed_migrations": "installed migrations", "installed_migrations": "installed migrations",
"open_connections": "open connections", "open_connections": "open connections",
"db_engine_name": "database engine name", "db_engine_name": "database engine name",
"database": "database", "database": "database",
"host": "host", "host": "host",
"port": "port", "port": "port",
"charset": "charset", "charset": "charset",
"prefix": "prefix", "prefix": "prefix",
"operations": "operations", "operations": "operations",
"run_migrations": "run migrations", "run_migrations": "run migrations",
"run_migrations_success": "Migrations executed successfully", "run_migrations_success": "Migrations executed successfully",
"run_seeding": "run seeding", "run_seeding": "run seeding",
"run_seeding_confirm_title": "Are you sure you want to run the seeding?", "run_seeding_confirm_title": "Are you sure you want to run the seeding?",
"run_seeding_confirm_text": "This action cannot be undone, and data can be loss in the DB.", "run_seeding_confirm_text": "This action cannot be undone, and data can be loss in the DB.",
"run_seeding_success": "Seeding executed successfully", "run_seeding_success": "Seeding executed successfully",
"show_tables": "show tables list", "show_tables": "show tables list",
"hide_tables": "hide tables list", "hide_tables": "hide tables list",
"table": "table", "table": "table",
"rows": "rows", "rows": "rows",
"updates_and_maintenance_title": "Updates and maintenance", "updates_and_maintenance_title": "Updates and maintenance",
"maintenance_mode_success": "Maintenance mode updated successfully", "maintenance_mode_success": "Maintenance mode updated successfully",
"optimization": "optimization", "optimization": "optimization",
"run_optimization": "run optimization", "run_optimization": "run optimization",
"run_optimization_success": "Optimization executed successfully", "run_optimization_success": "Optimization executed successfully",
"clear_optimization": "clear optimization", "clear_optimization": "clear optimization",
"clear_optimization_success": "Optimization cleared successfully", "clear_optimization_success": "Optimization cleared successfully",
"clear_cache": "clear cache", "clear_cache": "clear cache",
"clear_cache_success": "Cache cleared successfully", "clear_cache_success": "Cache cleared successfully",
"telegram_bot": "Telegram Bot", "telegram_bot": "Telegram Bot",
"telegram_webhook": "Telegram Webhook", "telegram_webhook": "Telegram Webhook",
"telegram_webhook_set": "Set Telegram Webhook", "telegram_webhook_set": "Set Telegram Webhook",
"telegram_webhook_set_success": "Telegram Webhook set successfully", "telegram_webhook_set_success": "Telegram Webhook set successfully",
"telegram_webhook_unset": "Unset Telegram Webhook", "telegram_webhook_unset": "Unset Telegram Webhook",
"telegram_webhook_unset_success": "Telegram Webhook unset successfully", "telegram_webhook_unset_success": "Telegram Webhook unset successfully",
"manual_execution": "manual execution", "manual_execution": "manual execution",
"run": "run", "run": "run",
"run_confirm_title": "Are you sure you want to run this command?", "run_confirm_title": "Are you sure you want to run this command?",
"run_confirm_text": "This action cannot be undone.", "run_confirm_text": "This action cannot be undone.",
"run_success": "Command executed successfully", "run_success": "Command executed successfully",
"env_operations": "environment variables operations", "env_operations": "environment variables operations",
"env_encrypt": "encrypt .env", "env_encrypt": "encrypt .env",
"env_encrypt_title": "Encrypt .env file", "env_encrypt_title": "Encrypt .env file",
"env_encrypt_confirm": "Insert the password to encrypt the .env file", "env_encrypt_confirm": "Insert the password to encrypt the .env file",
"env_encrypt_success": ".env encrypted successfully", "env_encrypt_success": ".env encrypted successfully",
"env_decrypt": "decrypt .env", "env_decrypt": "decrypt .env",
"env_decrypt_title": "Decrypt .env file", "env_decrypt_title": "Decrypt .env file",
"env_decrypt_confirm": "Insert the password to decrypt the .env file", "env_decrypt_confirm": "Insert the password to decrypt the .env file",
"env_decrypt_success": ".env decrypted successfully", "env_decrypt_success": ".env decrypted successfully",
"env_delete": "delete .env", "env_delete": "delete .env",
"env_delete_title": "Delete .env file", "env_delete_title": "Delete .env file",
"env_delete_confirm": "Are you sure you want to delete the .env file?", "env_delete_confirm": "Are you sure you want to delete the .env file?",
"env_delete_success": ".env deleted successfully", "env_delete_success": ".env deleted successfully",
"options": "Options", "options": "Options",
"option_update_success": "Option updated successfully" "option_update_success": "Option updated successfully"
}, },
"table": { "table": {
"remove_service_confirm": "Are you sure you want to remove this service?", "remove_service_confirm": "Are you sure you want to remove this service?",
"remove_service_confirm_text": "This action cannot be undone.", "remove_service_confirm_text": "This action cannot be undone.",
"service_deleted_successfully": "Service deleted successfully", "service_deleted_successfully": "Service deleted successfully",
"service_deleted_error": "Service could not be deleted. Please try again." "service_deleted_error": "Service could not be deleted. Please try again."
}, },
"list": { "list": {
"your_availability_is": "You are:", "your_availability_is": "You are:",
"enable_schedules": "Enable hour schedules", "enable_schedules": "Enable hour schedules",
"disable_schedules": "Disable hour schedules", "disable_schedules": "Disable hour schedules",
"update_schedules": "Update availability schedules", "update_schedules": "Update availability schedules",
"connect_telegram_bot": "Connect your account to the Telegram bot", "connect_telegram_bot": "Connect your account to the Telegram bot",
"tooltip_change_availability": "Change your availability to {{state}}", "tooltip_change_availability": "Change your availability to {{state}}",
"manual_mode_updated_successfully": "Manual mode updated successfully", "manual_mode_updated_successfully": "Manual mode updated successfully",
"schedule_load_failed": "Schedule could not be loaded. Please try again", "schedule_load_failed": "Schedule could not be loaded. Please try again",
"schedule_update_failed": "Schedule could not be updated. Please try again", "schedule_update_failed": "Schedule could not be updated. Please try again",
"availability_minutes_updated_at_deactivation": "Availability minutes are updated when the availability is removed, to allow a more precise calculation.", "availability_minutes_updated_at_deactivation": "Availability minutes are updated when the availability is removed, to allow a more precise calculation.",
"availability_change_failed": "Availability could not be changed. Please try again", "availability_change_failed": "Availability could not be changed. Please try again",
"manual_mode_update_failed": "Manual mode could not be updated. Please try again", "manual_mode_update_failed": "Manual mode could not be updated. Please try again",
"telegram_bot_token_request_failed": "Telegram bot token could not be generated. Please try again" "telegram_bot_token_request_failed": "Telegram bot token could not be generated. Please try again"
}, },
"alert": { "alert": {
"warning_body": "Alert in progress.", "warning_body": "Alert in progress.",
"current_alert": "Current alert", "current_alert": "Current alert",
"current_alerts": "Current alerts", "current_alerts": "Current alerts",
"state": "Alert state", "state": "Alert state",
"closed": "Alert closed", "closed": "Alert closed",
"request_response_question": "Do you respond to the alert?", "request_response_question": "Do you respond to the alert?",
"response_status": "Response status", "response_status": "Response status",
"no_response": "No response", "no_response": "No response",
"waiting_for_response": "Waiting for response", "waiting_for_response": "Waiting for response",
"response_yes": "Available", "response_yes": "Available",
"response_no": "Not available", "response_no": "Not available",
"details": "Alert details", "details": "Alert details",
"delete": "Remove current alert", "delete": "Remove current alert",
"delete_confirm_title": "Are you sure you want to remove this alert?", "delete_confirm_title": "Are you sure you want to remove this alert?",
"delete_confirm_text": "This action cannot be undone.", "delete_confirm_text": "This action cannot be undone.",
"deleted_successfully": "Alert removed successfully", "deleted_successfully": "Alert removed successfully",
"delete_failed": "Alert could not be removed. Please try again", "delete_failed": "Alert could not be removed. Please try again",
"settings_updated_successfully": "Settings updated successfully", "settings_updated_successfully": "Settings updated successfully",
"response_updated_successfully": "Response updated successfully" "response_updated_successfully": "Response updated successfully"
}, },
"login": { "login": {
"submit_btn": "Login" "submit_btn": "Login"
}, },
"place_details": { "place_details": {
"open_in_google_maps": "Open in Google Maps", "open_in_google_maps": "Open in Google Maps",
"place_name": "Place name", "place_name": "Place name",
"house_number": "house number", "house_number": "house number",
"road": "road", "road": "road",
"village": "village", "village": "village",
"postcode": "postcode", "postcode": "postcode",
"hamlet": "hamlet", "hamlet": "hamlet",
"municipality": "municipality", "municipality": "municipality",
"country": "country", "country": "country",
"place_load_failed": "Place could not be loaded. Please try again" "place_load_failed": "Place could not be loaded. Please try again"
}, },
"map_picker": { "map_picker": {
"loading_error": "Error loading search results. Please try again later" "loading_error": "Error loading search results. Please try again later"
}, },
"edit_service": { "edit_service": {
"select_start_datetime": "Select start date and time for the service", "select_start_datetime": "Select start date and time for the service",
"select_end_datetime": "Select end date and time for the service", "select_end_datetime": "Select end date and time for the service",
"insert_code": "Insert service code", "insert_code": "Insert service code",
"other_crew_members": "Other crew members", "other_crew_members": "Other crew members",
"select_service_type": "Select a service type", "select_service_type": "Select a service type",
"type_added_successfully": "Type added successfully", "type_added_successfully": "Type added successfully",
"service_added_successfully": "Service added successfully", "service_added_successfully": "Service added successfully",
"service_add_failed": "Service could not be added. Please try again", "service_add_failed": "Service could not be added. Please try again",
"service_updated_successfully": "Service updated successfully", "service_updated_successfully": "Service updated successfully",
"service_update_failed": "Service could not be updated. Please try again", "service_update_failed": "Service could not be updated. Please try again",
"service_load_failed": "Service could not be loaded. Please try again", "service_load_failed": "Service could not be loaded. Please try again",
"users_load_failed": "Users could not be loaded. Please try again", "users_load_failed": "Users could not be loaded. Please try again",
"types_load_failed": "Types could not be loaded. Please try again", "types_load_failed": "Types could not be loaded. Please try again",
"type_add_failed": "Type could not be added. Please try again" "type_add_failed": "Type could not be added. Please try again"
}, },
"edit_training": { "edit_training": {
"select_start_datetime": "Select start date and time for the training", "select_start_datetime": "Select start date and time for the training",
"select_end_datetime": "Select end date and time for the training", "select_end_datetime": "Select end date and time for the training",
"insert_name": "Insert training name", "insert_name": "Insert training name",
"name_placeholder": "Training name", "name_placeholder": "Training name",
"other_crew_members": "Other crew members", "other_crew_members": "Other crew members",
"training_added_successfully": "Training added successfully", "training_added_successfully": "Training added successfully",
"training_add_failed": "Training could not be added. Please try again", "training_add_failed": "Training could not be added. Please try again",
"training_updated_successfully": "Training updated successfully", "training_updated_successfully": "Training updated successfully",
"training_update_failed": "Training could not be updated. Please try again", "training_update_failed": "Training could not be updated. Please try again",
"training_load_failed": "Error loading training. Please try again", "training_load_failed": "Error loading training. Please try again",
"users_load_failed": "Error loading users. Please try again" "users_load_failed": "Error loading users. Please try again"
}, },
"edit_user": { "edit_user": {
"success_text": "User updated successfully", "success_text": "User updated successfully",
"error_text": "User could not be updated. Please try again", "error_text": "User could not be updated. Please try again",
"creation_date": "User creation date", "creation_date": "User creation date",
"last_update": "User last update", "last_update": "User last update",
"last_access": "User last access" "last_access": "User last access"
}, },
"user_info_modal": { "user_info_modal": {
"title": "User info" "title": "User info"
}, },
"training_course_modal": { "training_course_modal": {
"title": "Add training course", "title": "Add training course",
"doc_number": "document number" "doc_number": "document number"
}, },
"medical_examination_modal": { "medical_examination_modal": {
"title": "Add medical examination" "title": "Add medical examination"
}, },
"useragent_info_modal": { "useragent_info_modal": {
"title": "Client information" "title": "Client information"
}, },
"validation": { "validation": {
"place_min_length": "Place name must be at least 3 characters long", "place_min_length": "Place name must be at least 3 characters long",
"type_must_be_two_characters_long": "Type must be at least 2 characters long", "type_must_be_two_characters_long": "Type must be at least 2 characters long",
"type_already_exists": "Type already exists", "type_already_exists": "Type already exists",
"image_format_not_supported": "Image format not supported", "image_format_not_supported": "Image format not supported",
"document_format_not_supported": "Document format not supported", "document_format_not_supported": "Document format not supported",
"file_too_big": "File too big", "file_too_big": "File too big",
"password_min_length": "Password must be at least 6 characters long" "password_min_length": "Password must be at least 6 characters long"
}, },
"options": { "options": {
"service_place_selection_manual": "Manual place selection for services", "service_place_selection_use_map_picker": "Use map to select service place",
"no_selection_available": "No selection available" "no_selection_available": "No selection available"
}, },
"update_available": "Update available", "update_available": "Update available",
"update_available_text": "A new version of the application is available. Do you want to update now?", "update_available_text": "A new version of the application is available. Do you want to update now?",
"update_now": "Update now", "update_now": "Update now",
"yes_remove": "Yes, remove", "yes_remove": "Yes, remove",
"confirm": "Confirm", "confirm": "Confirm",
"cancel": "Cancel", "cancel": "Cancel",
"enable": "enable", "enable": "enable",
"disable": "disable", "disable": "disable",
"maintenance_mode": "maintenance mode", "maintenance_mode": "maintenance mode",
"maintenance_mode_warning": "The application is currently in maintenance mode. Some features may not be available.", "maintenance_mode_warning": "The application is currently in maintenance mode. Some features may not be available.",
"offline": "offline", "offline": "offline",
"offline_warning": "You're offline. Some features may not be available.", "offline_warning": "You're offline. Some features may not be available.",
"property": "property", "property": "property",
"value": "value", "value": "value",
"user_agent": "User Agent", "user_agent": "User Agent",
"browser": "browser", "browser": "browser",
"engine": "engine", "engine": "engine",
"os": "Operating System", "os": "Operating System",
"device": "device", "device": "device",
"cpu": "CPU", "cpu": "CPU",
"username": "username", "username": "username",
"password": "password", "password": "password",
"new_password": "new password", "new_password": "new password",
"confirm_password": "confirm password", "confirm_password": "confirm password",
"password_not_match": "Passwords do not match. Please try again.", "password_not_match": "Passwords do not match. Please try again.",
"password_changed_successfully": "Password changed successfully", "password_changed_successfully": "Password changed successfully",
"password_change_title": "Change password", "password_change_title": "Change password",
"change_password": "Change password", "change_password": "Change password",
"warning": "warning", "warning": "warning",
"press_for_more_info": "press here for more info", "press_for_more_info": "press here for more info",
"update_availability_schedule": "Update availability schedule", "update_availability_schedule": "Update availability schedule",
"select_type": "Select a type", "select_type": "Select a type",
"save_changes": "Save changes", "save_changes": "Save changes",
"close": "Close", "close": "Close",
"monday": "Monday", "monday": "Monday",
"monday_short": "Mon", "monday_short": "Mon",
"tuesday": "Tuesday", "tuesday": "Tuesday",
"tuesday_short": "Tue", "tuesday_short": "Tue",
"wednesday": "Wednesday", "wednesday": "Wednesday",
"wednesday_short": "Wed", "wednesday_short": "Wed",
"thursday": "Thursday", "thursday": "Thursday",
"thursday_short": "Thu", "thursday_short": "Thu",
"friday": "Friday", "friday": "Friday",
"friday_short": "Fri", "friday_short": "Fri",
"saturday": "Saturday", "saturday": "Saturday",
"saturday_short": "Sat", "saturday_short": "Sat",
"sunday": "Sunday", "sunday": "Sunday",
"sunday_short": "Sun", "sunday_short": "Sun",
"programmed": "programmed", "programmed": "programmed",
"available": "available", "available": "available",
"unavailable": "unavailable", "unavailable": "unavailable",
"set_available": "available", "set_available": "available",
"set_unavailable": "unavailable", "set_unavailable": "unavailable",
"name": "name", "name": "name",
"surname": "surname", "surname": "surname",
"ssn": "Social Security Number", "ssn": "Social Security Number",
"address": "address", "address": "address",
"zip_code": "zip code", "zip_code": "zip code",
"phone_number": "phone number", "cadastral_code": "cadastral code",
"email": "email", "phone_number": "phone number",
"birthday": "birthday", "fax": "fax",
"birthplace": "birthplace", "email": "email",
"personal_information": "personal information", "pec": "PEC",
"contact_information": "contact information", "birthday": "birthday",
"service_information": "service information", "birthplace": "birthplace",
"device_information": "device information", "personal_information": "personal information",
"course_date": "course date", "contact_information": "contact information",
"documents": "documents", "service_information": "service information",
"driving_license": "driving license", "device_information": "device information",
"driving_license_expiration_date": "driving license expiration date", "course_date": "course date",
"driving_license_number": "driving license number", "documents": "documents",
"driving_license_type": "driving license type", "driving_license": "driving license",
"driving_license_scan": "driving license scan", "driving_license_expiration_date": "driving license expiration date",
"upload_scan": "upload scan", "driving_license_number": "driving license number",
"upload_medical_examination_certificate": "upload medical examination certificate", "driving_license_type": "driving license type",
"upload_training_course_doc": "upload training course document", "driving_license_scan": "driving license scan",
"clothings": "clothings", "upload_scan": "upload scan",
"size": "size", "upload_medical_examination_certificate": "upload medical examination certificate",
"suit_size": "suit size", "upload_training_course_doc": "upload training course document",
"boot_size": "boot size", "clothings": "clothings",
"medical_examinations": "medical examinations", "size": "size",
"training_courses": "training courses", "suit_size": "suit size",
"date": "date", "boot_size": "boot size",
"expiration_date": "expiration date", "medical_examinations": "medical examinations",
"certifier": "certifier", "training_courses": "training courses",
"certificate_short": "cert.", "date": "date",
"banned": "banned", "expiration_date": "expiration date",
"hidden": "hidden", "certifier": "certifier",
"driver": "driver", "certificate_short": "cert.",
"drivers": "drivers", "banned": "banned",
"call": "call", "hidden": "hidden",
"service": "service", "driver": "driver",
"services": "services", "drivers": "drivers",
"training": "training", "call": "call",
"trainings": "trainings", "service": "service",
"user": "user", "services": "services",
"users": "users", "training": "training",
"availability_minutes": "availability_minutes", "trainings": "trainings",
"action": "action", "user": "user",
"changed": "changed", "users": "users",
"editor": "editor", "availability_minutes": "availability_minutes",
"datetime": "datetime", "action": "action",
"start": "start", "changed": "changed",
"end": "end", "editor": "editor",
"code": "code", "datetime": "datetime",
"chief": "chief", "start": "start",
"crew": "crew", "end": "end",
"place": "place", "code": "code",
"province": "province", "chief": "chief",
"notes": "notes", "crew": "crew",
"type": "type", "place": "place",
"add": "add", "province": "province",
"update": "update", "region": "region",
"remove": "remove", "notes": "notes",
"more details": "more details", "type": "type",
"search": "search", "add": "add",
"submit": "invia", "update": "update",
"reset": "reset", "remove": "remove",
"go_back": "Go back", "more details": "more details",
"next": "next", "search": "search",
"previous": "previous", "submit": "invia",
"last": "last", "reset": "reset",
"first": "first", "go_back": "Go back",
"total_elements_with_filters": "Total elements (curr. selection)", "next": "next",
"press_to_select_a_date": "Press to select a date", "previous": "previous",
"footer_text": "Allerta-VVF, free software developed for volunteer firefighters brigades.", "last": "last",
"revision": "revision", "first": "first",
"unknown": "unknown", "total_elements_with_filters": "Total elements (curr. selection)",
"edit": "edit", "press_to_select_a_date": "Press to select a date",
"never": "never", "footer_text": "Allerta-VVF, free software developed for volunteer firefighters brigades.",
"optional": "optional", "revision": "revision",
"not_enough_permissions": "You don't have enough permissions to access this page.", "unknown": "unknown",
"open_services_stats": "To view the statistics, go to the \"Stats\" page.", "edit": "edit",
"error_title": "Error", "never": "never",
"success_title": "Success", "optional": "optional",
"select_date_range": "Select date range", "not_enough_permissions": "You don't have enough permissions to access this page.",
"remove_date_filters": "Remove date filters", "open_services_stats": "To view the statistics, go to the \"Stats\" page.",
"yes": "yes", "error_title": "Error",
"no": "no" "success_title": "Success",
"select_date_range": "Select date range",
"remove_date_filters": "Remove date filters",
"yes": "yes",
"no": "no"
} }

View File

@ -1,333 +1,337 @@
{ {
"menu": { "menu": {
"list": "Lista disponibilità", "list": "Lista disponibilità",
"services": "Interventi", "services": "Interventi",
"trainings": "Esercitazioni", "trainings": "Esercitazioni",
"logs": "Logs", "logs": "Logs",
"stats": "Statistiche", "stats": "Statistiche",
"admin": "Amministrazione", "admin": "Amministrazione",
"logout": "Logout", "logout": "Logout",
"stop_impersonating": "Torna al vero account", "stop_impersonating": "Torna al vero account",
"hi": "Ciao" "hi": "Ciao"
}, },
"admin": { "admin": {
"info": "Info", "info": "Info",
"maintenance": "Manutenzione", "maintenance": "Manutenzione",
"roles": "Ruoli", "roles": "Ruoli",
"installed_migrations": "migrazioni installate", "installed_migrations": "migrazioni installate",
"open_connections": "connessioni aperte", "open_connections": "connessioni aperte",
"db_engine_name": "nome del motore del database", "db_engine_name": "nome del motore del database",
"database": "database", "database": "database",
"host": "host", "host": "host",
"port": "porta", "port": "porta",
"charset": "charset", "charset": "charset",
"prefix": "prefisso", "prefix": "prefisso",
"operations": "operazioni", "operations": "operazioni",
"run_migrations": "esegui migrazioni", "run_migrations": "esegui migrazioni",
"run_migrations_success": "Migrazioni eseguite con successo", "run_migrations_success": "Migrazioni eseguite con successo",
"run_seeding": "esegui seeding", "run_seeding": "esegui seeding",
"run_seeding_confirm_title": "Sei sicuro di voler eseguire il seeding?", "run_seeding_confirm_title": "Sei sicuro di voler eseguire il seeding?",
"run_seeding_confirm_text": "Questa operazione potrebbe sovrascrivere i dati presenti nel database.", "run_seeding_confirm_text": "Questa operazione potrebbe sovrascrivere i dati presenti nel database.",
"run_seeding_success": "Seeding eseguito con successo", "run_seeding_success": "Seeding eseguito con successo",
"show_tables": "mostra lista tabelle", "show_tables": "mostra lista tabelle",
"hide_tables": "nascondi lista tabelle", "hide_tables": "nascondi lista tabelle",
"table": "tabella", "table": "tabella",
"rows": "righe", "rows": "righe",
"updates_and_maintenance_title": "Aggiornamenti e manutenzione", "updates_and_maintenance_title": "Aggiornamenti e manutenzione",
"maintenance_mode_success": "Modalità manutenzione aggiornata con successo", "maintenance_mode_success": "Modalità manutenzione aggiornata con successo",
"optimization": "ottimizzazione", "optimization": "ottimizzazione",
"run_optimization": "esegui ottimizzazione", "run_optimization": "esegui ottimizzazione",
"run_optimization_success": "Ottimizzazione eseguita con successo", "run_optimization_success": "Ottimizzazione eseguita con successo",
"clear_optimization": "rimuovi ottimizzazione", "clear_optimization": "rimuovi ottimizzazione",
"clear_optimization_success": "Ottimizzazione rimossa con successo", "clear_optimization_success": "Ottimizzazione rimossa con successo",
"clear_cache": "svuota cache", "clear_cache": "svuota cache",
"clear_cache_success": "Cache svuotata con successo", "clear_cache_success": "Cache svuotata con successo",
"telegram_bot": "Bot Telegram", "telegram_bot": "Bot Telegram",
"telegram_webhook": "Webhook Telegram", "telegram_webhook": "Webhook Telegram",
"telegram_webhook_set": "Imposta Webhook Telegram", "telegram_webhook_set": "Imposta Webhook Telegram",
"telegram_webhook_set_success": "Webhook Telegram impostato con successo", "telegram_webhook_set_success": "Webhook Telegram impostato con successo",
"telegram_webhook_unset": "Rimuovi Webhook Telegram", "telegram_webhook_unset": "Rimuovi Webhook Telegram",
"telegram_webhook_unset_success": "Webhook Telegram rimosso con successo", "telegram_webhook_unset_success": "Webhook Telegram rimosso con successo",
"manual_execution": "esecuzione manuale", "manual_execution": "esecuzione manuale",
"run": "esegui", "run": "esegui",
"run_confirm_title": "Sei sicuro di voler eseguire questo comando?", "run_confirm_title": "Sei sicuro di voler eseguire questo comando?",
"run_confirm_text": "Questa operazione non potrà essere annullata.", "run_confirm_text": "Questa operazione non potrà essere annullata.",
"run_success": "Comando eseguito con successo", "run_success": "Comando eseguito con successo",
"env_operations": "operazioni alle variabili d'ambiente", "env_operations": "operazioni alle variabili d'ambiente",
"env_encrypt": "cripta .env", "env_encrypt": "cripta .env",
"env_encrypt_title": "Cripta il file .env", "env_encrypt_title": "Cripta il file .env",
"env_encrypt_text": "Inserisci la password per criptare il file .env", "env_encrypt_text": "Inserisci la password per criptare il file .env",
"env_encrypt_success": ".env criptato con successo", "env_encrypt_success": ".env criptato con successo",
"env_decrypt": "decripta .env", "env_decrypt": "decripta .env",
"env_decrypt_title": "Decripta il file .env", "env_decrypt_title": "Decripta il file .env",
"env_decrypt_text": "Inserisci la password per decriptare il file .env", "env_decrypt_text": "Inserisci la password per decriptare il file .env",
"env_decrypt_success": ".env decriptato con successo", "env_decrypt_success": ".env decriptato con successo",
"env_delete": "rimuovi .env", "env_delete": "rimuovi .env",
"env_delete_title": "Rimuovi il file .env", "env_delete_title": "Rimuovi il file .env",
"env_delete_text": "Sei sicuro di voler rimuovere il file .env?", "env_delete_text": "Sei sicuro di voler rimuovere il file .env?",
"env_delete_success": ".env rimosso con successo", "env_delete_success": ".env rimosso con successo",
"options": "Opzioni", "options": "Opzioni",
"option_update_success": "Opzione aggiornata con successo" "option_update_success": "Opzione aggiornata con successo"
}, },
"table": { "table": {
"remove_service_confirm": "Sei sicuro di voler rimuovere questo intervento?", "remove_service_confirm": "Sei sicuro di voler rimuovere questo intervento?",
"remove_service_confirm_text": "Questa operazione non può essere annullata.", "remove_service_confirm_text": "Questa operazione non può essere annullata.",
"service_deleted_successfully": "Intervento rimosso con successo", "service_deleted_successfully": "Intervento rimosso con successo",
"service_deleted_error": "Errore durante la rimozione dell'intervento. Riprova più tardi" "service_deleted_error": "Errore durante la rimozione dell'intervento. Riprova più tardi"
}, },
"list": { "list": {
"your_availability_is": "Attualmente sei:", "your_availability_is": "Attualmente sei:",
"enable_schedules": "Abilita programmazione oraria", "enable_schedules": "Abilita programmazione oraria",
"disable_schedules": "Disattiva programmazione oraria", "disable_schedules": "Disattiva programmazione oraria",
"update_schedules": "Modifica orari disponibilità", "update_schedules": "Modifica orari disponibilità",
"connect_telegram_bot": "Collega l'account al bot Telegram", "connect_telegram_bot": "Collega l'account al bot Telegram",
"tooltip_change_availability": "Cambia la tua disponibilità in {{state}}", "tooltip_change_availability": "Cambia la tua disponibilità in {{state}}",
"manual_mode_updated_successfully": "Modalità manuale aggiornata con successo", "manual_mode_updated_successfully": "Modalità manuale aggiornata con successo",
"schedule_load_failed": "Errore durante il caricamento della programmazione. Riprova più tardi", "schedule_load_failed": "Errore durante il caricamento della programmazione. Riprova più tardi",
"schedule_update_failed": "Errore durante l'aggiornamento della programmazione. Riprova più tardi", "schedule_update_failed": "Errore durante l'aggiornamento della programmazione. Riprova più tardi",
"availability_minutes_updated_at_deactivation": "I minuti di disponibilità vengono aggiornati al momento della rimozione della disponibilità, per permettere un calcolo più preciso.", "availability_minutes_updated_at_deactivation": "I minuti di disponibilità vengono aggiornati al momento della rimozione della disponibilità, per permettere un calcolo più preciso.",
"availability_change_failed": "Errore durante il cambio di disponibilità. Riprova più tardi", "availability_change_failed": "Errore durante il cambio di disponibilità. Riprova più tardi",
"manual_mode_update_failed": "Errore durante l'aggiornamento della modalità manuale. Riprova più tardi", "manual_mode_update_failed": "Errore durante l'aggiornamento della modalità manuale. Riprova più tardi",
"telegram_bot_token_request_failed": "Errore durante la richiesta del token del bot Telegram. Riprova più tardi" "telegram_bot_token_request_failed": "Errore durante la richiesta del token del bot Telegram. Riprova più tardi"
}, },
"alert": { "alert": {
"warning_body": "Allertamento in corso.", "warning_body": "Allertamento in corso.",
"current_alert": "Emergenza in corso", "current_alert": "Emergenza in corso",
"current_alerts": "Emergenze in corso", "current_alerts": "Emergenze in corso",
"state": "Stato dell'allerta", "state": "Stato dell'allerta",
"closed": "Allerta rientrata", "closed": "Allerta rientrata",
"request_response_question": "Sarai presente alla chiamata?", "request_response_question": "Sarai presente alla chiamata?",
"response_status": "Stato della risposta", "response_status": "Stato della risposta",
"no_response": "Nessuna risposta", "no_response": "Nessuna risposta",
"waiting_for_response": "In attesa di risposta", "waiting_for_response": "In attesa di risposta",
"response_yes": "Presente", "response_yes": "Presente",
"response_no": "Non presente", "response_no": "Non presente",
"details": "Dettagli dell'allerta", "details": "Dettagli dell'allerta",
"delete": "Ritira allerta", "delete": "Ritira allerta",
"delete_confirm_title": "Sei sicuro di voler rimuovere questa allerta?", "delete_confirm_title": "Sei sicuro di voler rimuovere questa allerta?",
"delete_confirm_text": "I vigili saranno avvisati della rimozione.", "delete_confirm_text": "I vigili saranno avvisati della rimozione.",
"deleted_successfully": "Allerta rimossa con successo", "deleted_successfully": "Allerta rimossa con successo",
"delete_failed": "L'eliminazione dell'allerta è fallita. Riprova più tardi", "delete_failed": "L'eliminazione dell'allerta è fallita. Riprova più tardi",
"settings_updated_successfully": "Impostazioni aggiornate con successo", "settings_updated_successfully": "Impostazioni aggiornate con successo",
"response_updated_successfully": "Risposta aggiornata con successo" "response_updated_successfully": "Risposta aggiornata con successo"
}, },
"login": { "login": {
"submit_btn": "Login" "submit_btn": "Login"
}, },
"place_details": { "place_details": {
"open_in_google_maps": "Apri il luogo in Google Maps", "open_in_google_maps": "Apri il luogo in Google Maps",
"place_name": "Nome del luogo", "place_name": "Nome del luogo",
"house_number": "numero civico", "house_number": "numero civico",
"road": "strada", "road": "strada",
"village": "comune", "village": "comune",
"postcode": "CAP", "postcode": "CAP",
"hamlet": "frazione", "hamlet": "frazione",
"municipality": "raggruppamento del comune", "municipality": "comune",
"country": "nazione/zona", "country": "nazione/zona",
"place_load_failed": "Errore durante il caricamento del luogo. Riprova più tardi" "place_load_failed": "Errore durante il caricamento del luogo. Riprova più tardi"
}, },
"map_picker": { "map_picker": {
"loading_error": "Errore di caricamento dei risultati della ricerca. Riprovare più tardi" "loading_error": "Errore di caricamento dei risultati della ricerca. Riprovare più tardi"
}, },
"edit_service": { "edit_service": {
"select_start_datetime": "Seleziona data e ora di inizio dell'intervento", "select_start_datetime": "Seleziona data e ora di inizio dell'intervento",
"select_end_datetime": "Seleziona data e ora di fine dell'intervento", "select_end_datetime": "Seleziona data e ora di fine dell'intervento",
"insert_code": "Inserisci il progressivo dell'intervento", "insert_code": "Inserisci il progressivo dell'intervento",
"other_crew_members": "Altri membri della squadra", "other_crew_members": "Altri membri della squadra",
"select_service_type": "Seleziona una tipologia di intervento", "select_service_type": "Seleziona una tipologia di intervento",
"type_added_successfully": "Tipologia aggiunta con successo", "type_added_successfully": "Tipologia aggiunta con successo",
"service_added_successfully": "Intervento aggiunto con successo", "service_added_successfully": "Intervento aggiunto con successo",
"service_add_failed": "Errore durante l'aggiunta dell'intervento. Riprovare più tardi", "service_add_failed": "Errore durante l'aggiunta dell'intervento. Riprovare più tardi",
"service_updated_successfully": "Intervento aggiornato con successo", "service_updated_successfully": "Intervento aggiornato con successo",
"service_update_failed": "Errore durante l'aggiornamento dell'intervento. Riprovare più tardi", "service_update_failed": "Errore durante l'aggiornamento dell'intervento. Riprovare più tardi",
"service_load_failed": "Errore durante il caricamento dell'intervento. Riprovare più tardi", "service_load_failed": "Errore durante il caricamento dell'intervento. Riprovare più tardi",
"users_load_failed": "Errore durante il caricamento degli utenti. Riprovare più tardi", "users_load_failed": "Errore durante il caricamento degli utenti. Riprovare più tardi",
"types_load_failed": "Errore durante il caricamento delle tipologie di intervento. Riprovare più tardi", "types_load_failed": "Errore durante il caricamento delle tipologie di intervento. Riprovare più tardi",
"type_add_failed": "Errore durante l'aggiunta della tipologia. Riprovare più tardi" "type_add_failed": "Errore durante l'aggiunta della tipologia. Riprovare più tardi"
}, },
"edit_training": { "edit_training": {
"select_start_datetime": "Seleziona data e ora di inizio dell'esercitazione", "select_start_datetime": "Seleziona data e ora di inizio dell'esercitazione",
"select_end_datetime": "Seleziona data e ora di fine dell'esercitazione", "select_end_datetime": "Seleziona data e ora di fine dell'esercitazione",
"insert_name": "Inserisci il nome dell'esercitazione", "insert_name": "Inserisci il nome dell'esercitazione",
"name_placeholder": "Esercitazione di gennaio", "name_placeholder": "Esercitazione di gennaio",
"other_crew_members": "Altri membri della squadra", "other_crew_members": "Altri membri della squadra",
"training_added_successfully": "Esercitazione aggiunta con successo", "training_added_successfully": "Esercitazione aggiunta con successo",
"training_add_failed": "Errore durante l'aggiunta dell'esercitazione. Riprovare più tardi", "training_add_failed": "Errore durante l'aggiunta dell'esercitazione. Riprovare più tardi",
"training_updated_successfully": "Esercitazione aggiornata con successo", "training_updated_successfully": "Esercitazione aggiornata con successo",
"training_update_failed": "Errore durante l'aggiornamento dell'esercitazione. Riprovare più tardi", "training_update_failed": "Errore durante l'aggiornamento dell'esercitazione. Riprovare più tardi",
"training_load_failed": "Errore durante il caricamento dell'intervento. Riprovare più tardi", "training_load_failed": "Errore durante il caricamento dell'intervento. Riprovare più tardi",
"users_load_failed": "Errore durante il caricamento degli utenti. Riprovare più tardi" "users_load_failed": "Errore durante il caricamento degli utenti. Riprovare più tardi"
}, },
"edit_user": { "edit_user": {
"success_text": "Utente aggiornato con successo", "success_text": "Utente aggiornato con successo",
"error_text": "L'utente non può essere aggiornato. Riprova più tardi", "error_text": "L'utente non può essere aggiornato. Riprova più tardi",
"creation_date": "Data di creazione dell'utente", "creation_date": "Data di creazione dell'utente",
"last_update": "Data di ultima modifica dell'utente", "last_update": "Data di ultima modifica dell'utente",
"last_access": "Ultimo accesso dell'utente" "last_access": "Ultimo accesso dell'utente"
}, },
"user_info_modal": { "user_info_modal": {
"title": "Scheda utente" "title": "Scheda utente"
}, },
"training_course_modal": { "training_course_modal": {
"title": "Aggiungi corso di formazione", "title": "Aggiungi corso di formazione",
"doc_number": "numero Ordine del Giorno" "doc_number": "numero Ordine del Giorno"
}, },
"medical_examination_modal": { "medical_examination_modal": {
"title": "Aggiungi visita medica" "title": "Aggiungi visita medica"
}, },
"useragent_info_modal": { "useragent_info_modal": {
"title": "Informazioni sul client" "title": "Informazioni sul client"
}, },
"validation": { "validation": {
"place_min_length": "Il nome della località deve essere di almeno 3 caratteri", "place_min_length": "Il nome della località deve essere di almeno 3 caratteri",
"type_must_be_two_characters_long": "La tipologia deve essere di almeno 2 caratteri", "type_must_be_two_characters_long": "La tipologia deve essere di almeno 2 caratteri",
"type_already_exists": "La tipologia è già presente", "type_already_exists": "La tipologia è già presente",
"image_format_not_supported": "Formato immagine non supportato", "image_format_not_supported": "Formato immagine non supportato",
"document_format_not_supported": "Formato documento non supportato", "document_format_not_supported": "Formato documento non supportato",
"file_too_big": "File troppo grande", "file_too_big": "File troppo grande",
"password_min_length": "La password deve essere di almeno 6 caratteri" "password_min_length": "La password deve essere di almeno 6 caratteri"
}, },
"options": { "options": {
"service_place_selection_manual": "Seleziona manualmente il luogo dell'intervento", "service_place_selection_use_map_picker": "Utilizza una mappa per selezionare il luogo dell'intervento",
"no_selection_available": "Nessuna selezione disponibile" "no_selection_available": "Nessuna selezione disponibile"
}, },
"update_available": "Aggiornamento disponibile", "update_available": "Aggiornamento disponibile",
"update_available_text": "È disponibile un aggiornamento per Allerta. Vuoi aggiornare ora?", "update_available_text": "È disponibile un aggiornamento per Allerta. Vuoi aggiornare ora?",
"update_now": "Aggiorna ora", "update_now": "Aggiorna ora",
"yes_remove": "Si, rimuovi", "yes_remove": "Si, rimuovi",
"confirm": "Conferma", "confirm": "Conferma",
"cancel": "Annulla", "cancel": "Annulla",
"enable": "attiva", "enable": "attiva",
"disable": "disattiva", "disable": "disattiva",
"maintenance_mode": "modalità manutenzione", "maintenance_mode": "modalità manutenzione",
"maintenance_mode_warning": "Il gestionale è in manutenzione. Alcune funzionalità potrebbero non essere disponibili.", "maintenance_mode_warning": "Il gestionale è in manutenzione. Alcune funzionalità potrebbero non essere disponibili.",
"offline": "offline", "offline": "offline",
"offline_warning": "Sei offline. Non è possibile interagire con il gestionale.", "offline_warning": "Sei offline. Non è possibile interagire con il gestionale.",
"property": "proprietà", "property": "proprietà",
"value": "valore", "value": "valore",
"user_agent": "User Agent", "user_agent": "User Agent",
"browser": "browser", "browser": "browser",
"engine": "motore", "engine": "motore",
"os": "Sistema Operativo", "os": "Sistema Operativo",
"device": "dispositivo", "device": "dispositivo",
"cpu": "CPU", "cpu": "CPU",
"username": "username", "username": "username",
"password": "password", "password": "password",
"new_password": "nuova password", "new_password": "nuova password",
"confirm_password": "conferma password", "confirm_password": "conferma password",
"password_not_match": "Le password non corrispondono. Riprova.", "password_not_match": "Le password non corrispondono. Riprova.",
"password_changed_successfully": "Password cambiata con successo", "password_changed_successfully": "Password cambiata con successo",
"password_change_title": "Cambio password", "password_change_title": "Cambio password",
"change_password": "Cambia password", "change_password": "Cambia password",
"warning": "attenzione", "warning": "attenzione",
"press_for_more_info": "premi qui per informazioni", "press_for_more_info": "premi qui per informazioni",
"update_availability_schedule": "Aggiorna programmazione disponibilità", "update_availability_schedule": "Aggiorna programmazione disponibilità",
"select_type": "Seleziona una tipologia", "select_type": "Seleziona una tipologia",
"save_changes": "Salva modifiche", "save_changes": "Salva modifiche",
"close": "Chiudi", "close": "Chiudi",
"monday": "Lunedì", "monday": "Lunedì",
"monday_short": "Lun", "monday_short": "Lun",
"tuesday": "Martedì", "tuesday": "Martedì",
"tuesday_short": "Mar", "tuesday_short": "Mar",
"wednesday": "Mercoledì", "wednesday": "Mercoledì",
"wednesday_short": "Mer", "wednesday_short": "Mer",
"thursday": "Giovedì", "thursday": "Giovedì",
"thursday_short": "Gio", "thursday_short": "Gio",
"friday": "Venerdì", "friday": "Venerdì",
"friday_short": "Ven", "friday_short": "Ven",
"saturday": "Sabato", "saturday": "Sabato",
"saturday_short": "Sab", "saturday_short": "Sab",
"sunday": "Domenica", "sunday": "Domenica",
"sunday_short": "Dom", "sunday_short": "Dom",
"programmed": "programmata", "programmed": "programmata",
"available": "disponibile", "available": "disponibile",
"unavailable": "non disponibile", "unavailable": "non disponibile",
"set_available": "attiva", "set_available": "attiva",
"set_unavailable": "disattiva", "set_unavailable": "disattiva",
"name": "nome", "name": "nome",
"surname": "cognome", "surname": "cognome",
"ssn": "codice fiscale", "ssn": "codice fiscale",
"address": "indirizzo", "address": "indirizzo",
"zip_code": "CAP", "zip_code": "CAP",
"phone_number": "numero di telefono", "cadastral_code": "codice catastale",
"email": "email", "phone_number": "numero di telefono",
"birthday": "data di nascita", "fax": "fax",
"birthplace": "luogo di nascita", "email": "email",
"personal_information": "anagrafica personale", "pec": "PEC",
"contact_information": "recapiti", "birthday": "data di nascita",
"service_information": "informazioni di servizio", "birthplace": "luogo di nascita",
"device_information": "informazioni sul dispositivo", "personal_information": "anagrafica personale",
"course_date": "data corso", "contact_information": "recapiti",
"documents": "documenti", "service_information": "informazioni di servizio",
"driving_license": "patente", "device_information": "informazioni sul dispositivo",
"driving_license_expiration_date": "scadenza patente", "course_date": "data corso",
"driving_license_number": "numero patente", "documents": "documenti",
"driving_license_type": "tipologia patente", "driving_license": "patente",
"driving_license_scan": "scansione patente", "driving_license_expiration_date": "scadenza patente",
"upload_scan": "carica scansione", "driving_license_number": "numero patente",
"upload_medical_examination_certificate": "carica certificato visita medica", "driving_license_type": "tipologia patente",
"upload_training_course_doc": "carica Ordine del Giorno", "driving_license_scan": "scansione patente",
"clothings": "indumenti", "upload_scan": "carica scansione",
"size": "dimensione", "upload_medical_examination_certificate": "carica certificato visita medica",
"suit_size": "taglia tuta", "upload_training_course_doc": "carica Ordine del Giorno",
"boot_size": "taglia scarponi", "clothings": "indumenti",
"medical_examinations": "visite mediche", "size": "dimensione",
"training_courses": "corsi di formazione", "suit_size": "taglia tuta",
"date": "data", "boot_size": "taglia scarponi",
"expiration_date": "data di scadenza", "medical_examinations": "visite mediche",
"certifier": "ente/certificatore", "training_courses": "corsi di formazione",
"certificate_short": "cert.", "date": "data",
"banned": "bannato", "expiration_date": "data di scadenza",
"hidden": "nascosto", "certifier": "ente/certificatore",
"driver": "autista", "certificate_short": "cert.",
"drivers": "autisti", "banned": "bannato",
"call": "chiama", "hidden": "nascosto",
"service": "intervento", "driver": "autista",
"services": "interventi", "drivers": "autisti",
"training": "esercitazione", "call": "chiama",
"trainings": "esercitazioni", "service": "intervento",
"user": "utente", "services": "interventi",
"users": "utenti", "training": "esercitazione",
"availability_minutes": "minuti di disponibilità", "trainings": "esercitazioni",
"action": "azione", "user": "utente",
"changed": "interessato", "users": "utenti",
"editor": "fatto da", "availability_minutes": "minuti di disponibilità",
"datetime": "data e ora", "action": "azione",
"start": "inizio", "changed": "interessato",
"end": "fine", "editor": "fatto da",
"code": "codice", "datetime": "data e ora",
"chief": "caposquadra", "start": "inizio",
"crew": "squadra", "end": "fine",
"place": "luogo", "code": "codice",
"province": "provincia", "chief": "caposquadra",
"notes": "note", "crew": "squadra",
"type": "tipologia", "place": "luogo",
"add": "aggiungi", "province": "provincia",
"update": "modifica", "region": "regione",
"remove": "rimuovi", "notes": "note",
"more details": "altri dettagli", "type": "tipologia",
"search": "cerca", "add": "aggiungi",
"submit": "invia", "update": "modifica",
"reset": "reset", "remove": "rimuovi",
"go_back": "Torna indietro", "more details": "altri dettagli",
"next": "successiva", "search": "cerca",
"previous": "precedente", "submit": "invia",
"last": "ultima", "reset": "reset",
"first": "prima", "go_back": "Torna indietro",
"total_elements_with_filters": "# Elementi (selezione corrente)", "next": "successiva",
"press_to_select_a_date": "Premi per selezionare una data", "previous": "precedente",
"footer_text": "Allerta-VVF, software libero realizzato per i Vigili del Fuoco volontari.", "last": "ultima",
"revision": "revisione", "first": "prima",
"unknown": "sconosciuto", "total_elements_with_filters": "# Elementi (selezione corrente)",
"edit": "modifica", "press_to_select_a_date": "Premi per selezionare una data",
"never": "mai", "footer_text": "Allerta-VVF, software libero realizzato per i Vigili del Fuoco volontari.",
"optional": "opzionale", "revision": "revisione",
"not_enough_permissions": "Non hai i permessi necessari per accedere a questa pagina.", "unknown": "sconosciuto",
"open_services_stats": "Per visualizzare statistiche e mappa degli interventi, vai alla sezione \"Statistiche\".", "edit": "modifica",
"error_title": "Errore", "never": "mai",
"success_title": "Successo", "optional": "opzionale",
"select_date_range": "Seleziona un intervallo di date", "not_enough_permissions": "Non hai i permessi necessari per accedere a questa pagina.",
"remove_date_filters": "Rimuovi filtri", "open_services_stats": "Per visualizzare statistiche e mappa degli interventi, vai alla sezione \"Statistiche\".",
"yes": "si", "error_title": "Errore",
"no": "no" "success_title": "Successo",
"select_date_range": "Seleziona un intervallo di date",
"remove_date_filters": "Rimuovi filtri",
"yes": "si",
"no": "no"
} }