Sistema multilingüe EN/ES: middleware SetLocale, LanguageSwitcher, campo locale en users, traducciones en dashboard/mapa/proyectos/gestores
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class SetLocale
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$locale = null;
|
||||
|
||||
// 1. From authenticated user preference
|
||||
if (Auth::check()) {
|
||||
$userLocale = Auth::user()->locale;
|
||||
if ($userLocale && in_array($userLocale, ['en', 'es'])) {
|
||||
$locale = $userLocale;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. From session
|
||||
if (!$locale && Session::has('locale')) {
|
||||
$sessionLocale = Session::get('locale');
|
||||
if (in_array($sessionLocale, ['en', 'es'])) {
|
||||
$locale = $sessionLocale;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. From browser Accept-Language
|
||||
if (!$locale) {
|
||||
$browserLang = substr($request->server('HTTP_ACCEPT_LANGUAGE', 'en'), 0, 2);
|
||||
if (in_array($browserLang, ['en', 'es'])) {
|
||||
$locale = $browserLang;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Default to English
|
||||
if (!$locale) {
|
||||
$locale = 'en';
|
||||
}
|
||||
|
||||
App::setLocale($locale);
|
||||
Session::put('locale', $locale);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class LanguageSwitcher extends Component
|
||||
{
|
||||
public $currentLocale;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->currentLocale = App::getLocale();
|
||||
}
|
||||
|
||||
public function switchLanguage($locale)
|
||||
{
|
||||
if (!in_array($locale, ['en', 'es'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
App::setLocale($locale);
|
||||
Session::put('locale', $locale);
|
||||
|
||||
if (Auth::check()) {
|
||||
$user = Auth::user();
|
||||
$user->locale = $locale;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
$this->currentLocale = $locale;
|
||||
$this->dispatch('localeChanged', $locale);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.language-switcher');
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
$middleware->appendToGroup('web', \App\Http\Middleware\SetLocale::class);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('locale', 5)->default('en')->after('remember_token');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('locale');
|
||||
});
|
||||
}
|
||||
};
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"Dashboard": "Dashboard",
|
||||
"Projects": "Projects",
|
||||
"Project": "Project",
|
||||
"Create Project": "Create Project",
|
||||
"Edit Project": "Edit Project",
|
||||
"New Project": "New Project",
|
||||
"Delete Project": "Delete Project",
|
||||
"Project name": "Project name",
|
||||
"Name": "Name",
|
||||
"Address": "Address",
|
||||
"Status": "Status",
|
||||
"Progress": "Progress",
|
||||
"Phases": "Phases",
|
||||
"Phase": "Phase",
|
||||
"Layer": "Layer",
|
||||
"Layers": "Layers",
|
||||
"Features": "Features",
|
||||
"Feature": "Element",
|
||||
"Create": "Create",
|
||||
"Edit": "Edit",
|
||||
"Delete": "Delete",
|
||||
"Save": "Save",
|
||||
"Update": "Update",
|
||||
"Cancel": "Cancel",
|
||||
"Search": "Search",
|
||||
"Filter": "Filter",
|
||||
"Back": "Back",
|
||||
"Actions": "Actions",
|
||||
"Map": "Map",
|
||||
"View Map": "View Map",
|
||||
"Manage Layers": "Manage Layers",
|
||||
"Upload Layer": "Upload Layer",
|
||||
"Upload file": "Upload file",
|
||||
"Download": "Download",
|
||||
"Fullscreen": "Fullscreen",
|
||||
"Close": "Close",
|
||||
"Are you sure?": "Are you sure?",
|
||||
"No results": "No results",
|
||||
"Loading": "Loading",
|
||||
"Active projects": "Active projects",
|
||||
"Total projects": "Total projects",
|
||||
"Total phases": "Total phases",
|
||||
"Total features": "Total elements",
|
||||
"Global progress": "Global progress",
|
||||
"Recent projects": "Recent projects",
|
||||
"Recent inspections": "Recent inspections",
|
||||
"Planning": "Planning",
|
||||
"In progress": "In progress",
|
||||
"Paused": "Paused",
|
||||
"Completed": "Completed",
|
||||
"Progress updated": "Progress updated",
|
||||
"Save progress": "Save progress",
|
||||
"Responsible": "Responsible",
|
||||
"Comment": "Comment",
|
||||
"History": "History",
|
||||
"Inspection": "Inspection",
|
||||
"Template": "Template",
|
||||
"Templates": "Templates",
|
||||
"Create Template": "Create Template",
|
||||
"Inspection template": "Inspection template",
|
||||
"Field": "Field",
|
||||
"Fields": "Fields",
|
||||
"Field name": "Field name",
|
||||
"Field type": "Field type",
|
||||
"Required": "Required",
|
||||
"Options": "Options",
|
||||
"Show images on map": "Show images on map",
|
||||
"Project files": "Project files",
|
||||
"Media": "Media",
|
||||
"Images": "Images",
|
||||
"Documents": "Documents",
|
||||
"Upload images": "Upload images",
|
||||
"Upload documents": "Upload documents",
|
||||
"Upload files": "Upload files",
|
||||
"Drop files here": "Drop files here",
|
||||
"File": "File",
|
||||
"Files": "Files",
|
||||
"Category": "Category",
|
||||
"Description": "Description",
|
||||
"Size": "Size",
|
||||
"Uploaded by": "Uploaded by",
|
||||
"Uploaded at": "Uploaded at",
|
||||
"No files yet": "No files yet",
|
||||
"Delete file": "Delete file",
|
||||
"Layer name": "Layer name",
|
||||
"Layer color": "Layer color",
|
||||
"Import file": "Import file",
|
||||
"Empty layer": "Empty layer",
|
||||
"Create empty layer": "Create empty layer",
|
||||
"Select phase": "Select phase",
|
||||
"Select project": "Select project",
|
||||
"Visible layers": "Visible layers",
|
||||
"Edit layer": "Edit layer",
|
||||
"Delete layer": "Delete layer",
|
||||
"Sign in": "Sign in",
|
||||
"Sign out": "Sign out",
|
||||
"Register": "Register",
|
||||
"Email": "Email",
|
||||
"Password": "Password",
|
||||
"Confirm password": "Confirm password",
|
||||
"Forgot password": "Forgot password",
|
||||
"Remember me": "Remember me",
|
||||
"Profile": "Profile",
|
||||
"Update profile": "Update profile",
|
||||
"Language": "Language",
|
||||
"English": "English",
|
||||
"Spanish": "Spanish",
|
||||
"Select language": "Select language",
|
||||
"Notifications": "Notifications",
|
||||
"Permissions": "Permissions",
|
||||
"Administrator": "Administrator",
|
||||
"Users": "Users",
|
||||
"Roles": "Roles",
|
||||
"Click on an element to edit": "Click on an element to edit",
|
||||
"No templates yet": "No templates yet",
|
||||
"Create an inspection template": "Create an inspection template",
|
||||
"No phases yet": "No phases yet",
|
||||
"Start date": "Start date",
|
||||
"Estimated end date": "Estimated end date",
|
||||
"Centered in project": "Centered in project",
|
||||
"My location": "My location",
|
||||
"File type not allowed": "File type not allowed",
|
||||
"Upload failed": "Upload failed",
|
||||
"Conversion failed": "Conversion failed",
|
||||
"All": "All",
|
||||
"Latitude": "Latitude",
|
||||
"Longitude": "Longitude",
|
||||
"Register inspection": "Register inspection",
|
||||
"Files of element": "Files of element",
|
||||
"Fases and layers": "Phases and layers",
|
||||
"Elements": "Elements",
|
||||
"optional": "optional",
|
||||
"each": "each",
|
||||
"Image": "Image",
|
||||
"Document": "Document",
|
||||
"Other": "Other",
|
||||
"Color": "Color",
|
||||
"Upload": "Upload"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used during authentication for various
|
||||
| messages that we need to display to the user. You are free to modify
|
||||
| these language lines according to your application's requirements.
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => 'These credentials do not match our records.',
|
||||
'password' => 'The provided password is incorrect.',
|
||||
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
|
||||
|
||||
];
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« Previous',
|
||||
'next' => 'Next »',
|
||||
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| outcome such as failure due to an invalid password / reset token.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => 'Your password has been reset.',
|
||||
'sent' => 'We have emailed your password reset link.',
|
||||
'throttled' => 'Please wait before retrying.',
|
||||
'token' => 'This password reset token is invalid.',
|
||||
'user' => "We can't find a user with that email address.",
|
||||
|
||||
];
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines contain the default error messages used by
|
||||
| the validator class. Some of these rules have multiple versions such
|
||||
| as the size rules. Feel free to tweak each of these messages here.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => 'The :attribute field must be accepted.',
|
||||
'accepted_if' => 'The :attribute field must be accepted when :other is :value.',
|
||||
'active_url' => 'The :attribute field must be a valid URL.',
|
||||
'after' => 'The :attribute field must be a date after :date.',
|
||||
'after_or_equal' => 'The :attribute field must be a date after or equal to :date.',
|
||||
'alpha' => 'The :attribute field must only contain letters.',
|
||||
'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.',
|
||||
'alpha_num' => 'The :attribute field must only contain letters and numbers.',
|
||||
'any_of' => 'The :attribute field is invalid.',
|
||||
'array' => 'The :attribute field must be an array.',
|
||||
'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
|
||||
'before' => 'The :attribute field must be a date before :date.',
|
||||
'before_or_equal' => 'The :attribute field must be a date before or equal to :date.',
|
||||
'between' => [
|
||||
'array' => 'The :attribute field must have between :min and :max items.',
|
||||
'file' => 'The :attribute field must be between :min and :max kilobytes.',
|
||||
'numeric' => 'The :attribute field must be between :min and :max.',
|
||||
'string' => 'The :attribute field must be between :min and :max characters.',
|
||||
],
|
||||
'boolean' => 'The :attribute field must be true or false.',
|
||||
'can' => 'The :attribute field contains an unauthorized value.',
|
||||
'confirmed' => 'The :attribute field confirmation does not match.',
|
||||
'contains' => 'The :attribute field is missing a required value.',
|
||||
'current_password' => 'The password is incorrect.',
|
||||
'date' => 'The :attribute field must be a valid date.',
|
||||
'date_equals' => 'The :attribute field must be a date equal to :date.',
|
||||
'date_format' => 'The :attribute field must match the format :format.',
|
||||
'decimal' => 'The :attribute field must have :decimal decimal places.',
|
||||
'declined' => 'The :attribute field must be declined.',
|
||||
'declined_if' => 'The :attribute field must be declined when :other is :value.',
|
||||
'different' => 'The :attribute field and :other must be different.',
|
||||
'digits' => 'The :attribute field must be :digits digits.',
|
||||
'digits_between' => 'The :attribute field must be between :min and :max digits.',
|
||||
'dimensions' => 'The :attribute field has invalid image dimensions.',
|
||||
'distinct' => 'The :attribute field has a duplicate value.',
|
||||
'doesnt_contain' => 'The :attribute field must not contain any of the following: :values.',
|
||||
'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.',
|
||||
'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.',
|
||||
'email' => 'The :attribute field must be a valid email address.',
|
||||
'encoding' => 'The :attribute field must be encoded in :encoding.',
|
||||
'ends_with' => 'The :attribute field must end with one of the following: :values.',
|
||||
'enum' => 'The selected :attribute is invalid.',
|
||||
'exists' => 'The selected :attribute is invalid.',
|
||||
'extensions' => 'The :attribute field must have one of the following extensions: :values.',
|
||||
'file' => 'The :attribute field must be a file.',
|
||||
'filled' => 'The :attribute field must have a value.',
|
||||
'gt' => [
|
||||
'array' => 'The :attribute field must have more than :value items.',
|
||||
'file' => 'The :attribute field must be greater than :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be greater than :value.',
|
||||
'string' => 'The :attribute field must be greater than :value characters.',
|
||||
],
|
||||
'gte' => [
|
||||
'array' => 'The :attribute field must have :value items or more.',
|
||||
'file' => 'The :attribute field must be greater than or equal to :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be greater than or equal to :value.',
|
||||
'string' => 'The :attribute field must be greater than or equal to :value characters.',
|
||||
],
|
||||
'hex_color' => 'The :attribute field must be a valid hexadecimal color.',
|
||||
'image' => 'The :attribute field must be an image.',
|
||||
'in' => 'The selected :attribute is invalid.',
|
||||
'in_array' => 'The :attribute field must exist in :other.',
|
||||
'in_array_keys' => 'The :attribute field must contain at least one of the following keys: :values.',
|
||||
'integer' => 'The :attribute field must be an integer.',
|
||||
'ip' => 'The :attribute field must be a valid IP address.',
|
||||
'ipv4' => 'The :attribute field must be a valid IPv4 address.',
|
||||
'ipv6' => 'The :attribute field must be a valid IPv6 address.',
|
||||
'json' => 'The :attribute field must be a valid JSON string.',
|
||||
'list' => 'The :attribute field must be a list.',
|
||||
'lowercase' => 'The :attribute field must be lowercase.',
|
||||
'lt' => [
|
||||
'array' => 'The :attribute field must have less than :value items.',
|
||||
'file' => 'The :attribute field must be less than :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be less than :value.',
|
||||
'string' => 'The :attribute field must be less than :value characters.',
|
||||
],
|
||||
'lte' => [
|
||||
'array' => 'The :attribute field must not have more than :value items.',
|
||||
'file' => 'The :attribute field must be less than or equal to :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be less than or equal to :value.',
|
||||
'string' => 'The :attribute field must be less than or equal to :value characters.',
|
||||
],
|
||||
'mac_address' => 'The :attribute field must be a valid MAC address.',
|
||||
'max' => [
|
||||
'array' => 'The :attribute field must not have more than :max items.',
|
||||
'file' => 'The :attribute field must not be greater than :max kilobytes.',
|
||||
'numeric' => 'The :attribute field must not be greater than :max.',
|
||||
'string' => 'The :attribute field must not be greater than :max characters.',
|
||||
],
|
||||
'max_digits' => 'The :attribute field must not have more than :max digits.',
|
||||
'mimes' => 'The :attribute field must be a file of type: :values.',
|
||||
'mimetypes' => 'The :attribute field must be a file of type: :values.',
|
||||
'min' => [
|
||||
'array' => 'The :attribute field must have at least :min items.',
|
||||
'file' => 'The :attribute field must be at least :min kilobytes.',
|
||||
'numeric' => 'The :attribute field must be at least :min.',
|
||||
'string' => 'The :attribute field must be at least :min characters.',
|
||||
],
|
||||
'min_digits' => 'The :attribute field must have at least :min digits.',
|
||||
'missing' => 'The :attribute field must be missing.',
|
||||
'missing_if' => 'The :attribute field must be missing when :other is :value.',
|
||||
'missing_unless' => 'The :attribute field must be missing unless :other is :value.',
|
||||
'missing_with' => 'The :attribute field must be missing when :values is present.',
|
||||
'missing_with_all' => 'The :attribute field must be missing when :values are present.',
|
||||
'multiple_of' => 'The :attribute field must be a multiple of :value.',
|
||||
'not_in' => 'The selected :attribute is invalid.',
|
||||
'not_regex' => 'The :attribute field format is invalid.',
|
||||
'numeric' => 'The :attribute field must be a number.',
|
||||
'password' => [
|
||||
'letters' => 'The :attribute field must contain at least one letter.',
|
||||
'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.',
|
||||
'numbers' => 'The :attribute field must contain at least one number.',
|
||||
'symbols' => 'The :attribute field must contain at least one symbol.',
|
||||
'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.',
|
||||
],
|
||||
'present' => 'The :attribute field must be present.',
|
||||
'present_if' => 'The :attribute field must be present when :other is :value.',
|
||||
'present_unless' => 'The :attribute field must be present unless :other is :value.',
|
||||
'present_with' => 'The :attribute field must be present when :values is present.',
|
||||
'present_with_all' => 'The :attribute field must be present when :values are present.',
|
||||
'prohibited' => 'The :attribute field is prohibited.',
|
||||
'prohibited_if' => 'The :attribute field is prohibited when :other is :value.',
|
||||
'prohibited_if_accepted' => 'The :attribute field is prohibited when :other is accepted.',
|
||||
'prohibited_if_declined' => 'The :attribute field is prohibited when :other is declined.',
|
||||
'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.',
|
||||
'prohibits' => 'The :attribute field prohibits :other from being present.',
|
||||
'regex' => 'The :attribute field format is invalid.',
|
||||
'required' => 'The :attribute field is required.',
|
||||
'required_array_keys' => 'The :attribute field must contain entries for: :values.',
|
||||
'required_if' => 'The :attribute field is required when :other is :value.',
|
||||
'required_if_accepted' => 'The :attribute field is required when :other is accepted.',
|
||||
'required_if_declined' => 'The :attribute field is required when :other is declined.',
|
||||
'required_unless' => 'The :attribute field is required unless :other is in :values.',
|
||||
'required_with' => 'The :attribute field is required when :values is present.',
|
||||
'required_with_all' => 'The :attribute field is required when :values are present.',
|
||||
'required_without' => 'The :attribute field is required when :values is not present.',
|
||||
'required_without_all' => 'The :attribute field is required when none of :values are present.',
|
||||
'same' => 'The :attribute field must match :other.',
|
||||
'size' => [
|
||||
'array' => 'The :attribute field must contain :size items.',
|
||||
'file' => 'The :attribute field must be :size kilobytes.',
|
||||
'numeric' => 'The :attribute field must be :size.',
|
||||
'string' => 'The :attribute field must be :size characters.',
|
||||
],
|
||||
'starts_with' => 'The :attribute field must start with one of the following: :values.',
|
||||
'string' => 'The :attribute field must be a string.',
|
||||
'timezone' => 'The :attribute field must be a valid timezone.',
|
||||
'unique' => 'The :attribute has already been taken.',
|
||||
'uploaded' => 'The :attribute failed to upload.',
|
||||
'uppercase' => 'The :attribute field must be uppercase.',
|
||||
'url' => 'The :attribute field must be a valid URL.',
|
||||
'ulid' => 'The :attribute field must be a valid ULID.',
|
||||
'uuid' => 'The :attribute field must be a valid UUID.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify custom validation messages for attributes using the
|
||||
| convention "attribute.rule" to name the lines. This makes it quick to
|
||||
| specify a specific custom language line for a given attribute rule.
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'custom-message',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used to swap our attribute placeholder
|
||||
| with something more reader friendly such as "E-Mail Address" instead
|
||||
| of "email". This simply helps us make our message more expressive.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
];
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"Dashboard": "Panel de control",
|
||||
"Projects": "Proyectos",
|
||||
"Project": "Proyecto",
|
||||
"Create Project": "Crear proyecto",
|
||||
"Edit Project": "Editar proyecto",
|
||||
"New Project": "Nuevo proyecto",
|
||||
"Delete Project": "Eliminar proyecto",
|
||||
"Project name": "Nombre del proyecto",
|
||||
"Name": "Nombre",
|
||||
"Address": "Dirección",
|
||||
"Status": "Estado",
|
||||
"Progress": "Progreso",
|
||||
"Phases": "Fases",
|
||||
"Phase": "Fase",
|
||||
"Layer": "Capa",
|
||||
"Layers": "Capas",
|
||||
"Features": "Elementos",
|
||||
"Feature": "Elemento",
|
||||
"Create": "Crear",
|
||||
"Edit": "Editar",
|
||||
"Delete": "Eliminar",
|
||||
"Save": "Guardar",
|
||||
"Update": "Actualizar",
|
||||
"Cancel": "Cancelar",
|
||||
"Search": "Buscar",
|
||||
"Filter": "Filtro",
|
||||
"Back": "Volver",
|
||||
"Actions": "Acciones",
|
||||
"Map": "Mapa",
|
||||
"View Map": "Ver mapa",
|
||||
"Manage Layers": "Gestionar capas",
|
||||
"Upload Layer": "Subir capa",
|
||||
"Upload file": "Subir archivo",
|
||||
"Download": "Descargar",
|
||||
"Fullscreen": "Pantalla completa",
|
||||
"Close": "Cerrar",
|
||||
"Are you sure?": "¿Estás seguro?",
|
||||
"No results": "Sin resultados",
|
||||
"Loading": "Cargando",
|
||||
"Active projects": "Proyectos activos",
|
||||
"Total projects": "Proyectos totales",
|
||||
"Total phases": "Fases totales",
|
||||
"Total features": "Elementos totales",
|
||||
"Global progress": "Progreso global",
|
||||
"Recent projects": "Proyectos recientes",
|
||||
"Recent inspections": "Inspecciones recientes",
|
||||
"Planning": "Planificación",
|
||||
"In progress": "En obra",
|
||||
"Paused": "Pausado",
|
||||
"Completed": "Finalizado",
|
||||
"Progress updated": "Progreso actualizado",
|
||||
"Save progress": "Guardar progreso",
|
||||
"Responsible": "Responsable",
|
||||
"Comment": "Comentario",
|
||||
"History": "Historial",
|
||||
"Inspection": "Inspección",
|
||||
"Template": "Plantilla",
|
||||
"Templates": "Plantillas",
|
||||
"Create Template": "Crear plantilla",
|
||||
"Inspection template": "Plantilla de inspección",
|
||||
"Field": "Campo",
|
||||
"Fields": "Campos",
|
||||
"Field name": "Nombre del campo",
|
||||
"Field type": "Tipo de campo",
|
||||
"Required": "Obligatorio",
|
||||
"Options": "Opciones",
|
||||
"Show images on map": "Mostrar imágenes en mapa",
|
||||
"Project files": "Archivos del proyecto",
|
||||
"Media": "Archivos",
|
||||
"Images": "Imágenes",
|
||||
"Documents": "Documentos",
|
||||
"Upload images": "Subir imágenes",
|
||||
"Upload documents": "Subir documentos",
|
||||
"Upload files": "Subir archivos",
|
||||
"Drop files here": "Suelta archivos aquí",
|
||||
"File": "Archivo",
|
||||
"Files": "Archivos",
|
||||
"Category": "Categoría",
|
||||
"Description": "Descripción",
|
||||
"Size": "Tamaño",
|
||||
"Uploaded by": "Subido por",
|
||||
"Uploaded at": "Subido el",
|
||||
"No files yet": "Aún no hay archivos",
|
||||
"Delete file": "Eliminar archivo",
|
||||
"Layer name": "Nombre de capa",
|
||||
"Layer color": "Color de capa",
|
||||
"Import file": "Importar archivo",
|
||||
"Empty layer": "Capa vacía",
|
||||
"Create empty layer": "Crear capa vacía",
|
||||
"Select phase": "Seleccionar fase",
|
||||
"Select project": "Seleccionar proyecto",
|
||||
"Visible layers": "Capas visibles",
|
||||
"Edit layer": "Editar capa",
|
||||
"Delete layer": "Eliminar capa",
|
||||
"Sign in": "Iniciar sesión",
|
||||
"Sign out": "Cerrar sesión",
|
||||
"Register": "Registrarse",
|
||||
"Email": "Correo",
|
||||
"Password": "Contraseña",
|
||||
"Confirm password": "Confirmar contraseña",
|
||||
"Forgot password": "Olvidé mi contraseña",
|
||||
"Remember me": "Recordarme",
|
||||
"Profile": "Perfil",
|
||||
"Update profile": "Actualizar perfil",
|
||||
"Language": "Idioma",
|
||||
"English": "Inglés",
|
||||
"Spanish": "Español",
|
||||
"Select language": "Seleccionar idioma",
|
||||
"Notifications": "Notificaciones",
|
||||
"Permissions": "Permisos",
|
||||
"Administrator": "Administrador",
|
||||
"Users": "Usuarios",
|
||||
"Roles": "Roles",
|
||||
"Click on an element to edit": "Haz clic en un elemento para editarlo",
|
||||
"No templates yet": "No hay plantillas aún",
|
||||
"Create an inspection template": "Crea una plantilla de inspección",
|
||||
"No phases yet": "No hay fases aún",
|
||||
"Start date": "Fecha de inicio",
|
||||
"Estimated end date": "Fecha estimada de fin",
|
||||
"Centered in project": "Centrar en proyecto",
|
||||
"My location": "Mi ubicación",
|
||||
"File type not allowed": "Tipo de archivo no permitido",
|
||||
"Upload failed": "Error al subir",
|
||||
"Conversion failed": "Error de conversión",
|
||||
"All": "Todos",
|
||||
"Latitude": "Latitud",
|
||||
"Longitude": "Longitud",
|
||||
"Register inspection": "Registrar inspección",
|
||||
"Files of element": "Archivos del elemento",
|
||||
"Fases and layers": "Fases y capas",
|
||||
"Elements": "Elementos",
|
||||
"Log Out": "Cerrar sesión",
|
||||
"optional": "opcional",
|
||||
"each": "cada",
|
||||
"Image": "Imagen",
|
||||
"Document": "Documento",
|
||||
"Other": "Otro",
|
||||
"Color": "Color",
|
||||
"Upload": "Subir"
|
||||
}
|
||||
@@ -10,46 +10,46 @@
|
||||
{{-- Stats cards --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="text-sm text-gray-500 uppercase tracking-wide">Proyectos activos</div>
|
||||
<div class="text-sm text-gray-500 uppercase tracking-wide">{{ __('Active projects') }}</div>
|
||||
<div class="text-3xl font-bold mt-1">{{ $stats['active_projects'] }}</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="text-sm text-gray-500 uppercase tracking-wide">Proyectos totales</div>
|
||||
<div class="text-sm text-gray-500 uppercase tracking-wide">{{ __('Total projects') }}</div>
|
||||
<div class="text-3xl font-bold mt-1">{{ $stats['total_projects'] }}</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="text-sm text-gray-500 uppercase tracking-wide">Fases totales</div>
|
||||
<div class="text-sm text-gray-500 uppercase tracking-wide">{{ __('Total phases') }}</div>
|
||||
<div class="text-3xl font-bold mt-1">{{ $stats['total_phases'] }}</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="text-sm text-gray-500 uppercase tracking-wide">Elementos (features)</div>
|
||||
<div class="text-sm text-gray-500 uppercase tracking-wide">{{ __('Total features') }}</div>
|
||||
<div class="text-3xl font-bold mt-1">{{ $stats['total_features'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Barra de progreso global --}}
|
||||
{{-- Global progress bar --}}
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-8">
|
||||
<h3 class="text-lg font-semibold mb-2">Progreso global</h3>
|
||||
<h3 class="text-lg font-semibold mb-2">{{ __('Global progress') }}</h3>
|
||||
<div class="w-full bg-gray-200 rounded-full h-4">
|
||||
<div class="bg-primary h-4 rounded-full transition-all" style="width: {{ $stats['global_progress'] }}%"></div>
|
||||
</div>
|
||||
<p class="text-right text-sm text-gray-500 mt-1">{{ $stats['global_progress'] }}%</p>
|
||||
</div>
|
||||
|
||||
{{-- Proyectos recientes --}}
|
||||
{{-- Recent projects --}}
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-8">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold">Proyectos recientes</h3>
|
||||
<a href="{{ route('projects.list') }}" class="text-sm text-primary hover:underline">Ver todos</a>
|
||||
<h3 class="text-lg font-semibold">{{ __('Recent projects') }}</h3>
|
||||
<a href="{{ route('projects.list') }}" class="text-sm text-primary hover:underline">{{ __('View Map') }}</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Estado</th>
|
||||
<th>Fases</th>
|
||||
<th>Progreso</th>
|
||||
<th>{{ __('Name') }}</th>
|
||||
<th>{{ __('Status') }}</th>
|
||||
<th>{{ __('Phases') }}</th>
|
||||
<th>{{ __('Progress') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -67,13 +67,7 @@
|
||||
default => 'badge-ghost'
|
||||
};
|
||||
@endphp
|
||||
<span class="badge {{ $badgeClass }}">{{ match($project->status) {
|
||||
'planning' => 'Planificación',
|
||||
'in_progress' => 'En obra',
|
||||
'paused' => 'Pausado',
|
||||
'completed' => 'Finalizado',
|
||||
default => $project->status
|
||||
} }}</span>
|
||||
<span class="badge {{ $badgeClass }}">{{ __(ucfirst(str_replace('_', ' ', $project->status))) }}</span>
|
||||
</td>
|
||||
<td>{{ $project->phases_count }}</td>
|
||||
<td>
|
||||
@@ -86,26 +80,26 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ route('projects.map', $project) }}" class="btn btn-xs btn-outline">Mapa</a>
|
||||
<a href="{{ route('projects.map', $project) }}" class="btn btn-xs btn-outline">{{ __('Map') }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="5" class="text-center text-gray-400 py-4">No hay proyectos aún</td></tr>
|
||||
<tr><td colspan="5" class="text-center text-gray-400 py-4">{{ __('No results') }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Inspecciones recientes --}}
|
||||
{{-- Recent inspections --}}
|
||||
@if($recentInspections->isNotEmpty())
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">Inspecciones recientes</h3>
|
||||
<h3 class="text-lg font-semibold mb-4">{{ __('Recent inspections') }}</h3>
|
||||
<div class="space-y-2">
|
||||
@foreach($recentInspections as $inspection)
|
||||
<div class="border rounded p-3 flex justify-between items-center">
|
||||
<div>
|
||||
<span class="font-medium">{{ $inspection->template?->name ?? 'Inspección' }}</span>
|
||||
<span class="font-medium">{{ $inspection->template?->name ?? __('Inspection') }}</span>
|
||||
<span class="text-sm text-gray-500 ml-2">{{ $inspection->feature?->name }}</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400">{{ $inspection->created_at->diffForHumans() }}</span>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="flex items-center gap-1">
|
||||
@foreach(['en' => '🇬🇧 EN', 'es' => '🇪🇸 ES'] as $code => $label)
|
||||
<button wire:click="switchLanguage('{{ $code }}')"
|
||||
class="btn btn-xs {{ $currentLocale === $code ? 'btn-primary' : 'btn-ghost' }}"
|
||||
title="{{ __('Language') }}: {{ __($code === 'en' ? 'English' : 'Spanish') }}">
|
||||
{{ $label }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Subir capa</h2>
|
||||
<h2 class="card-title">{{ __("Upload Layer") }}</h2>
|
||||
|
||||
<form wire:submit.prevent="upload" class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label">Proyecto</label>
|
||||
<label class="label">{{ __("Project") }}</label>
|
||||
<select wire:model.live="projectId" class="select select-bordered" required>
|
||||
<option value="">Seleccionar proyecto...</option>
|
||||
<option value="">{{ __("Select project") }}...</option>
|
||||
@foreach($projects as $p)
|
||||
<option value="{{ $p->id }}">{{ $p->name }}</option>
|
||||
@endforeach
|
||||
@@ -22,9 +22,9 @@
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">Fase</label>
|
||||
<label class="label">{{ __("Phase") }}</label>
|
||||
<select wire:model.live="phaseId" class="select select-bordered" required @if(!$projectId) disabled @endif>
|
||||
<option value="">Seleccionar fase...</option>
|
||||
<option value="">{{ __("Select phase") }}...</option>
|
||||
@foreach($phases as $ph)
|
||||
<option value="{{ $ph->id }}">{{ $ph->name }}</option>
|
||||
@endforeach
|
||||
@@ -32,24 +32,24 @@
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">Nombre de la capa</label>
|
||||
<label class="label">{{ __("Layer name") }}</label>
|
||||
<input type="text" wire:model="layerName" class="input input-bordered" placeholder="Ej: Cimentación" required />
|
||||
@error('layerName') <span class="text-error text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">Color</label>
|
||||
<input type="color" wire:model="layerColor" class="input input-bordered w-20" />
|
||||
<label class="label">{{ __("Color") }}</label>
|
||||
<input type="color" wire:model="layer{{ __("Color") }}" class="input input-bordered w-20" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">Archivo (GeoJSON, KML, KMZ, Shapefile .zip, DWG)</label>
|
||||
<label class="label">{{ __("File") }} (GeoJSON, KML, KMZ, Shapefile .zip, DWG)</label>
|
||||
<input type="file" wire:model="uploadFile" class="file-input file-input-bordered" accept=".geojson,.kml,.kmz,.zip,.shp,.dwg" />
|
||||
@error('uploadFile') <span class="text-error text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-full">
|
||||
Subir capa
|
||||
{{ __("Upload Layer") }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div class="flex flex-col h-screen">
|
||||
{{-- Cabecera fija --}}
|
||||
<div class="flex justify-between items-center mb-4 px-4 pt-4 flex-shrink-0">
|
||||
<h1 class="text-2xl font-bold">Gestión de elementos - {{ $phase->name }}</h1>
|
||||
<a href="{{ route('projects.map', $project) }}" class="btn btn-outline btn-sm">← Volver al mapa</a>
|
||||
<h1 class="text-2xl font-bold">{{ __("Manage Layers") }} - {{ $phase->name }}</h1>
|
||||
<a href="{{ route('projects.map', $project) }}" class="btn btn-outline btn-sm">← {{ __("Back") }}</a>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-hidden px-4 pb-4">
|
||||
@@ -11,10 +11,10 @@
|
||||
<div class="space-y-4 overflow-y-auto h-full pr-2">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Importar archivo</h2>
|
||||
<h2 class="card-title">{{ __("Import file") }}</h2>
|
||||
<form wire:submit.prevent="importFile">
|
||||
<div class="form-control">
|
||||
<label class="label">Nombre de capa</label>
|
||||
<label class="label">{{ __("Layer name") }}</label>
|
||||
<input type="text" wire:model="layerName" class="input input-bordered" required>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
@@ -26,15 +26,15 @@
|
||||
<input type="file" wire:model="uploadFile" class="file-input file-input-bordered">
|
||||
@error('uploadFile') <span class="text-error">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-full mt-2">Subir y convertir</button>
|
||||
<button type="submit" class="btn btn-primary w-full mt-2">{{ __("Upload") }}</button>
|
||||
</form>
|
||||
<div class="divider"></div>
|
||||
<button wire:click="createEmptyLayer" class="btn btn-secondary w-full">Crear capa vacía</button>
|
||||
<button wire:click="createEmptyLayer" class="btn btn-secondary w-full">{{ __("Create empty layer") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Capas existentes</h2>
|
||||
<h2 class="card-title">{{ __("Layers") }}</h2>
|
||||
<div class="space-y-2 max-h-96 overflow-y-auto">
|
||||
@foreach($layers as $layer)
|
||||
<div wire:key="layer-{{ $layer->id }}" class="flex justify-between items-center p-2 border rounded">
|
||||
@@ -50,12 +50,12 @@
|
||||
</div>
|
||||
<div>
|
||||
<button wire:click="selectLayer({{ $layer->id }})" class="btn btn-xs btn-info">✏️ Editar</button>
|
||||
<button wire:click="deleteLayer({{ $layer->id }})" class="btn btn-xs btn-error" onclick="return confirm('¿Eliminar capa?')">🗑️</button>
|
||||
<button wire:click="deleteLayer({{ $layer->id }})" class="btn btn-xs btn-error" onclick="return confirm('¿{{ __("Delete layer") }}?')">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@if($layers->isEmpty())
|
||||
<p class="text-center">Sin capas. Crea una o importa.</p>
|
||||
<p class="text-center">{{ __("No results") }}. Crea una o importa.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,7 +66,7 @@
|
||||
<div class="lg:col-span-2 flex flex-col h-full">
|
||||
<div class="card bg-base-100 shadow-xl flex-1 flex flex-col">
|
||||
<div class="card-body flex-1 flex flex-col p-2">
|
||||
<h2 class="card-title">Editor gráfico</h2>
|
||||
<h2 class="card-title">{{ __("Edit") }}</h2>
|
||||
@if($selectedLayer)
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button id="saveDrawingBtn" class="btn btn-primary">💾 Guardar cambios</button>
|
||||
|
||||
@@ -44,8 +44,13 @@ new class extends Component
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<!-- Language Switcher -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
@livewire('language-switcher')
|
||||
</div>
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-2">
|
||||
<x-dropdown align="right" width="48">
|
||||
<x-slot name="trigger">
|
||||
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
|
||||
|
||||
@@ -9,31 +9,31 @@
|
||||
{{-- Subida --}}
|
||||
<div class="card bg-base-100 shadow-xl mb-4">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm">Subir archivos</h3>
|
||||
<h3 class="card-title text-sm">{{ __("Upload files") }}</h3>
|
||||
|
||||
<form wire:submit.prevent="upload" class="space-y-3">
|
||||
<div class="form-control">
|
||||
<label class="label-text">Archivos (hasta 100MB c/u)</label>
|
||||
<label class="label-text">{{ __("Upload files") }} (100MB {{ __("each") }})</label>
|
||||
<input type="file" wire:model="uploadFiles" multiple class="file-input file-input-bordered file-input-sm" />
|
||||
@error('uploadFiles.*') <span class="text-error text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="form-control">
|
||||
<label class="label-text">Categoría</label>
|
||||
<label class="label-text">{{ __("Category") }}</label>
|
||||
<select wire:model="uploadCategory" class="select select-bordered select-sm">
|
||||
<option value="image">Imagen</option>
|
||||
<option value="document">Documento</option>
|
||||
<option value="other">Otro</option>
|
||||
<option value="image">{{ __("Image") }}</option>
|
||||
<option value="document">{{ __("Document") }}</option>
|
||||
<option value="other">{{ __("Other") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label-text">Descripción</label>
|
||||
<label class="label-text">{{ __("Description") }}</label>
|
||||
<input type="text" wire:model="uploadDescription" class="input input-bordered input-sm" placeholder="Opcional" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-sm w-full">Subir archivos</button>
|
||||
<button type="submit" class="btn btn-primary btn-sm w-full">{{ __("Upload files") }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
@if($images->isNotEmpty())
|
||||
<div class="card bg-base-100 shadow-xl mb-4">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm">Imágenes ({{ $images->count() }})</h3>
|
||||
<h3 class="card-title text-sm">{{ __("Images") }} ({{ $images->count() }})</h3>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
@foreach($images as $media)
|
||||
<div class="relative group cursor-pointer" wire:click="viewMedia({{ $media->id }})">
|
||||
@@ -61,11 +61,11 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Documentos --}}
|
||||
{{-- {{ __("Document") }}s --}}
|
||||
@if($documents->isNotEmpty())
|
||||
<div class="card bg-base-100 shadow-xl mb-4">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm">Documentos ({{ $documents->count() }})</h3>
|
||||
<h3 class="card-title text-sm">{{ __("Document") }}s ({{ $documents->count() }})</h3>
|
||||
<div class="space-y-1">
|
||||
@foreach($documents as $media)
|
||||
<div class="flex items-center gap-2 p-2 border rounded text-sm hover:bg-base-200">
|
||||
@@ -95,7 +95,7 @@
|
||||
@if($mediaItems->isEmpty())
|
||||
<div class="text-center text-gray-400 py-6 text-sm">
|
||||
<p class="text-2xl mb-2">📁</p>
|
||||
<p>No hay archivos. Sube imágenes o documentos.</p>
|
||||
<p>{{ __("No files yet") }}. Sube imágenes o documentos.</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Progreso de fase: {{ $phase->name }}</h2>
|
||||
<h2 class="card-title">{{ __('Progress') }}: {{ $phase->name }}</h2>
|
||||
<p class="text-sm opacity-70">{{ $phase->project->name ?? '' }}</p>
|
||||
|
||||
<div class="mt-4">
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<form wire:submit.prevent="updateProgressManual" class="mt-6 space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label">Nuevo porcentaje de progreso</label>
|
||||
<label class="label">{{ __('Progress updated') }}</label>
|
||||
<input type="range" min="0" max="100" wire:model.live="progress" class="range range-primary" />
|
||||
<div class="flex justify-between text-xs px-2">
|
||||
<span>0%</span><span>25%</span><span>50%</span><span>75%</span><span>100%</span>
|
||||
@@ -26,16 +26,16 @@
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">Comentario (opcional)</label>
|
||||
<textarea wire:model="comment" rows="3" class="textarea textarea-bordered" placeholder="Notas sobre el progreso..."></textarea>
|
||||
<label class="label">{{ __('Comment') }} ({{ __('optional') }})</label>
|
||||
<textarea wire:model="comment" rows="3" class="textarea textarea-bordered" placeholder="{{ __('Comment') }}..."></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-full">Actualizar progreso</button>
|
||||
<button type="submit" class="btn btn-primary w-full">{{ __('Save progress') }}</button>
|
||||
</form>
|
||||
|
||||
@if($phase->progressUpdates->count() > 0)
|
||||
<div class="mt-6">
|
||||
<h3 class="font-semibold mb-2">Historial</h3>
|
||||
<h3 class="font-semibold mb-2">{{ __('History') }}</h3>
|
||||
<div class="space-y-2">
|
||||
@foreach($phase->progressUpdates()->latest()->take(10)->get() as $update)
|
||||
<div class="border rounded p-2 text-sm">
|
||||
@@ -55,6 +55,6 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{{ url()->previous() }}" class="btn btn-outline btn-sm">← Volver</a>
|
||||
<a href="{{ url()->previous() }}" class="btn btn-outline btn-sm">← {{ __('Back') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<div>
|
||||
<div class="flex justify-between mb-4">
|
||||
<input type="text" wire:model.live="search" placeholder="Buscar proyecto..." class="input input-bordered w-64" />
|
||||
<input type="text" wire:model.live="search" placeholder="{{ __('Search') }}..." class="input input-bordered w-64" />
|
||||
<select wire:model.live="statusFilter" class="select select-bordered">
|
||||
<option value="">Todos</option>
|
||||
<option value="planning">Planificación</option>
|
||||
<option value="in_progress">En obra</option>
|
||||
<option value="paused">Pausado</option>
|
||||
<option value="completed">Finalizado</option>
|
||||
<option value="">{{ __('All') }}</option>
|
||||
<option value="planning">{{ __('Planning') }}</option>
|
||||
<option value="in_progress">{{ __('In progress') }}</option>
|
||||
<option value="paused">{{ __('Paused') }}</option>
|
||||
<option value="completed">{{ __('Completed') }}</option>
|
||||
</select>
|
||||
@can('create projects')
|
||||
<a href="{{ route('projects.create') }}" class="btn btn-primary">+ Nuevo Proyecto</a>
|
||||
<a href="{{ route('projects.create') }}" class="btn btn-primary">+ {{ __('New Project') }}</a>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr><th>Nombre</th><th>Dirección</th><th>Estado</th><th>Progreso global</th><th>Acciones</th></tr>
|
||||
<tr><th>{{ __('Name') }}</th><th>{{ __('Address') }}</th><th>{{ __('Status') }}</th><th>{{ __('Progress') }}</th><th>{{ __('Actions') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($projects as $project)
|
||||
<tr>
|
||||
<td>{{ $project->name }}</td>
|
||||
<td>{{ $project->address }}</td>
|
||||
<td>{{ ucfirst($project->status) }}</td>
|
||||
<td>{{ __(ucfirst(str_replace('_', ' ', $project->status))) }}</td>
|
||||
<td>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div class="bg-primary h-2.5 rounded-full" style="width: {{ $project->phases->avg('progress_percent') }}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ route('projects.map', $project) }}" class="btn btn-sm btn-outline">Ver Mapa</a>
|
||||
<a href="{{ route('projects.map', $project) }}" class="btn btn-sm btn-outline">{{ __('Map') }}</a>
|
||||
@can('edit projects')
|
||||
<a href="{{ route('projects.edit', $project) }}" class="btn btn-sm btn-warning">Editar</a>
|
||||
<a href="{{ route('projects.edit', $project) }}" class="btn btn-sm btn-warning">{{ __('Edit') }}</a>
|
||||
@endcan
|
||||
</td>
|
||||
</tr>
|
||||
@@ -41,4 +41,4 @@
|
||||
</table>
|
||||
</div>
|
||||
{{ $projects->links() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<!-- Panel lateral de capas -->
|
||||
<div class="absolute top-2 right-2 z-[1000] bg-base-100 rounded-box shadow-xl p-4 w-72 border border-base-300 text-sm max-h-[calc(100vh-4rem)] overflow-y-auto">
|
||||
<h3 class="font-semibold text-base mb-2">Fases y capas</h3>
|
||||
<h3 class="font-semibold text-base mb-2">{{ __("Fases and layers") }}</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach($phases as $phase)
|
||||
<div class="border rounded-lg p-2 {{ in_array($phase->id, $activeLayers) ? 'bg-base-200' : '' }}">
|
||||
@@ -36,10 +36,10 @@
|
||||
{{-- Botón para ir a gestión de capas de esta fase --}}
|
||||
<div class="mt-1 ml-7">
|
||||
<a href="{{ route('layers.manage', [$project, $phase]) }}" class="btn btn-xs btn-outline btn-primary">
|
||||
✏️ Gestionar capas
|
||||
✏️ {{ __("Manage Layers") }}
|
||||
</a>
|
||||
<a href="{{ route('phases.progress', $phase) }}" class="btn btn-xs btn-outline">
|
||||
📊 Progreso
|
||||
📊 {{ __("Progress") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,7 +50,7 @@
|
||||
<div class="mt-3">
|
||||
<label class="flex items-center gap-2 text-xs cursor-pointer">
|
||||
<input type="checkbox" wire:change="toggleFeatureImages" @if($showFeatureImages) checked @endif class="checkbox checkbox-xs checkbox-primary" />
|
||||
🖼️ Mostrar imágenes en mapa
|
||||
🖼️ {{ __("Show images on map") }}
|
||||
@if($featureImageMarkers)
|
||||
<span class="badge badge-xs">{{ count($featureImageMarkers) }}</span>
|
||||
@endif
|
||||
@@ -60,24 +60,24 @@
|
||||
{{-- Botones generales --}}
|
||||
<div class="mt-2 space-y-1">
|
||||
<a href="{{ route('projects.media', $project) }}" class="btn btn-sm btn-outline w-full">
|
||||
📁 Archivos del proyecto
|
||||
📁 {{ __("Project files") }}
|
||||
</a>
|
||||
<button wire:click="$dispatch('centerMap')" class="btn btn-sm btn-outline w-full">
|
||||
📍 Centrar mapa
|
||||
📍 {{ __("Centered in project") }}
|
||||
</button>
|
||||
<button onclick="getUserLocation()" class="btn btn-sm btn-outline w-full">
|
||||
🧭 Mi ubicación
|
||||
🧭 {{ __("My location") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columna derecha: Editor de progreso / inspecciones -->
|
||||
<!-- Columna derecha: {{ __("Edit") }} de progreso / inspecciones -->
|
||||
<div class="w-full lg:w-1/3 transition-all duration-300" :class="{'lg:w-full': formFullscreen}">
|
||||
<div class="card bg-base-100 shadow-xl h-full flex flex-col">
|
||||
<div class="card-body overflow-y-auto flex-1">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h2 class="card-title">Editor</h2>
|
||||
<h2 class="card-title">{{ __("Edit") }}</h2>
|
||||
<button wire:click="toggleFullscreen" class="btn btn-circle btn-sm" title="Pantalla completa">
|
||||
<span x-text="formFullscreen ? '✕' : '⤢'"></span>
|
||||
</button>
|
||||
@@ -91,9 +91,9 @@
|
||||
<p class="text-xs text-gray-500">Capa: {{ $selectedFeature->layer?->name ?? '—' }}</p>
|
||||
</div>
|
||||
|
||||
{{-- Progreso --}}
|
||||
{{-- {{ __("Progress") }} --}}
|
||||
<div class="form-control mb-3">
|
||||
<label class="label-text">Progreso: {{ $editProgress }}%</label>
|
||||
<label class="label-text">{{ __("Progress") }}: {{ $editProgress }}%</label>
|
||||
<input type="range" min="0" max="100" wire:model.live="editProgress" class="range range-primary range-sm" />
|
||||
<div class="flex justify-between text-xs">
|
||||
<span>0%</span><span>50%</span><span>100%</span>
|
||||
@@ -101,18 +101,18 @@
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label-text">Responsable</label>
|
||||
<label class="label-text">{{ __("Responsible") }}</label>
|
||||
<input type="text" wire:model="editResponsible" class="input input-bordered input-sm" placeholder="Nombre" />
|
||||
</div>
|
||||
|
||||
<button wire:click="saveFeatureProgress" class="btn btn-primary btn-sm w-full mb-3">
|
||||
💾 Guardar progreso
|
||||
💾 {{ __("Save progress") }}
|
||||
</button>
|
||||
|
||||
{{-- Gestor de archivos del feature --}}
|
||||
<details class="mb-3 border rounded-lg">
|
||||
<summary class="text-xs font-semibold cursor-pointer p-2 bg-base-200 rounded-t-lg">
|
||||
📎 Archivos del elemento
|
||||
📎 {{ __("Files of element") }}
|
||||
</summary>
|
||||
<div class="p-2">
|
||||
@livewire('media-manager', [
|
||||
@@ -124,7 +124,7 @@
|
||||
|
||||
{{-- Templates / Inspecciones --}}
|
||||
@if($templates->isNotEmpty())
|
||||
<div class="divider text-xs">Inspección</div>
|
||||
<div class="divider text-xs">{{ __("Inspection") }}</div>
|
||||
<div class="form-control mb-2">
|
||||
<label class="label-text">Plantilla</label>
|
||||
<select wire:model.live="selectedTemplateId" wire:change="onTemplateChange" class="select select-bordered select-sm">
|
||||
@@ -168,18 +168,18 @@
|
||||
@endswitch
|
||||
</div>
|
||||
@endforeach
|
||||
<button wire:click="saveInspection" class="btn btn-primary btn-xs w-full mt-1">Registrar inspección</button>
|
||||
<button wire:click="saveInspection" class="btn btn-primary btn-xs w-full mt-1">{{ __("Register inspection") }}</button>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Historial de inspecciones --}}
|
||||
{{-- {{ __("History") }} de inspecciones --}}
|
||||
@if($inspectionHistory->isNotEmpty())
|
||||
<div class="divider text-xs">Historial</div>
|
||||
<div class="divider text-xs">{{ __("History") }}</div>
|
||||
<div class="space-y-1 max-h-40 overflow-y-auto">
|
||||
@foreach($inspectionHistory as $ins)
|
||||
<div class="border rounded p-2 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">{{ $ins->template?->name ?? 'Inspección' }}</span>
|
||||
<span class="font-medium">{{ $ins->template?->name ?? '{{ __("Inspection") }}' }}</span>
|
||||
<span class="text-gray-400">{{ $ins->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
@if($ins->user)<span class="text-gray-500">por {{ $ins->user->name }}</span>@endif
|
||||
@@ -193,10 +193,10 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">Sin plantillas</h3>
|
||||
<div class="text-xs">Crea una plantilla de inspección para este proyecto.</div>
|
||||
<h3 class="font-bold">{{ __("No templates yet") }}</h3>
|
||||
<div class="text-xs">{{ __("Create an inspection template") }}.</div>
|
||||
</div>
|
||||
<a href="{{ route('projects.templates', $project) }}" class="btn btn-primary btn-sm">Crear</a>
|
||||
<a href="{{ route('projects.templates', $project) }}" class="btn btn-primary btn-sm">{{ __("Create") }}</a>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
@@ -260,8 +260,8 @@
|
||||
const props = feature.properties || {};
|
||||
const featId = props._feature_id || feature.id;
|
||||
let content = `<b>${props.name || 'Elemento'}</b><br>
|
||||
Progreso: ${props.progress || 0}%<br>
|
||||
Responsable: ${props.responsible || '-'}<br>
|
||||
{{ __("Progress") }}: ${props.progress || 0}%<br>
|
||||
{{ __("Responsible") }}: ${props.responsible || '-'}<br>
|
||||
<button class="btn btn-xs btn-primary mt-1" onclick="selectFeature(${featId})">✏️ Editar</button>`;
|
||||
layer.bindPopup(content);
|
||||
layer.on('click', function() { selectFeature(featId); });
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
<x-app-layout>
|
||||
<div class="max-w-2xl mx-auto p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Crear Nuevo Proyecto</h1>
|
||||
<h1 class="text-2xl font-bold mb-4">{{ __('Create Project') }}</h1>
|
||||
<form action="{{ route('projects.store') }}" method="POST" class="space-y-4">
|
||||
@csrf
|
||||
<div>
|
||||
<label class="label">Nombre del proyecto</label>
|
||||
<label class="label">{{ __('Project name') }}</label>
|
||||
<input type="text" name="name" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Dirección</label>
|
||||
<label class="label">{{ __('Address') }}</label>
|
||||
<input type="text" name="address" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">Latitud</label>
|
||||
<label class="label">{{ __('Latitude') }}</label>
|
||||
<input type="number" step="any" name="lat" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Longitud</label>
|
||||
<label class="label">{{ __('Longitude') }}</label>
|
||||
<input type="number" step="any" name="lng" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">Fecha inicio</label>
|
||||
<label class="label">{{ __('Start date') }}</label>
|
||||
<input type="date" name="start_date" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Fecha estimada fin</label>
|
||||
<label class="label">{{ __('Estimated end date') }}</label>
|
||||
<input type="date" name="end_date_estimated" class="input input-bordered w-full">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-full">Crear Proyecto</button>
|
||||
<button type="submit" class="btn btn-primary w-full">{{ __('Create') }} {{ __('Project') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
<x-app-layout>
|
||||
<div class="max-w-2xl mx-auto p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Editar Proyecto: {{ $project->name }}</h1>
|
||||
<h1 class="text-2xl font-bold mb-4">{{ __('Edit Project') }}: {{ $project->name }}</h1>
|
||||
<form action="{{ route('projects.update', $project) }}" method="POST" class="space-y-4">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<div>
|
||||
<label class="label">Nombre</label>
|
||||
<label class="label">{{ __('Name') }}</label>
|
||||
<input type="text" name="name" value="{{ old('name', $project->name) }}" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Dirección</label>
|
||||
<label class="label">{{ __('Address') }}</label>
|
||||
<input type="text" name="address" value="{{ old('address', $project->address) }}" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">Latitud</label>
|
||||
<label class="label">{{ __('Latitude') }}</label>
|
||||
<input type="number" step="any" name="lat" value="{{ old('lat', $project->lat) }}" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Longitud</label>
|
||||
<label class="label">{{ __('Longitude') }}</label>
|
||||
<input type="number" step="any" name="lng" value="{{ old('lng', $project->lng) }}" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Estado</label>
|
||||
<label class="label">{{ __('Status') }}</label>
|
||||
<select name="status" class="select select-bordered w-full">
|
||||
<option value="planning" @selected($project->status == 'planning')>Planificación</option>
|
||||
<option value="in_progress" @selected($project->status == 'in_progress')>En obra</option>
|
||||
<option value="paused" @selected($project->status == 'paused')>Pausado</option>
|
||||
<option value="completed" @selected($project->status == 'completed')>Finalizado</option>
|
||||
<option value="planning" @selected($project->status == 'planning')>{{ __('Planning') }}</option>
|
||||
<option value="in_progress" @selected($project->status == 'in_progress')>{{ __('In progress') }}</option>
|
||||
<option value="paused" @selected($project->status == 'paused')>{{ __('Paused') }}</option>
|
||||
<option value="completed" @selected($project->status == 'completed')>{{ __('Completed') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">Fecha inicio</label>
|
||||
<label class="label">{{ __('Start date') }}</label>
|
||||
<input type="date" name="start_date" value="{{ old('start_date', $project->start_date->format('Y-m-d')) }}" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Fecha fin estimada</label>
|
||||
<label class="label">{{ __('Estimated end date') }}</label>
|
||||
<input type="date" name="end_date_estimated" value="{{ old('end_date_estimated', $project->end_date_estimated?->format('Y-m-d')) }}" class="input input-bordered w-full">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-full">Actualizar Proyecto</button>
|
||||
<button type="submit" class="btn btn-primary w-full">{{ __('Update') }}</button>
|
||||
</form>
|
||||
|
||||
<hr class="my-6">
|
||||
|
||||
<h2 class="text-xl font-bold mb-2">Fases del proyecto</h2>
|
||||
<h2 class="text-xl font-bold mb-2">{{ __('Phases') }}</h2>
|
||||
<livewire:phase-list :project="$project" />
|
||||
</div>
|
||||
</x-app-layout>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<x-app-layout>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Mis Proyectos</h1>
|
||||
<h1 class="text-2xl font-bold">{{ __('Projects') }}</h1>
|
||||
@can('create projects')
|
||||
<a href="{{ route('projects.create') }}" class="btn btn-primary">+ Nuevo Proyecto</a>
|
||||
<a href="{{ route('projects.create') }}" class="btn btn-primary">+ {{ __('New Project') }}</a>
|
||||
@endcan
|
||||
</div>
|
||||
<livewire:project-list />
|
||||
</div>
|
||||
</x-app-layout>
|
||||
</x-app-layout>
|
||||
|
||||
Reference in New Issue
Block a user