From ee310a3155a799493ffac0c0ba72f272f87ea67a Mon Sep 17 00:00:00 2001 From: Matteo Gheza Date: Fri, 23 Feb 2024 00:27:22 +0100 Subject: [PATCH] Add new service place selection procedure --- .../app/Http/Controllers/AuthController.php | 19 +- .../app/Http/Controllers/PlacesController.php | 4 +- .../Http/Controllers/ServiceController.php | 194 +++-- .../app/Http/Controllers/UserController.php | 16 +- backend/app/Models/Place.php | 12 +- backend/app/Utils/Helpers.php | 23 + ...3_011004_add_italian_places_indicators.php | 12 +- ...op_old_municipality_column_from_places.php | 34 + backend/database/seeders/OptionsSeeder.php | 2 +- backend/routes/api.php | 2 +- .../place-picker/place-picker.component.html | 22 + .../place-picker/place-picker.component.scss | 0 .../place-picker/place-picker.component.ts | 99 +++ .../place-picker/place-picker.module.ts | 24 + .../_components/table/table.component.html | 9 +- .../edit-service/edit-service.component.html | 13 +- .../edit-service/edit-service.component.ts | 53 +- .../edit-service/edit-service.module.ts | 2 + .../place-details.component.html | 43 +- .../place-details/place-details.component.ts | 55 +- .../stats-services.component.html | 3 - .../stats-services.component.ts | 23 - frontend/src/app/_services/auth.service.ts | 349 ++++----- frontend/src/assets/i18n/en.json | 668 +++++++++--------- frontend/src/assets/i18n/it.json | 668 +++++++++--------- 25 files changed, 1366 insertions(+), 983 deletions(-) create mode 100644 backend/app/Utils/Helpers.php create mode 100644 backend/database/migrations/2024_01_23_011936_drop_old_municipality_column_from_places.php create mode 100644 frontend/src/app/_components/place-picker/place-picker.component.html create mode 100644 frontend/src/app/_components/place-picker/place-picker.component.scss create mode 100644 frontend/src/app/_components/place-picker/place-picker.component.ts create mode 100644 frontend/src/app/_components/place-picker/place-picker.module.ts diff --git a/backend/app/Http/Controllers/AuthController.php b/backend/app/Http/Controllers/AuthController.php index ee348cb..bf63e0e 100644 --- a/backend/app/Http/Controllers/AuthController.php +++ b/backend/app/Http/Controllers/AuthController.php @@ -3,7 +3,7 @@ namespace App\Http\Controllers; use App\Models\User; -use Illuminate\Support\Facades\Auth; +use App\Models\Option; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; use Illuminate\Http\Request; @@ -94,13 +94,26 @@ class AuthController extends Controller public function me(Request $request) { $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 [ ...$request->user()->toArray(), "permissions" => array_map(function($p) { return $p["name"]; }, $request->user()->allPermissions()->toArray()), "impersonating_user" => $impersonateManager->isImpersonating(), - "impersonator_id" => $impersonateManager->getImpersonatorId() + "impersonator_id" => $impersonateManager->getImpersonatorId(), + "options" => $options ]; } @@ -148,7 +161,7 @@ class AuthController extends Controller 'token_type' => 'Bearer', ]); } - + public function stopImpersonating(Request $request) { $manager = app('impersonate'); diff --git a/backend/app/Http/Controllers/PlacesController.php b/backend/app/Http/Controllers/PlacesController.php index ef385b5..e2ce771 100644 --- a/backend/app/Http/Controllers/PlacesController.php +++ b/backend/app/Http/Controllers/PlacesController.php @@ -64,7 +64,9 @@ class PlacesController extends Controller User::where('id', $request->user()->id)->update(['last_access' => now()]); return response()->json( - Place::find($id) + Place::where('id', $id) + ->with('municipality', 'municipality.province') + ->firstOrFail() ); } } diff --git a/backend/app/Http/Controllers/ServiceController.php b/backend/app/Http/Controllers/ServiceController.php index cf6918a..b816237 100644 --- a/backend/app/Http/Controllers/ServiceController.php +++ b/backend/app/Http/Controllers/ServiceController.php @@ -3,13 +3,17 @@ namespace App\Http\Controllers; use App\Models\Place; +use App\Models\PlaceMunicipality; +use App\Models\PlaceProvince; use App\Models\Service; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use App\Utils\Logger; use App\Utils\DBTricks; +use App\Utils\Helpers; class ServiceController extends Controller { @@ -18,31 +22,48 @@ class ServiceController extends Controller */ 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()]); $query = Service::join('users', 'users.id', '=', 'chief_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('crew:name,surname') - ->with('place') + ->with('place.municipality.province') ->orderBy('start', 'desc'); - if($request->has('from')) { + if ($request->has('from')) { try { $from = Carbon::parse($request->input('from')); $query->whereDate('start', '>=', $from->toDateString()); - } catch (\Carbon\Exceptions\InvalidFormatException $e) { } + } catch (\Carbon\Exceptions\InvalidFormatException $e) { + } } - if($request->has('to')) { + if ($request->has('to')) { try { $to = Carbon::parse($request->input('to')); $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) { - 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()]); return response()->json( @@ -59,7 +80,7 @@ class ServiceController extends Controller ->select('services.*', DBTricks::nameSelect("chief", "users"), 'services_types.name as type') ->with('drivers:name,surname') ->with('crew:name,surname') - ->with('place') + ->with('place.municipality.province') ->find($id) ); } @@ -67,10 +88,10 @@ class ServiceController extends Controller private function extractServiceUsers($service) { $usersList = [$service->chief_id]; - foreach($service->drivers as $driver) { + foreach ($service->drivers as $driver) { $usersList[] = $driver->id; } - foreach($service->crew as $crew) { + foreach ($service->crew as $crew) { $usersList[] = $crew->id; } return array_unique($usersList); @@ -83,14 +104,14 @@ class ServiceController extends Controller { $adding = !isset($request->id) || is_null($request->id); - if(!$adding && !$request->user()->hasPermission("services-update")) abort(401); - if($adding && !$request->user()->hasPermission("services-create")) abort(401); + if (!$adding && !$request->user()->hasPermission("services-update")) 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); User::whereIn('id', $usersToDecrement)->decrement('services'); @@ -99,36 +120,111 @@ class ServiceController extends Controller $service->save(); } - //Find Place by lat lon - $place = Place::where('lat', $request->lat)->where('lon', $request->lon)->first(); - if(!$place) { + $is_map_picker = Helpers::get_option('service_place_selection_use_map_picker', false); + + 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->lat = $request->lat; - $place->lon = $request->lon; + $place->name = $request->place["address"]; - $response = Http::withUrlParameters([ - 'lat' => $request->lat, - 'lon' => $request->lon, - ])->get('https://nominatim.openstreetmap.org/reverse?format=json&lat={lat}&lon={lon}'); - if(!$response->ok()) abort(500); + //Check if municipality exists + $municipality = PlaceMunicipality::where('code', $request->place["municipalityCode"])->first(); + if (!$municipality) { + //Check if province exists + $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; - $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->municipality = isset($response["address"]["municipality"]) ? $response["address"]["municipality"] : null; + //Find province + foreach ($provinces as $p) { + if ($p->codice == $request->place["provinceCode"]) { + $province = new PlaceProvince(); + $province->code = $p->codice; + $province->name = $p->nome; + $province->short_name = $p->sigla; + $province->region = $p->regione; + $province->save(); + break; + } + } + if (!$province) { + abort(400); + } + } + + $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(); } @@ -137,8 +233,8 @@ class ServiceController extends Controller $service->chief()->associate($request->chief); $service->type()->associate($request->type); $service->notes = $request->notes; - $service->start = $request->start/1000; - $service->end = $request->end/1000; + $service->start = $request->start / 1000; + $service->end = $request->end / 1000; $service->place()->associate($place); $service->addedBy()->associate($request->user()); $service->updatedBy()->associate($request->user()); @@ -163,7 +259,7 @@ class ServiceController extends Controller */ 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); $usersToDecrement = $this->extractServiceUsers($service); User::whereIn('id', $usersToDecrement)->decrement('services'); diff --git a/backend/app/Http/Controllers/UserController.php b/backend/app/Http/Controllers/UserController.php index 5a4d55d..a4f614d 100644 --- a/backend/app/Http/Controllers/UserController.php +++ b/backend/app/Http/Controllers/UserController.php @@ -33,7 +33,7 @@ class UserController extends Controller ->orderBy('name', 'asc') ->orderBy('surname', 'asc') ->get(); - + $now = now(); foreach($list as $user) { //Add online status @@ -67,7 +67,7 @@ class UserController extends Controller ->join('document_files', 'document_files.id', '=', 'documents.document_file_id') ->select('documents.doc_type', 'documents.doc_number', 'documents.expiration_date', 'document_files.uuid as scan_uuid') ->get(); - + if($dl_tmp->count() > 0) { $user->driving_license = $dl_tmp[0]; } @@ -78,7 +78,7 @@ class UserController extends Controller ->leftJoin('training_course_types', 'training_course_types.id', '=', 'documents.doc_type') ->select('documents.doc_number as doc_number', 'documents.date', 'document_files.uuid as doc_uuid', 'training_course_types.name as type') ->get(); - + if($tc_tmp->count() > 0) { $user->training_courses = $tc_tmp; foreach($user->training_courses as $tc) { @@ -98,7 +98,7 @@ class UserController extends Controller ->leftJoin('document_files', 'document_files.id', '=', 'documents.document_file_id') ->select('documents.doc_certifier as certifier', 'documents.date', 'documents.expiration_date', 'document_files.uuid as cert_uuid') ->get(); - + if($me_tmp->count() > 0) { $user->medical_examinations = $me_tmp; foreach($user->medical_examinations as $me) { @@ -251,12 +251,4 @@ class UserController extends Controller return response()->json($user); } - - /** - * Remove the specified resource from storage. - */ - public function destroy(User $user) - { - // - } } diff --git a/backend/app/Models/Place.php b/backend/app/Models/Place.php index 02a85c5..8ce5c83 100644 --- a/backend/app/Models/Place.php +++ b/backend/app/Models/Place.php @@ -4,6 +4,8 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use App\Models\PlaceMunicipality; class Place extends Model { @@ -32,7 +34,13 @@ class Place extends Model 'state', 'village', 'suburb', - 'city', - 'municipality' + 'city' ]; + + /** + * Get the municipality + */ + public function municipality(): BelongsTo { + return $this->belongsTo(PlaceMunicipality::class); + } } diff --git a/backend/app/Utils/Helpers.php b/backend/app/Utils/Helpers.php new file mode 100644 index 0000000..963c47d --- /dev/null +++ b/backend/app/Utils/Helpers.php @@ -0,0 +1,23 @@ +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; + } + +} diff --git a/backend/database/migrations/2024_01_23_011004_add_italian_places_indicators.php b/backend/database/migrations/2024_01_23_011004_add_italian_places_indicators.php index 1bc46ef..bf76f73 100644 --- a/backend/database/migrations/2024_01_23_011004_add_italian_places_indicators.php +++ b/backend/database/migrations/2024_01_23_011004_add_italian_places_indicators.php @@ -11,14 +11,14 @@ return new class extends Migration */ public function up(): void { - Schema::create('PlaceProvince', function (Blueprint $table) { + Schema::create('place_provinces', function (Blueprint $table) { $table->id(); - $table->string('code', 2)->unique(); + $table->string('code', 20)->unique(); $table->string('name', 100); $table->string('short_name', 2); $table->string('region', 25); }); - Schema::create('PlaceMunicipality', function (Blueprint $table) { + Schema::create('place_municipalities', function (Blueprint $table) { $table->id(); $table->string('code', 6)->unique(); $table->string('name', 200); @@ -32,7 +32,7 @@ return new class extends Migration $table->string('fax', 30)->nullable(); $table->decimal('latitude', 10, 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 { - Schema::dropIfExists('PlaceMunicipality'); - Schema::dropIfExists('PlaceProvince'); + Schema::dropIfExists('place_municipalities'); + Schema::dropIfExists('place_provinces'); } }; diff --git a/backend/database/migrations/2024_01_23_011936_drop_old_municipality_column_from_places.php b/backend/database/migrations/2024_01_23_011936_drop_old_municipality_column_from_places.php new file mode 100644 index 0000000..7e410a5 --- /dev/null +++ b/backend/database/migrations/2024_01_23_011936_drop_old_municipality_column_from_places.php @@ -0,0 +1,34 @@ +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(); + }); + } +}; diff --git a/backend/database/seeders/OptionsSeeder.php b/backend/database/seeders/OptionsSeeder.php index 6bd177e..a2d8c95 100644 --- a/backend/database/seeders/OptionsSeeder.php +++ b/backend/database/seeders/OptionsSeeder.php @@ -14,7 +14,7 @@ class OptionsSeeder extends Seeder { $options = [ [ - 'name' => 'service_place_selection_manual', + 'name' => 'service_place_selection_use_map_picker', 'value' => true, 'type' => 'boolean' ] diff --git a/backend/routes/api.php b/backend/routes/api.php index 361216b..a132074 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -82,7 +82,7 @@ Route::middleware('auth:sanctum')->group( function () { Route::get('/places/italy/regions', [PlacesController::class, 'italyListRegions']); Route::get('/places/italy/provinces/{region_name}', [PlacesController::class, 'italyListProvincesByRegion']); Route::get('/places/italy/municipalities/{province_name}', [PlacesController::class, 'italyListMunicipalitiesByProvince']); - + Route::get('/places/{id}', [PlacesController::class, 'show']); Route::get('/trainings', [TrainingController::class, 'index'])->middleware(ETag::class); diff --git a/frontend/src/app/_components/place-picker/place-picker.component.html b/frontend/src/app/_components/place-picker/place-picker.component.html new file mode 100644 index 0000000..761a1d0 --- /dev/null +++ b/frontend/src/app/_components/place-picker/place-picker.component.html @@ -0,0 +1,22 @@ +
+
+ {{ 'region'|translate|titlecase }} + +
+
+ {{ 'province'|translate|titlecase }} + +
+
+ {{ 'place_details.municipality'|translate|titlecase }} + +
+
+ {{ 'address'|translate|titlecase }} + +
+
diff --git a/frontend/src/app/_components/place-picker/place-picker.component.scss b/frontend/src/app/_components/place-picker/place-picker.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/_components/place-picker/place-picker.component.ts b/frontend/src/app/_components/place-picker/place-picker.component.ts new file mode 100644 index 0000000..e464846 --- /dev/null +++ b/frontend/src/app/_components/place-picker/place-picker.component.ts @@ -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(); + + 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 + }); + } +} diff --git a/frontend/src/app/_components/place-picker/place-picker.module.ts b/frontend/src/app/_components/place-picker/place-picker.module.ts new file mode 100644 index 0000000..5689552 --- /dev/null +++ b/frontend/src/app/_components/place-picker/place-picker.module.ts @@ -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 { } diff --git a/frontend/src/app/_components/table/table.component.html b/frontend/src/app/_components/table/table.component.html index 606f2dd..8da0cfe 100644 --- a/frontend/src/app/_components/table/table.component.html +++ b/frontend/src/app/_components/table/table.component.html @@ -138,6 +138,7 @@ {{ row.place.name }}
{{ row.place.village }}
+ {{ row.place.municipality.name }} {{ row.place.municipality.province.short_name }}
{{ 'more details'|translate|ftitlecase }} {{ row.notes }} @@ -206,19 +207,19 @@ {{ page.number }} - + {{ 'next'|translate|ftitlecase }} - + {{ 'previous'|translate|ftitlecase }} - + {{ 'last'|translate|ftitlecase }} - + {{ 'first'|translate|ftitlecase }} diff --git a/frontend/src/app/_routes/edit-service/edit-service.component.html b/frontend/src/app/_routes/edit-service/edit-service.component.html index 19b2f85..8331f39 100644 --- a/frontend/src/app/_routes/edit-service/edit-service.component.html +++ b/frontend/src/app/_routes/edit-service/edit-service.component.html @@ -63,10 +63,15 @@ -
+
- - + + +
+
+ + +

