Sistema multilingüe EN/ES: middleware SetLocale, LanguageSwitcher, campo locale en users, traducciones en dashboard/mapa/proyectos/gestores

This commit is contained in:
2026-05-09 23:14:48 +02:00
parent 7bf5a90a24
commit 3e8b6f1eb3
22 changed files with 798 additions and 131 deletions
@@ -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>
+10 -10
View File
@@ -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); });