@@ -101,4 +106,4 @@
- \ No newline at end of file + diff --git a/frontend/src/app/_routes/edit-service/edit-service.component.ts b/frontend/src/app/_routes/edit-service/edit-service.component.ts index 4a3bcbf..010d4d7 100644 --- a/frontend/src/app/_routes/edit-service/edit-service.component.ts +++ b/frontend/src/app/_routes/edit-service/edit-service.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { AbstractControl, UntypedFormBuilder, ValidationErrors, Validators } from '@angular/forms'; import { ApiClientService } from 'src/app/_services/api-client.service'; +import { AuthService } from 'src/app/_services/auth.service'; import { ToastrService } from 'ngx-toastr'; import { TranslateService } from '@ngx-translate/core'; @@ -22,11 +23,15 @@ export class EditServiceComponent implements OnInit { crew: [], lat: -1, lon: -1, + provinceCode: '', + municipalityCode: '', + address: '', notes: '', type: '' }; loadedServiceLat = ""; loadedServiceLng = ""; + usingMapSelector = true; users: any[] = []; types: any[] = []; @@ -44,8 +49,9 @@ export class EditServiceComponent implements OnInit { get chief() { return this.serviceForm.get('chief'); } get drivers() { return this.serviceForm.get('drivers'); } get crew() { return this.serviceForm.get('crew'); } - get lat() { return this.serviceForm.get('lat'); } - get lon() { return this.serviceForm.get('lon'); } + get lat() { return this.serviceForm.get('place.lat'); } + get lon() { return this.serviceForm.get('place.lon'); } + get address() { return this.serviceForm.get('place.address'); } get type() { return this.serviceForm.get('type'); } ngOnInit() { @@ -56,14 +62,26 @@ export class EditServiceComponent implements OnInit { chief: [this.loadedService.chief, [Validators.required]], drivers: [this.loadedService.drivers, []], crew: [this.loadedService.crew, [Validators.required]], - lat: [this.loadedService.lat, [Validators.required, (control: AbstractControl): ValidationErrors | null => { - const valid = control.value >= -90 && control.value <= 90; - return valid ? null : { 'invalidLatitude': { value: control.value } }; - }]], - lon: [this.loadedService.lon, [Validators.required, (control: AbstractControl): ValidationErrors | null => { - const valid = control.value >= -180 && control.value <= 180; - return valid ? null : { 'invalidLongitude': { value: control.value } }; - }]], + place: this.fb.group({ + lat: [this.loadedService.lat, this.usingMapSelector ? + [Validators.required, (control: AbstractControl): ValidationErrors | null => { + const valid = control.value >= -90 && control.value <= 90; + return valid ? null : { 'invalidLatitude': { 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], type: [this.loadedService.type, [Validators.required, Validators.minLength(1)]] }); @@ -72,10 +90,12 @@ export class EditServiceComponent implements OnInit { constructor( private route: ActivatedRoute, private api: ApiClientService, + public auth: AuthService, private toastr: ToastrService, private fb: UntypedFormBuilder, private translate: TranslateService ) { + this.usingMapSelector = this.auth.profile.getOption("service_place_selection_use_map_picker", true); this.route.paramMap.subscribe(params => { this.serviceId = params.get('id') || undefined; if (this.serviceId === "new") { @@ -182,15 +202,24 @@ export class EditServiceComponent implements OnInit { return this.crew.value.find((x: number) => x == id); } - setPlace(lat: number, lng: number) { + setPlaceMap(lat: number, lng: number) { this.lat.setValue(lat); this.lon.setValue(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/ 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() { diff --git a/frontend/src/app/_routes/edit-service/edit-service.module.ts b/frontend/src/app/_routes/edit-service/edit-service.module.ts index a9695a8..b0c0057 100644 --- a/frontend/src/app/_routes/edit-service/edit-service.module.ts +++ b/frontend/src/app/_routes/edit-service/edit-service.module.ts @@ -4,6 +4,7 @@ import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; 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 { BackBtnModule } from '../../_components/back-btn/back-btn.module'; import { TranslationModule } from '../../translation.module'; @@ -23,6 +24,7 @@ import { EditServiceComponent } from './edit-service.component'; ReactiveFormsModule, BsDatepickerModule.forRoot(), MapPickerModule, + PlacePickerModule, DatetimePickerModule, BackBtnModule, TranslationModule, diff --git a/frontend/src/app/_routes/place-details/place-details.component.html b/frontend/src/app/_routes/place-details/place-details.component.html index 899ed83..37b6ca5 100644 --- a/frontend/src/app/_routes/place-details/place-details.component.html +++ b/frontend/src/app/_routes/place-details/place-details.component.html @@ -3,10 +3,11 @@

-
+
-
+ +

{{ 'place_details.open_in_google_maps'|translate }}

@@ -26,13 +27,43 @@ - {{ place_info.suburb }} ({{ 'place_details.postcode'|translate }} {{ place_info.postcode }}) -

- {{ 'place_details.municipality'|translate|ftitlecase }}: {{ place_info.municipality }} -

{{ 'place_details.road'|translate|ftitlecase }}: {{ place_info.road }}

{{ 'place_details.house_number'|translate|ftitlecase }}: {{ place_info.house_number }}

-
\ No newline at end of file +
+ +
+

+ {{ 'place_details.open_in_google_maps'|translate }} +

+
+

+ {{ 'name'|translate|ftitlecase }}: {{ place_info.name }} - {{ place_info.municipality.name }}
+ {{ 'province'|translate|ftitlecase }}: {{ place_info.municipality.province.name }} {{ place_info.municipality.province.short_name }}
+ {{ 'region'|translate|ftitlecase }}: {{ place_info.municipality.province.region }} +

+
+

+ {{ 'cadastral_code'|translate|ftitlecase }}: {{ place_info.municipality.cadastral_code }}
+ {{ 'zip_code'|translate|ftitlecase }}: {{ place_info.municipality.postal_code }}
+ {{ 'prefix'|translate|ftitlecase }}: {{ place_info.municipality.prefix }} +

+
+

+ {{ 'email'|translate|ftitlecase }}: + {{ place_info.municipality.email }} +
+ {{ 'pec'|translate|ftitlecase }}: + {{ place_info.municipality.pec }} +
+ {{ 'phone_number'|translate|ftitlecase }}: + {{ place_info.municipality.phone }} +
+ {{ 'fax'|translate|ftitlecase }}: + {{ place_info.municipality.fax }} + +

+
diff --git a/frontend/src/app/_routes/place-details/place-details.component.ts b/frontend/src/app/_routes/place-details/place-details.component.ts index 0429723..0171fa9 100644 --- a/frontend/src/app/_routes/place-details/place-details.component.ts +++ b/frontend/src/app/_routes/place-details/place-details.component.ts @@ -14,6 +14,7 @@ export class PlaceDetailsComponent implements OnInit { id: number = 0; lat: number = 0; lon: number = 0; + place_query: string = ''; place_info: any = {}; place_loaded = false; @@ -37,32 +38,36 @@ export class PlaceDetailsComponent implements OnInit { this.lon = parseFloat(place_info.lon || ''); console.log(this.lat, this.lon); - this.options = { - layers: [ - tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors' }) - ], - zoom: 17, - center: latLng(this.lat, this.lon) - }; + if(Number.isNaN(this.lat) || Number.isNaN(this.lon)) { + this.place_query = encodeURIComponent(place_info.name + ", " + place_info.municipality.name + " " + place_info.municipality.province.short_name); + } else { + this.options = { + layers: [ + tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors' }) + ], + zoom: 17, + center: latLng(this.lat, this.lon) + }; - const iconRetinaUrl = "./assets/icons/marker-icon-2x.png"; - const iconUrl = "./assets/icons/marker-icon.png"; - const shadowUrl = "./assets/icons/marker-shadow.png"; - const iconDefault = new Icon({ - iconRetinaUrl, - iconUrl, - shadowUrl, - iconSize: [25, 41], - iconAnchor: [12, 41], - popupAnchor: [1, -34], - tooltipAnchor: [16, -28], - shadowSize: [41, 41] - }); - this.layers = [ - marker([this.lat, this.lon], { - icon: iconDefault - }) - ]; + const iconRetinaUrl = "./assets/icons/marker-icon-2x.png"; + const iconUrl = "./assets/icons/marker-icon.png"; + const shadowUrl = "./assets/icons/marker-shadow.png"; + const iconDefault = new Icon({ + iconRetinaUrl, + iconUrl, + shadowUrl, + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + tooltipAnchor: [16, -28], + shadowSize: [41, 41] + }); + this.layers = [ + marker([this.lat, this.lon], { + icon: iconDefault + }) + ]; + } this.place_loaded = true; }).catch((err) => { diff --git a/frontend/src/app/_routes/stats/stats-services/stats-services.component.html b/frontend/src/app/_routes/stats/stats-services/stats-services.component.html index 6fa3a09..f07d436 100644 --- a/frontend/src/app/_routes/stats/stats-services/stats-services.component.html +++ b/frontend/src/app/_routes/stats/stats-services/stats-services.component.html @@ -21,6 +21,3 @@

Interventi per paese

- -

Interventi per area di competenza

- diff --git a/frontend/src/app/_routes/stats/stats-services/stats-services.component.ts b/frontend/src/app/_routes/stats/stats-services/stats-services.component.ts index dcb2328..16ba318 100644 --- a/frontend/src/app/_routes/stats/stats-services/stats-services.component.ts +++ b/frontend/src/app/_routes/stats/stats-services/stats-services.component.ts @@ -21,7 +21,6 @@ export class StatsServicesComponent implements OnInit { chartServicesByDriverData: any; chartServicesByTypeData: any; chartServicesByVillageData: any; - chartServicesByMunicipalityData: any; @ViewChild("servicesMap") servicesMap!: MapComponent; @@ -109,14 +108,6 @@ export class StatsServicesComponent implements OnInit { villages[service.place.village] = 0; } 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); @@ -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() { diff --git a/frontend/src/app/_services/auth.service.ts b/frontend/src/app/_services/auth.service.ts index fa14fef..9b606ad 100644 --- a/frontend/src/app/_services/auth.service.ts +++ b/frontend/src/app/_services/auth.service.ts @@ -14,175 +14,190 @@ export interface LoginResponse { providedIn: 'root' }) export class AuthService { - private defaultPlaceholderProfile: any = { - id: undefined, - impersonating: false, - can: (permission: string) => false - }; - public profile: any = this.defaultPlaceholderProfile; - public authChanged = new Subject(); - public _authLoaded = false; + private defaultPlaceholderProfile: any = { + id: undefined, + impersonating: false, + options: {}, + can: (permission: string) => false, + getOption: (option: string, defaultValue: string) => defaultValue, + }; + public profile: any = this.defaultPlaceholderProfile; + public authChanged = new Subject(); + public _authLoaded = false; - public loadProfile() { - console.log("Loading profile data..."); - return new Promise((resolve, reject) => { - this.api.post("me").then((data: any) => { - this.profile = data; - - this.profile.can = (permission: string) => { - return this.profile.permissions.includes(permission); - } + public loadProfile() { + console.log("Loading profile data..."); + return new Promise((resolve, reject) => { + this.api.post("me").then((data: any) => { + this.profile = data; + this.profile.options = this.profile.options.reduce((acc: any, val: any) => { + acc[val.name] = val.value; + return acc; + }, {}); - 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((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 { - 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 { - 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.can = (permission: string) => { + return this.profile.permissions.includes(permission); } + + 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((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 { + 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 { + 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); + }); } + } } diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index e592e09..9166fa6 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -1,333 +1,337 @@ { - "menu": { - "list": "List", - "services": "Services", - "trainings": "Trainings", - "logs": "Logs", - "stats": "Stats", - "admin": "Admin", - "logout": "Logout", - "stop_impersonating": "Stop impersonating", - "hi": "hi" - }, - "admin": { - "info": "info", - "maintenance": "maintenance", - "roles": "roles", - "installed_migrations": "installed migrations", - "open_connections": "open connections", - "db_engine_name": "database engine name", - "database": "database", - "host": "host", - "port": "port", - "charset": "charset", - "prefix": "prefix", - "operations": "operations", - "run_migrations": "run migrations", - "run_migrations_success": "Migrations executed successfully", - "run_seeding": "run 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_success": "Seeding executed successfully", - "show_tables": "show tables list", - "hide_tables": "hide tables list", - "table": "table", - "rows": "rows", - "updates_and_maintenance_title": "Updates and maintenance", - "maintenance_mode_success": "Maintenance mode updated successfully", - "optimization": "optimization", - "run_optimization": "run optimization", - "run_optimization_success": "Optimization executed successfully", - "clear_optimization": "clear optimization", - "clear_optimization_success": "Optimization cleared successfully", - "clear_cache": "clear cache", - "clear_cache_success": "Cache cleared successfully", - "telegram_bot": "Telegram Bot", - "telegram_webhook": "Telegram Webhook", - "telegram_webhook_set": "Set Telegram Webhook", - "telegram_webhook_set_success": "Telegram Webhook set successfully", - "telegram_webhook_unset": "Unset Telegram Webhook", - "telegram_webhook_unset_success": "Telegram Webhook unset successfully", - "manual_execution": "manual execution", - "run": "run", - "run_confirm_title": "Are you sure you want to run this command?", - "run_confirm_text": "This action cannot be undone.", - "run_success": "Command executed successfully", - "env_operations": "environment variables operations", - "env_encrypt": "encrypt .env", - "env_encrypt_title": "Encrypt .env file", - "env_encrypt_confirm": "Insert the password to encrypt the .env file", - "env_encrypt_success": ".env encrypted successfully", - "env_decrypt": "decrypt .env", - "env_decrypt_title": "Decrypt .env file", - "env_decrypt_confirm": "Insert the password to decrypt the .env file", - "env_decrypt_success": ".env decrypted successfully", - "env_delete": "delete .env", - "env_delete_title": "Delete .env file", - "env_delete_confirm": "Are you sure you want to delete the .env file?", - "env_delete_success": ".env deleted successfully", - "options": "Options", - "option_update_success": "Option updated successfully" - }, - "table": { - "remove_service_confirm": "Are you sure you want to remove this service?", - "remove_service_confirm_text": "This action cannot be undone.", - "service_deleted_successfully": "Service deleted successfully", - "service_deleted_error": "Service could not be deleted. Please try again." - }, - "list": { - "your_availability_is": "You are:", - "enable_schedules": "Enable hour schedules", - "disable_schedules": "Disable hour schedules", - "update_schedules": "Update availability schedules", - "connect_telegram_bot": "Connect your account to the Telegram bot", - "tooltip_change_availability": "Change your availability to {{state}}", - "manual_mode_updated_successfully": "Manual mode updated successfully", - "schedule_load_failed": "Schedule could not be loaded. 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_change_failed": "Availability could not be changed. 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" - }, - "alert": { - "warning_body": "Alert in progress.", - "current_alert": "Current alert", - "current_alerts": "Current alerts", - "state": "Alert state", - "closed": "Alert closed", - "request_response_question": "Do you respond to the alert?", - "response_status": "Response status", - "no_response": "No response", - "waiting_for_response": "Waiting for response", - "response_yes": "Available", - "response_no": "Not available", - "details": "Alert details", - "delete": "Remove current alert", - "delete_confirm_title": "Are you sure you want to remove this alert?", - "delete_confirm_text": "This action cannot be undone.", - "deleted_successfully": "Alert removed successfully", - "delete_failed": "Alert could not be removed. Please try again", - "settings_updated_successfully": "Settings updated successfully", - "response_updated_successfully": "Response updated successfully" - }, - "login": { - "submit_btn": "Login" - }, - "place_details": { - "open_in_google_maps": "Open in Google Maps", - "place_name": "Place name", - "house_number": "house number", - "road": "road", - "village": "village", - "postcode": "postcode", - "hamlet": "hamlet", - "municipality": "municipality", - "country": "country", - "place_load_failed": "Place could not be loaded. Please try again" - }, - "map_picker": { - "loading_error": "Error loading search results. Please try again later" - }, - "edit_service": { - "select_start_datetime": "Select start date and time for the service", - "select_end_datetime": "Select end date and time for the service", - "insert_code": "Insert service code", - "other_crew_members": "Other crew members", - "select_service_type": "Select a service type", - "type_added_successfully": "Type added successfully", - "service_added_successfully": "Service added successfully", - "service_add_failed": "Service could not be added. Please try again", - "service_updated_successfully": "Service updated successfully", - "service_update_failed": "Service could not be updated. 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", - "types_load_failed": "Types could not be loaded. Please try again", - "type_add_failed": "Type could not be added. Please try again" - }, - "edit_training": { - "select_start_datetime": "Select start date and time for the training", - "select_end_datetime": "Select end date and time for the training", - "insert_name": "Insert training name", - "name_placeholder": "Training name", - "other_crew_members": "Other crew members", - "training_added_successfully": "Training added successfully", - "training_add_failed": "Training could not be added. Please try again", - "training_updated_successfully": "Training updated successfully", - "training_update_failed": "Training could not be updated. Please try again", - "training_load_failed": "Error loading training. Please try again", - "users_load_failed": "Error loading users. Please try again" - }, - "edit_user": { - "success_text": "User updated successfully", - "error_text": "User could not be updated. Please try again", - "creation_date": "User creation date", - "last_update": "User last update", - "last_access": "User last access" - }, - "user_info_modal": { - "title": "User info" - }, - "training_course_modal": { - "title": "Add training course", - "doc_number": "document number" - }, - "medical_examination_modal": { - "title": "Add medical examination" - }, - "useragent_info_modal": { - "title": "Client information" - }, - "validation": { - "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_already_exists": "Type already exists", - "image_format_not_supported": "Image format not supported", - "document_format_not_supported": "Document format not supported", - "file_too_big": "File too big", - "password_min_length": "Password must be at least 6 characters long" - }, - "options": { - "service_place_selection_manual": "Manual place selection for services", - "no_selection_available": "No selection available" - }, - "update_available": "Update available", - "update_available_text": "A new version of the application is available. Do you want to update now?", - "update_now": "Update now", - "yes_remove": "Yes, remove", - "confirm": "Confirm", - "cancel": "Cancel", - "enable": "enable", - "disable": "disable", - "maintenance_mode": "maintenance mode", - "maintenance_mode_warning": "The application is currently in maintenance mode. Some features may not be available.", - "offline": "offline", - "offline_warning": "You're offline. Some features may not be available.", - "property": "property", - "value": "value", - "user_agent": "User Agent", - "browser": "browser", - "engine": "engine", - "os": "Operating System", - "device": "device", - "cpu": "CPU", - "username": "username", - "password": "password", - "new_password": "new password", - "confirm_password": "confirm password", - "password_not_match": "Passwords do not match. Please try again.", - "password_changed_successfully": "Password changed successfully", - "password_change_title": "Change password", - "change_password": "Change password", - "warning": "warning", - "press_for_more_info": "press here for more info", - "update_availability_schedule": "Update availability schedule", - "select_type": "Select a type", - "save_changes": "Save changes", - "close": "Close", - "monday": "Monday", - "monday_short": "Mon", - "tuesday": "Tuesday", - "tuesday_short": "Tue", - "wednesday": "Wednesday", - "wednesday_short": "Wed", - "thursday": "Thursday", - "thursday_short": "Thu", - "friday": "Friday", - "friday_short": "Fri", - "saturday": "Saturday", - "saturday_short": "Sat", - "sunday": "Sunday", - "sunday_short": "Sun", - "programmed": "programmed", - "available": "available", - "unavailable": "unavailable", - "set_available": "available", - "set_unavailable": "unavailable", - "name": "name", - "surname": "surname", - "ssn": "Social Security Number", - "address": "address", - "zip_code": "zip code", - "phone_number": "phone number", - "email": "email", - "birthday": "birthday", - "birthplace": "birthplace", - "personal_information": "personal information", - "contact_information": "contact information", - "service_information": "service information", - "device_information": "device information", - "course_date": "course date", - "documents": "documents", - "driving_license": "driving license", - "driving_license_expiration_date": "driving license expiration date", - "driving_license_number": "driving license number", - "driving_license_type": "driving license type", - "driving_license_scan": "driving license scan", - "upload_scan": "upload scan", - "upload_medical_examination_certificate": "upload medical examination certificate", - "upload_training_course_doc": "upload training course document", - "clothings": "clothings", - "size": "size", - "suit_size": "suit size", - "boot_size": "boot size", - "medical_examinations": "medical examinations", - "training_courses": "training courses", - "date": "date", - "expiration_date": "expiration date", - "certifier": "certifier", - "certificate_short": "cert.", - "banned": "banned", - "hidden": "hidden", - "driver": "driver", - "drivers": "drivers", - "call": "call", - "service": "service", - "services": "services", - "training": "training", - "trainings": "trainings", - "user": "user", - "users": "users", - "availability_minutes": "availability_minutes", - "action": "action", - "changed": "changed", - "editor": "editor", - "datetime": "datetime", - "start": "start", - "end": "end", - "code": "code", - "chief": "chief", - "crew": "crew", - "place": "place", - "province": "province", - "notes": "notes", - "type": "type", - "add": "add", - "update": "update", - "remove": "remove", - "more details": "more details", - "search": "search", - "submit": "invia", - "reset": "reset", - "go_back": "Go back", - "next": "next", - "previous": "previous", - "last": "last", - "first": "first", - "total_elements_with_filters": "Total elements (curr. selection)", - "press_to_select_a_date": "Press to select a date", - "footer_text": "Allerta-VVF, free software developed for volunteer firefighters brigades.", - "revision": "revision", - "unknown": "unknown", - "edit": "edit", - "never": "never", - "optional": "optional", - "not_enough_permissions": "You don't have enough permissions to access this page.", - "open_services_stats": "To view the statistics, go to the \"Stats\" page.", - "error_title": "Error", - "success_title": "Success", - "select_date_range": "Select date range", - "remove_date_filters": "Remove date filters", - "yes": "yes", - "no": "no" -} \ No newline at end of file + "menu": { + "list": "List", + "services": "Services", + "trainings": "Trainings", + "logs": "Logs", + "stats": "Stats", + "admin": "Admin", + "logout": "Logout", + "stop_impersonating": "Stop impersonating", + "hi": "hi" + }, + "admin": { + "info": "info", + "maintenance": "maintenance", + "roles": "roles", + "installed_migrations": "installed migrations", + "open_connections": "open connections", + "db_engine_name": "database engine name", + "database": "database", + "host": "host", + "port": "port", + "charset": "charset", + "prefix": "prefix", + "operations": "operations", + "run_migrations": "run migrations", + "run_migrations_success": "Migrations executed successfully", + "run_seeding": "run 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_success": "Seeding executed successfully", + "show_tables": "show tables list", + "hide_tables": "hide tables list", + "table": "table", + "rows": "rows", + "updates_and_maintenance_title": "Updates and maintenance", + "maintenance_mode_success": "Maintenance mode updated successfully", + "optimization": "optimization", + "run_optimization": "run optimization", + "run_optimization_success": "Optimization executed successfully", + "clear_optimization": "clear optimization", + "clear_optimization_success": "Optimization cleared successfully", + "clear_cache": "clear cache", + "clear_cache_success": "Cache cleared successfully", + "telegram_bot": "Telegram Bot", + "telegram_webhook": "Telegram Webhook", + "telegram_webhook_set": "Set Telegram Webhook", + "telegram_webhook_set_success": "Telegram Webhook set successfully", + "telegram_webhook_unset": "Unset Telegram Webhook", + "telegram_webhook_unset_success": "Telegram Webhook unset successfully", + "manual_execution": "manual execution", + "run": "run", + "run_confirm_title": "Are you sure you want to run this command?", + "run_confirm_text": "This action cannot be undone.", + "run_success": "Command executed successfully", + "env_operations": "environment variables operations", + "env_encrypt": "encrypt .env", + "env_encrypt_title": "Encrypt .env file", + "env_encrypt_confirm": "Insert the password to encrypt the .env file", + "env_encrypt_success": ".env encrypted successfully", + "env_decrypt": "decrypt .env", + "env_decrypt_title": "Decrypt .env file", + "env_decrypt_confirm": "Insert the password to decrypt the .env file", + "env_decrypt_success": ".env decrypted successfully", + "env_delete": "delete .env", + "env_delete_title": "Delete .env file", + "env_delete_confirm": "Are you sure you want to delete the .env file?", + "env_delete_success": ".env deleted successfully", + "options": "Options", + "option_update_success": "Option updated successfully" + }, + "table": { + "remove_service_confirm": "Are you sure you want to remove this service?", + "remove_service_confirm_text": "This action cannot be undone.", + "service_deleted_successfully": "Service deleted successfully", + "service_deleted_error": "Service could not be deleted. Please try again." + }, + "list": { + "your_availability_is": "You are:", + "enable_schedules": "Enable hour schedules", + "disable_schedules": "Disable hour schedules", + "update_schedules": "Update availability schedules", + "connect_telegram_bot": "Connect your account to the Telegram bot", + "tooltip_change_availability": "Change your availability to {{state}}", + "manual_mode_updated_successfully": "Manual mode updated successfully", + "schedule_load_failed": "Schedule could not be loaded. 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_change_failed": "Availability could not be changed. 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" + }, + "alert": { + "warning_body": "Alert in progress.", + "current_alert": "Current alert", + "current_alerts": "Current alerts", + "state": "Alert state", + "closed": "Alert closed", + "request_response_question": "Do you respond to the alert?", + "response_status": "Response status", + "no_response": "No response", + "waiting_for_response": "Waiting for response", + "response_yes": "Available", + "response_no": "Not available", + "details": "Alert details", + "delete": "Remove current alert", + "delete_confirm_title": "Are you sure you want to remove this alert?", + "delete_confirm_text": "This action cannot be undone.", + "deleted_successfully": "Alert removed successfully", + "delete_failed": "Alert could not be removed. Please try again", + "settings_updated_successfully": "Settings updated successfully", + "response_updated_successfully": "Response updated successfully" + }, + "login": { + "submit_btn": "Login" + }, + "place_details": { + "open_in_google_maps": "Open in Google Maps", + "place_name": "Place name", + "house_number": "house number", + "road": "road", + "village": "village", + "postcode": "postcode", + "hamlet": "hamlet", + "municipality": "municipality", + "country": "country", + "place_load_failed": "Place could not be loaded. Please try again" + }, + "map_picker": { + "loading_error": "Error loading search results. Please try again later" + }, + "edit_service": { + "select_start_datetime": "Select start date and time for the service", + "select_end_datetime": "Select end date and time for the service", + "insert_code": "Insert service code", + "other_crew_members": "Other crew members", + "select_service_type": "Select a service type", + "type_added_successfully": "Type added successfully", + "service_added_successfully": "Service added successfully", + "service_add_failed": "Service could not be added. Please try again", + "service_updated_successfully": "Service updated successfully", + "service_update_failed": "Service could not be updated. 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", + "types_load_failed": "Types could not be loaded. Please try again", + "type_add_failed": "Type could not be added. Please try again" + }, + "edit_training": { + "select_start_datetime": "Select start date and time for the training", + "select_end_datetime": "Select end date and time for the training", + "insert_name": "Insert training name", + "name_placeholder": "Training name", + "other_crew_members": "Other crew members", + "training_added_successfully": "Training added successfully", + "training_add_failed": "Training could not be added. Please try again", + "training_updated_successfully": "Training updated successfully", + "training_update_failed": "Training could not be updated. Please try again", + "training_load_failed": "Error loading training. Please try again", + "users_load_failed": "Error loading users. Please try again" + }, + "edit_user": { + "success_text": "User updated successfully", + "error_text": "User could not be updated. Please try again", + "creation_date": "User creation date", + "last_update": "User last update", + "last_access": "User last access" + }, + "user_info_modal": { + "title": "User info" + }, + "training_course_modal": { + "title": "Add training course", + "doc_number": "document number" + }, + "medical_examination_modal": { + "title": "Add medical examination" + }, + "useragent_info_modal": { + "title": "Client information" + }, + "validation": { + "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_already_exists": "Type already exists", + "image_format_not_supported": "Image format not supported", + "document_format_not_supported": "Document format not supported", + "file_too_big": "File too big", + "password_min_length": "Password must be at least 6 characters long" + }, + "options": { + "service_place_selection_use_map_picker": "Use map to select service place", + "no_selection_available": "No selection available" + }, + "update_available": "Update available", + "update_available_text": "A new version of the application is available. Do you want to update now?", + "update_now": "Update now", + "yes_remove": "Yes, remove", + "confirm": "Confirm", + "cancel": "Cancel", + "enable": "enable", + "disable": "disable", + "maintenance_mode": "maintenance mode", + "maintenance_mode_warning": "The application is currently in maintenance mode. Some features may not be available.", + "offline": "offline", + "offline_warning": "You're offline. Some features may not be available.", + "property": "property", + "value": "value", + "user_agent": "User Agent", + "browser": "browser", + "engine": "engine", + "os": "Operating System", + "device": "device", + "cpu": "CPU", + "username": "username", + "password": "password", + "new_password": "new password", + "confirm_password": "confirm password", + "password_not_match": "Passwords do not match. Please try again.", + "password_changed_successfully": "Password changed successfully", + "password_change_title": "Change password", + "change_password": "Change password", + "warning": "warning", + "press_for_more_info": "press here for more info", + "update_availability_schedule": "Update availability schedule", + "select_type": "Select a type", + "save_changes": "Save changes", + "close": "Close", + "monday": "Monday", + "monday_short": "Mon", + "tuesday": "Tuesday", + "tuesday_short": "Tue", + "wednesday": "Wednesday", + "wednesday_short": "Wed", + "thursday": "Thursday", + "thursday_short": "Thu", + "friday": "Friday", + "friday_short": "Fri", + "saturday": "Saturday", + "saturday_short": "Sat", + "sunday": "Sunday", + "sunday_short": "Sun", + "programmed": "programmed", + "available": "available", + "unavailable": "unavailable", + "set_available": "available", + "set_unavailable": "unavailable", + "name": "name", + "surname": "surname", + "ssn": "Social Security Number", + "address": "address", + "zip_code": "zip code", + "cadastral_code": "cadastral code", + "phone_number": "phone number", + "fax": "fax", + "email": "email", + "pec": "PEC", + "birthday": "birthday", + "birthplace": "birthplace", + "personal_information": "personal information", + "contact_information": "contact information", + "service_information": "service information", + "device_information": "device information", + "course_date": "course date", + "documents": "documents", + "driving_license": "driving license", + "driving_license_expiration_date": "driving license expiration date", + "driving_license_number": "driving license number", + "driving_license_type": "driving license type", + "driving_license_scan": "driving license scan", + "upload_scan": "upload scan", + "upload_medical_examination_certificate": "upload medical examination certificate", + "upload_training_course_doc": "upload training course document", + "clothings": "clothings", + "size": "size", + "suit_size": "suit size", + "boot_size": "boot size", + "medical_examinations": "medical examinations", + "training_courses": "training courses", + "date": "date", + "expiration_date": "expiration date", + "certifier": "certifier", + "certificate_short": "cert.", + "banned": "banned", + "hidden": "hidden", + "driver": "driver", + "drivers": "drivers", + "call": "call", + "service": "service", + "services": "services", + "training": "training", + "trainings": "trainings", + "user": "user", + "users": "users", + "availability_minutes": "availability_minutes", + "action": "action", + "changed": "changed", + "editor": "editor", + "datetime": "datetime", + "start": "start", + "end": "end", + "code": "code", + "chief": "chief", + "crew": "crew", + "place": "place", + "province": "province", + "region": "region", + "notes": "notes", + "type": "type", + "add": "add", + "update": "update", + "remove": "remove", + "more details": "more details", + "search": "search", + "submit": "invia", + "reset": "reset", + "go_back": "Go back", + "next": "next", + "previous": "previous", + "last": "last", + "first": "first", + "total_elements_with_filters": "Total elements (curr. selection)", + "press_to_select_a_date": "Press to select a date", + "footer_text": "Allerta-VVF, free software developed for volunteer firefighters brigades.", + "revision": "revision", + "unknown": "unknown", + "edit": "edit", + "never": "never", + "optional": "optional", + "not_enough_permissions": "You don't have enough permissions to access this page.", + "open_services_stats": "To view the statistics, go to the \"Stats\" page.", + "error_title": "Error", + "success_title": "Success", + "select_date_range": "Select date range", + "remove_date_filters": "Remove date filters", + "yes": "yes", + "no": "no" +} diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 2f961d0..b7c2d8a 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -1,333 +1,337 @@ { - "menu": { - "list": "Lista disponibilità", - "services": "Interventi", - "trainings": "Esercitazioni", - "logs": "Logs", - "stats": "Statistiche", - "admin": "Amministrazione", - "logout": "Logout", - "stop_impersonating": "Torna al vero account", - "hi": "Ciao" - }, - "admin": { - "info": "Info", - "maintenance": "Manutenzione", - "roles": "Ruoli", - "installed_migrations": "migrazioni installate", - "open_connections": "connessioni aperte", - "db_engine_name": "nome del motore del database", - "database": "database", - "host": "host", - "port": "porta", - "charset": "charset", - "prefix": "prefisso", - "operations": "operazioni", - "run_migrations": "esegui migrazioni", - "run_migrations_success": "Migrazioni eseguite con successo", - "run_seeding": "esegui 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_success": "Seeding eseguito con successo", - "show_tables": "mostra lista tabelle", - "hide_tables": "nascondi lista tabelle", - "table": "tabella", - "rows": "righe", - "updates_and_maintenance_title": "Aggiornamenti e manutenzione", - "maintenance_mode_success": "Modalità manutenzione aggiornata con successo", - "optimization": "ottimizzazione", - "run_optimization": "esegui ottimizzazione", - "run_optimization_success": "Ottimizzazione eseguita con successo", - "clear_optimization": "rimuovi ottimizzazione", - "clear_optimization_success": "Ottimizzazione rimossa con successo", - "clear_cache": "svuota cache", - "clear_cache_success": "Cache svuotata con successo", - "telegram_bot": "Bot Telegram", - "telegram_webhook": "Webhook Telegram", - "telegram_webhook_set": "Imposta Webhook Telegram", - "telegram_webhook_set_success": "Webhook Telegram impostato con successo", - "telegram_webhook_unset": "Rimuovi Webhook Telegram", - "telegram_webhook_unset_success": "Webhook Telegram rimosso con successo", - "manual_execution": "esecuzione manuale", - "run": "esegui", - "run_confirm_title": "Sei sicuro di voler eseguire questo comando?", - "run_confirm_text": "Questa operazione non potrà essere annullata.", - "run_success": "Comando eseguito con successo", - "env_operations": "operazioni alle variabili d'ambiente", - "env_encrypt": "cripta .env", - "env_encrypt_title": "Cripta il file .env", - "env_encrypt_text": "Inserisci la password per criptare il file .env", - "env_encrypt_success": ".env criptato con successo", - "env_decrypt": "decripta .env", - "env_decrypt_title": "Decripta il file .env", - "env_decrypt_text": "Inserisci la password per decriptare il file .env", - "env_decrypt_success": ".env decriptato con successo", - "env_delete": "rimuovi .env", - "env_delete_title": "Rimuovi il file .env", - "env_delete_text": "Sei sicuro di voler rimuovere il file .env?", - "env_delete_success": ".env rimosso con successo", - "options": "Opzioni", - "option_update_success": "Opzione aggiornata con successo" - }, - "table": { - "remove_service_confirm": "Sei sicuro di voler rimuovere questo intervento?", - "remove_service_confirm_text": "Questa operazione non può essere annullata.", - "service_deleted_successfully": "Intervento rimosso con successo", - "service_deleted_error": "Errore durante la rimozione dell'intervento. Riprova più tardi" - }, - "list": { - "your_availability_is": "Attualmente sei:", - "enable_schedules": "Abilita programmazione oraria", - "disable_schedules": "Disattiva programmazione oraria", - "update_schedules": "Modifica orari disponibilità", - "connect_telegram_bot": "Collega l'account al bot Telegram", - "tooltip_change_availability": "Cambia la tua disponibilità in {{state}}", - "manual_mode_updated_successfully": "Modalità manuale aggiornata con successo", - "schedule_load_failed": "Errore durante il caricamento 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_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", - "telegram_bot_token_request_failed": "Errore durante la richiesta del token del bot Telegram. Riprova più tardi" - }, - "alert": { - "warning_body": "Allertamento in corso.", - "current_alert": "Emergenza in corso", - "current_alerts": "Emergenze in corso", - "state": "Stato dell'allerta", - "closed": "Allerta rientrata", - "request_response_question": "Sarai presente alla chiamata?", - "response_status": "Stato della risposta", - "no_response": "Nessuna risposta", - "waiting_for_response": "In attesa di risposta", - "response_yes": "Presente", - "response_no": "Non presente", - "details": "Dettagli dell'allerta", - "delete": "Ritira allerta", - "delete_confirm_title": "Sei sicuro di voler rimuovere questa allerta?", - "delete_confirm_text": "I vigili saranno avvisati della rimozione.", - "deleted_successfully": "Allerta rimossa con successo", - "delete_failed": "L'eliminazione dell'allerta è fallita. Riprova più tardi", - "settings_updated_successfully": "Impostazioni aggiornate con successo", - "response_updated_successfully": "Risposta aggiornata con successo" - }, - "login": { - "submit_btn": "Login" - }, - "place_details": { - "open_in_google_maps": "Apri il luogo in Google Maps", - "place_name": "Nome del luogo", - "house_number": "numero civico", - "road": "strada", - "village": "comune", - "postcode": "CAP", - "hamlet": "frazione", - "municipality": "raggruppamento del comune", - "country": "nazione/zona", - "place_load_failed": "Errore durante il caricamento del luogo. Riprova più tardi" - }, - "map_picker": { - "loading_error": "Errore di caricamento dei risultati della ricerca. Riprovare più tardi" - }, - "edit_service": { - "select_start_datetime": "Seleziona data e ora di inizio dell'intervento", - "select_end_datetime": "Seleziona data e ora di fine dell'intervento", - "insert_code": "Inserisci il progressivo dell'intervento", - "other_crew_members": "Altri membri della squadra", - "select_service_type": "Seleziona una tipologia di intervento", - "type_added_successfully": "Tipologia aggiunta con successo", - "service_added_successfully": "Intervento aggiunto con successo", - "service_add_failed": "Errore durante l'aggiunta dell'intervento. Riprovare più tardi", - "service_updated_successfully": "Intervento aggiornato con successo", - "service_update_failed": "Errore durante l'aggiornamento 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", - "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" - }, - "edit_training": { - "select_start_datetime": "Seleziona data e ora di inizio dell'esercitazione", - "select_end_datetime": "Seleziona data e ora di fine dell'esercitazione", - "insert_name": "Inserisci il nome dell'esercitazione", - "name_placeholder": "Esercitazione di gennaio", - "other_crew_members": "Altri membri della squadra", - "training_added_successfully": "Esercitazione aggiunta con successo", - "training_add_failed": "Errore durante l'aggiunta dell'esercitazione. Riprovare più tardi", - "training_updated_successfully": "Esercitazione aggiornata con successo", - "training_update_failed": "Errore durante l'aggiornamento dell'esercitazione. 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" - }, - "edit_user": { - "success_text": "Utente aggiornato con successo", - "error_text": "L'utente non può essere aggiornato. Riprova più tardi", - "creation_date": "Data di creazione dell'utente", - "last_update": "Data di ultima modifica dell'utente", - "last_access": "Ultimo accesso dell'utente" - }, - "user_info_modal": { - "title": "Scheda utente" - }, - "training_course_modal": { - "title": "Aggiungi corso di formazione", - "doc_number": "numero Ordine del Giorno" - }, - "medical_examination_modal": { - "title": "Aggiungi visita medica" - }, - "useragent_info_modal": { - "title": "Informazioni sul client" - }, - "validation": { - "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_already_exists": "La tipologia è già presente", - "image_format_not_supported": "Formato immagine non supportato", - "document_format_not_supported": "Formato documento non supportato", - "file_too_big": "File troppo grande", - "password_min_length": "La password deve essere di almeno 6 caratteri" - }, - "options": { - "service_place_selection_manual": "Seleziona manualmente il luogo dell'intervento", - "no_selection_available": "Nessuna selezione disponibile" - }, - "update_available": "Aggiornamento disponibile", - "update_available_text": "È disponibile un aggiornamento per Allerta. Vuoi aggiornare ora?", - "update_now": "Aggiorna ora", - "yes_remove": "Si, rimuovi", - "confirm": "Conferma", - "cancel": "Annulla", - "enable": "attiva", - "disable": "disattiva", - "maintenance_mode": "modalità manutenzione", - "maintenance_mode_warning": "Il gestionale è in manutenzione. Alcune funzionalità potrebbero non essere disponibili.", - "offline": "offline", - "offline_warning": "Sei offline. Non è possibile interagire con il gestionale.", - "property": "proprietà", - "value": "valore", - "user_agent": "User Agent", - "browser": "browser", - "engine": "motore", - "os": "Sistema Operativo", - "device": "dispositivo", - "cpu": "CPU", - "username": "username", - "password": "password", - "new_password": "nuova password", - "confirm_password": "conferma password", - "password_not_match": "Le password non corrispondono. Riprova.", - "password_changed_successfully": "Password cambiata con successo", - "password_change_title": "Cambio password", - "change_password": "Cambia password", - "warning": "attenzione", - "press_for_more_info": "premi qui per informazioni", - "update_availability_schedule": "Aggiorna programmazione disponibilità", - "select_type": "Seleziona una tipologia", - "save_changes": "Salva modifiche", - "close": "Chiudi", - "monday": "Lunedì", - "monday_short": "Lun", - "tuesday": "Martedì", - "tuesday_short": "Mar", - "wednesday": "Mercoledì", - "wednesday_short": "Mer", - "thursday": "Giovedì", - "thursday_short": "Gio", - "friday": "Venerdì", - "friday_short": "Ven", - "saturday": "Sabato", - "saturday_short": "Sab", - "sunday": "Domenica", - "sunday_short": "Dom", - "programmed": "programmata", - "available": "disponibile", - "unavailable": "non disponibile", - "set_available": "attiva", - "set_unavailable": "disattiva", - "name": "nome", - "surname": "cognome", - "ssn": "codice fiscale", - "address": "indirizzo", - "zip_code": "CAP", - "phone_number": "numero di telefono", - "email": "email", - "birthday": "data di nascita", - "birthplace": "luogo di nascita", - "personal_information": "anagrafica personale", - "contact_information": "recapiti", - "service_information": "informazioni di servizio", - "device_information": "informazioni sul dispositivo", - "course_date": "data corso", - "documents": "documenti", - "driving_license": "patente", - "driving_license_expiration_date": "scadenza patente", - "driving_license_number": "numero patente", - "driving_license_type": "tipologia patente", - "driving_license_scan": "scansione patente", - "upload_scan": "carica scansione", - "upload_medical_examination_certificate": "carica certificato visita medica", - "upload_training_course_doc": "carica Ordine del Giorno", - "clothings": "indumenti", - "size": "dimensione", - "suit_size": "taglia tuta", - "boot_size": "taglia scarponi", - "medical_examinations": "visite mediche", - "training_courses": "corsi di formazione", - "date": "data", - "expiration_date": "data di scadenza", - "certifier": "ente/certificatore", - "certificate_short": "cert.", - "banned": "bannato", - "hidden": "nascosto", - "driver": "autista", - "drivers": "autisti", - "call": "chiama", - "service": "intervento", - "services": "interventi", - "training": "esercitazione", - "trainings": "esercitazioni", - "user": "utente", - "users": "utenti", - "availability_minutes": "minuti di disponibilità", - "action": "azione", - "changed": "interessato", - "editor": "fatto da", - "datetime": "data e ora", - "start": "inizio", - "end": "fine", - "code": "codice", - "chief": "caposquadra", - "crew": "squadra", - "place": "luogo", - "province": "provincia", - "notes": "note", - "type": "tipologia", - "add": "aggiungi", - "update": "modifica", - "remove": "rimuovi", - "more details": "altri dettagli", - "search": "cerca", - "submit": "invia", - "reset": "reset", - "go_back": "Torna indietro", - "next": "successiva", - "previous": "precedente", - "last": "ultima", - "first": "prima", - "total_elements_with_filters": "# Elementi (selezione corrente)", - "press_to_select_a_date": "Premi per selezionare una data", - "footer_text": "Allerta-VVF, software libero realizzato per i Vigili del Fuoco volontari.", - "revision": "revisione", - "unknown": "sconosciuto", - "edit": "modifica", - "never": "mai", - "optional": "opzionale", - "not_enough_permissions": "Non hai i permessi necessari per accedere a questa pagina.", - "open_services_stats": "Per visualizzare statistiche e mappa degli interventi, vai alla sezione \"Statistiche\".", - "error_title": "Errore", - "success_title": "Successo", - "select_date_range": "Seleziona un intervallo di date", - "remove_date_filters": "Rimuovi filtri", - "yes": "si", - "no": "no" -} \ No newline at end of file + "menu": { + "list": "Lista disponibilità", + "services": "Interventi", + "trainings": "Esercitazioni", + "logs": "Logs", + "stats": "Statistiche", + "admin": "Amministrazione", + "logout": "Logout", + "stop_impersonating": "Torna al vero account", + "hi": "Ciao" + }, + "admin": { + "info": "Info", + "maintenance": "Manutenzione", + "roles": "Ruoli", + "installed_migrations": "migrazioni installate", + "open_connections": "connessioni aperte", + "db_engine_name": "nome del motore del database", + "database": "database", + "host": "host", + "port": "porta", + "charset": "charset", + "prefix": "prefisso", + "operations": "operazioni", + "run_migrations": "esegui migrazioni", + "run_migrations_success": "Migrazioni eseguite con successo", + "run_seeding": "esegui 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_success": "Seeding eseguito con successo", + "show_tables": "mostra lista tabelle", + "hide_tables": "nascondi lista tabelle", + "table": "tabella", + "rows": "righe", + "updates_and_maintenance_title": "Aggiornamenti e manutenzione", + "maintenance_mode_success": "Modalità manutenzione aggiornata con successo", + "optimization": "ottimizzazione", + "run_optimization": "esegui ottimizzazione", + "run_optimization_success": "Ottimizzazione eseguita con successo", + "clear_optimization": "rimuovi ottimizzazione", + "clear_optimization_success": "Ottimizzazione rimossa con successo", + "clear_cache": "svuota cache", + "clear_cache_success": "Cache svuotata con successo", + "telegram_bot": "Bot Telegram", + "telegram_webhook": "Webhook Telegram", + "telegram_webhook_set": "Imposta Webhook Telegram", + "telegram_webhook_set_success": "Webhook Telegram impostato con successo", + "telegram_webhook_unset": "Rimuovi Webhook Telegram", + "telegram_webhook_unset_success": "Webhook Telegram rimosso con successo", + "manual_execution": "esecuzione manuale", + "run": "esegui", + "run_confirm_title": "Sei sicuro di voler eseguire questo comando?", + "run_confirm_text": "Questa operazione non potrà essere annullata.", + "run_success": "Comando eseguito con successo", + "env_operations": "operazioni alle variabili d'ambiente", + "env_encrypt": "cripta .env", + "env_encrypt_title": "Cripta il file .env", + "env_encrypt_text": "Inserisci la password per criptare il file .env", + "env_encrypt_success": ".env criptato con successo", + "env_decrypt": "decripta .env", + "env_decrypt_title": "Decripta il file .env", + "env_decrypt_text": "Inserisci la password per decriptare il file .env", + "env_decrypt_success": ".env decriptato con successo", + "env_delete": "rimuovi .env", + "env_delete_title": "Rimuovi il file .env", + "env_delete_text": "Sei sicuro di voler rimuovere il file .env?", + "env_delete_success": ".env rimosso con successo", + "options": "Opzioni", + "option_update_success": "Opzione aggiornata con successo" + }, + "table": { + "remove_service_confirm": "Sei sicuro di voler rimuovere questo intervento?", + "remove_service_confirm_text": "Questa operazione non può essere annullata.", + "service_deleted_successfully": "Intervento rimosso con successo", + "service_deleted_error": "Errore durante la rimozione dell'intervento. Riprova più tardi" + }, + "list": { + "your_availability_is": "Attualmente sei:", + "enable_schedules": "Abilita programmazione oraria", + "disable_schedules": "Disattiva programmazione oraria", + "update_schedules": "Modifica orari disponibilità", + "connect_telegram_bot": "Collega l'account al bot Telegram", + "tooltip_change_availability": "Cambia la tua disponibilità in {{state}}", + "manual_mode_updated_successfully": "Modalità manuale aggiornata con successo", + "schedule_load_failed": "Errore durante il caricamento 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_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", + "telegram_bot_token_request_failed": "Errore durante la richiesta del token del bot Telegram. Riprova più tardi" + }, + "alert": { + "warning_body": "Allertamento in corso.", + "current_alert": "Emergenza in corso", + "current_alerts": "Emergenze in corso", + "state": "Stato dell'allerta", + "closed": "Allerta rientrata", + "request_response_question": "Sarai presente alla chiamata?", + "response_status": "Stato della risposta", + "no_response": "Nessuna risposta", + "waiting_for_response": "In attesa di risposta", + "response_yes": "Presente", + "response_no": "Non presente", + "details": "Dettagli dell'allerta", + "delete": "Ritira allerta", + "delete_confirm_title": "Sei sicuro di voler rimuovere questa allerta?", + "delete_confirm_text": "I vigili saranno avvisati della rimozione.", + "deleted_successfully": "Allerta rimossa con successo", + "delete_failed": "L'eliminazione dell'allerta è fallita. Riprova più tardi", + "settings_updated_successfully": "Impostazioni aggiornate con successo", + "response_updated_successfully": "Risposta aggiornata con successo" + }, + "login": { + "submit_btn": "Login" + }, + "place_details": { + "open_in_google_maps": "Apri il luogo in Google Maps", + "place_name": "Nome del luogo", + "house_number": "numero civico", + "road": "strada", + "village": "comune", + "postcode": "CAP", + "hamlet": "frazione", + "municipality": "comune", + "country": "nazione/zona", + "place_load_failed": "Errore durante il caricamento del luogo. Riprova più tardi" + }, + "map_picker": { + "loading_error": "Errore di caricamento dei risultati della ricerca. Riprovare più tardi" + }, + "edit_service": { + "select_start_datetime": "Seleziona data e ora di inizio dell'intervento", + "select_end_datetime": "Seleziona data e ora di fine dell'intervento", + "insert_code": "Inserisci il progressivo dell'intervento", + "other_crew_members": "Altri membri della squadra", + "select_service_type": "Seleziona una tipologia di intervento", + "type_added_successfully": "Tipologia aggiunta con successo", + "service_added_successfully": "Intervento aggiunto con successo", + "service_add_failed": "Errore durante l'aggiunta dell'intervento. Riprovare più tardi", + "service_updated_successfully": "Intervento aggiornato con successo", + "service_update_failed": "Errore durante l'aggiornamento 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", + "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" + }, + "edit_training": { + "select_start_datetime": "Seleziona data e ora di inizio dell'esercitazione", + "select_end_datetime": "Seleziona data e ora di fine dell'esercitazione", + "insert_name": "Inserisci il nome dell'esercitazione", + "name_placeholder": "Esercitazione di gennaio", + "other_crew_members": "Altri membri della squadra", + "training_added_successfully": "Esercitazione aggiunta con successo", + "training_add_failed": "Errore durante l'aggiunta dell'esercitazione. Riprovare più tardi", + "training_updated_successfully": "Esercitazione aggiornata con successo", + "training_update_failed": "Errore durante l'aggiornamento dell'esercitazione. 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" + }, + "edit_user": { + "success_text": "Utente aggiornato con successo", + "error_text": "L'utente non può essere aggiornato. Riprova più tardi", + "creation_date": "Data di creazione dell'utente", + "last_update": "Data di ultima modifica dell'utente", + "last_access": "Ultimo accesso dell'utente" + }, + "user_info_modal": { + "title": "Scheda utente" + }, + "training_course_modal": { + "title": "Aggiungi corso di formazione", + "doc_number": "numero Ordine del Giorno" + }, + "medical_examination_modal": { + "title": "Aggiungi visita medica" + }, + "useragent_info_modal": { + "title": "Informazioni sul client" + }, + "validation": { + "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_already_exists": "La tipologia è già presente", + "image_format_not_supported": "Formato immagine non supportato", + "document_format_not_supported": "Formato documento non supportato", + "file_too_big": "File troppo grande", + "password_min_length": "La password deve essere di almeno 6 caratteri" + }, + "options": { + "service_place_selection_use_map_picker": "Utilizza una mappa per selezionare il luogo dell'intervento", + "no_selection_available": "Nessuna selezione disponibile" + }, + "update_available": "Aggiornamento disponibile", + "update_available_text": "È disponibile un aggiornamento per Allerta. Vuoi aggiornare ora?", + "update_now": "Aggiorna ora", + "yes_remove": "Si, rimuovi", + "confirm": "Conferma", + "cancel": "Annulla", + "enable": "attiva", + "disable": "disattiva", + "maintenance_mode": "modalità manutenzione", + "maintenance_mode_warning": "Il gestionale è in manutenzione. Alcune funzionalità potrebbero non essere disponibili.", + "offline": "offline", + "offline_warning": "Sei offline. Non è possibile interagire con il gestionale.", + "property": "proprietà", + "value": "valore", + "user_agent": "User Agent", + "browser": "browser", + "engine": "motore", + "os": "Sistema Operativo", + "device": "dispositivo", + "cpu": "CPU", + "username": "username", + "password": "password", + "new_password": "nuova password", + "confirm_password": "conferma password", + "password_not_match": "Le password non corrispondono. Riprova.", + "password_changed_successfully": "Password cambiata con successo", + "password_change_title": "Cambio password", + "change_password": "Cambia password", + "warning": "attenzione", + "press_for_more_info": "premi qui per informazioni", + "update_availability_schedule": "Aggiorna programmazione disponibilità", + "select_type": "Seleziona una tipologia", + "save_changes": "Salva modifiche", + "close": "Chiudi", + "monday": "Lunedì", + "monday_short": "Lun", + "tuesday": "Martedì", + "tuesday_short": "Mar", + "wednesday": "Mercoledì", + "wednesday_short": "Mer", + "thursday": "Giovedì", + "thursday_short": "Gio", + "friday": "Venerdì", + "friday_short": "Ven", + "saturday": "Sabato", + "saturday_short": "Sab", + "sunday": "Domenica", + "sunday_short": "Dom", + "programmed": "programmata", + "available": "disponibile", + "unavailable": "non disponibile", + "set_available": "attiva", + "set_unavailable": "disattiva", + "name": "nome", + "surname": "cognome", + "ssn": "codice fiscale", + "address": "indirizzo", + "zip_code": "CAP", + "cadastral_code": "codice catastale", + "phone_number": "numero di telefono", + "fax": "fax", + "email": "email", + "pec": "PEC", + "birthday": "data di nascita", + "birthplace": "luogo di nascita", + "personal_information": "anagrafica personale", + "contact_information": "recapiti", + "service_information": "informazioni di servizio", + "device_information": "informazioni sul dispositivo", + "course_date": "data corso", + "documents": "documenti", + "driving_license": "patente", + "driving_license_expiration_date": "scadenza patente", + "driving_license_number": "numero patente", + "driving_license_type": "tipologia patente", + "driving_license_scan": "scansione patente", + "upload_scan": "carica scansione", + "upload_medical_examination_certificate": "carica certificato visita medica", + "upload_training_course_doc": "carica Ordine del Giorno", + "clothings": "indumenti", + "size": "dimensione", + "suit_size": "taglia tuta", + "boot_size": "taglia scarponi", + "medical_examinations": "visite mediche", + "training_courses": "corsi di formazione", + "date": "data", + "expiration_date": "data di scadenza", + "certifier": "ente/certificatore", + "certificate_short": "cert.", + "banned": "bannato", + "hidden": "nascosto", + "driver": "autista", + "drivers": "autisti", + "call": "chiama", + "service": "intervento", + "services": "interventi", + "training": "esercitazione", + "trainings": "esercitazioni", + "user": "utente", + "users": "utenti", + "availability_minutes": "minuti di disponibilità", + "action": "azione", + "changed": "interessato", + "editor": "fatto da", + "datetime": "data e ora", + "start": "inizio", + "end": "fine", + "code": "codice", + "chief": "caposquadra", + "crew": "squadra", + "place": "luogo", + "province": "provincia", + "region": "regione", + "notes": "note", + "type": "tipologia", + "add": "aggiungi", + "update": "modifica", + "remove": "rimuovi", + "more details": "altri dettagli", + "search": "cerca", + "submit": "invia", + "reset": "reset", + "go_back": "Torna indietro", + "next": "successiva", + "previous": "precedente", + "last": "ultima", + "first": "prima", + "total_elements_with_filters": "# Elementi (selezione corrente)", + "press_to_select_a_date": "Premi per selezionare una data", + "footer_text": "Allerta-VVF, software libero realizzato per i Vigili del Fuoco volontari.", + "revision": "revisione", + "unknown": "sconosciuto", + "edit": "modifica", + "never": "mai", + "optional": "opzionale", + "not_enough_permissions": "Non hai i permessi necessari per accedere a questa pagina.", + "open_services_stats": "Per visualizzare statistiche e mappa degli interventi, vai alla sezione \"Statistiche\".", + "error_title": "Errore", + "success_title": "Successo", + "select_date_range": "Seleziona un intervallo di date", + "remove_date_filters": "Rimuovi filtri", + "yes": "si", + "no": "no" +}