feat: i18n, language switcher fix, DataTable improvements, blade translations
- Translation system: lang/es/ PHP files (auth, validation, pagination, passwords)
- Rappasoft vendor translations published (lang/vendor/livewire-tables/es/)
- JSON files synced to 391 keys (EN + ES, full parity)
- APP_LOCALE changed to 'es', users.locale column default changed to 'es'
- Language switcher fixed: JS event + window.location.reload() avoids /livewire/update redirect
- SetLocale middleware fallback uses config('app.locale') instead of hardcoded 'en'
- setSortingPillsEnabled(false) on ProjectTable, CompanyTable, UserTable
- Translated 17 blade views: project-map, template-manager, layer-manager,
company-management, phase-list, media-manager, reports-dashboard,
client-projects, layer-upload, project-form, project-map-editor-tab,
admin/users, projects/media, projects/templates, layouts/client
- Navigation 'Empresas' link uses __('Companies')
- Fixed typo key 'Fases and layers' -> 'Phases and layers'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
<div>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ route('admin.users') }}" class="btn btn-ghost btn-sm px-2" wire:navigate>
|
||||
<x-heroicon-o-arrow-left class="w-4 h-4" />
|
||||
</a>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ $user ? 'Editar usuario: ' . $user->name : 'Nuevo usuario' }}
|
||||
</h2>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8">
|
||||
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
|
||||
|
||||
@if(session('notify'))
|
||||
<div class="alert alert-success mb-4">
|
||||
<x-heroicon-o-check-circle class="w-5 h-5" />
|
||||
{{ session('notify') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-8">
|
||||
|
||||
<form wire:submit.prevent="save">
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-error text-sm mb-6">
|
||||
<x-heroicon-o-exclamation-circle class="w-5 h-5 shrink-0" />
|
||||
<ul class="list-disc pl-3 space-y-0.5">
|
||||
@foreach($errors->all() as $e) <li>{{ $e }}</li> @endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════
|
||||
1. INFORMACIÓN PERSONAL
|
||||
══════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Información personal
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Título de cortesía
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<select wire:model="title" class="select select-bordered w-full max-w-xs">
|
||||
<option value="">— Sin título —</option>
|
||||
<option value="Sr.">Sr.</option>
|
||||
<option value="Sra.">Sra.</option>
|
||||
<option value="Dr.">Dr.</option>
|
||||
<option value="Dra.">Dra.</option>
|
||||
<option value="Ing.">Ing.</option>
|
||||
<option value="Arq.">Arq.</option>
|
||||
<option value="Prof.">Prof.</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Apellidos <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<input type="text" wire:model="lastName"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="García López" />
|
||||
@error('lastName') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Nombre <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<input type="text" wire:model="firstName"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Ana" />
|
||||
@error('firstName') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════
|
||||
2. VALIDACIÓN
|
||||
══════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Validación de acceso
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
|
||||
{{-- Intervalo de fechas --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Válido desde / hasta
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">Vacío = sin límite</p>
|
||||
</label>
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<input type="date" wire:model="validFrom"
|
||||
class="input input-bordered flex-1" />
|
||||
<span class="text-gray-400 shrink-0">→</span>
|
||||
<input type="date" wire:model="validUntil"
|
||||
class="input input-bordered flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
@error('validFrom') <div class="flex gap-4"><div class="w-48 shrink-0"></div><p class="text-error text-xs flex-1">{{ $message }}</p></div> @enderror
|
||||
@error('validUntil') <div class="flex gap-4"><div class="w-48 shrink-0"></div><p class="text-error text-xs flex-1">{{ $message }}</p></div> @enderror
|
||||
|
||||
{{-- Contraseña con generador --}}
|
||||
<div class="flex items-start gap-4"
|
||||
x-data="{
|
||||
show: false,
|
||||
generate() {
|
||||
const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||||
const lower = 'abcdefghjkmnpqrstuvwxyz';
|
||||
const digits = '23456789';
|
||||
const symbols = '!@#$%&*';
|
||||
const all = upper + lower + digits + symbols;
|
||||
let pwd = upper[Math.floor(Math.random()*upper.length)]
|
||||
+ lower[Math.floor(Math.random()*lower.length)]
|
||||
+ digits[Math.floor(Math.random()*digits.length)]
|
||||
+ symbols[Math.floor(Math.random()*symbols.length)];
|
||||
for (let i = 4; i < 12; i++) {
|
||||
pwd += all[Math.floor(Math.random()*all.length)];
|
||||
}
|
||||
pwd = pwd.split('').sort(() => Math.random()-0.5).join('');
|
||||
$wire.set('formPassword', pwd);
|
||||
this.show = true;
|
||||
}
|
||||
}">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Contraseña
|
||||
@if(!$user) <span class="text-error">*</span> @endif
|
||||
@if($user)
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">Vacío = no cambiar</p>
|
||||
@endif
|
||||
</label>
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<label class="input input-bordered flex items-center gap-2 flex-1">
|
||||
<x-heroicon-o-lock-closed class="w-4 h-4 opacity-40 shrink-0" />
|
||||
<input wire:model="formPassword" class="grow"
|
||||
:type="show ? 'text' : 'password'"
|
||||
placeholder="{{ $user ? '••••••••' : 'Mínimo 8 caracteres' }}" />
|
||||
<button type="button" @click="show = !show"
|
||||
class="opacity-50 hover:opacity-100 transition-opacity">
|
||||
<template x-if="show">
|
||||
<x-heroicon-o-eye-slash class="w-4 h-4" />
|
||||
</template>
|
||||
<template x-if="!show">
|
||||
<x-heroicon-o-eye class="w-4 h-4" />
|
||||
</template>
|
||||
</button>
|
||||
</label>
|
||||
<button type="button" @click="generate()"
|
||||
class="btn btn-outline btn-sm gap-1 shrink-0"
|
||||
title="Generar contraseña aleatoria">
|
||||
<x-heroicon-o-arrow-path class="w-4 h-4" />
|
||||
Generar
|
||||
</button>
|
||||
</div>
|
||||
@if(!$user)
|
||||
<p class="text-xs text-gray-400">Mayúsculas, minúsculas, números y símbolo.</p>
|
||||
@endif
|
||||
@error('formPassword') <p class="text-error text-xs">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Estado --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Estado <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<select wire:model="userStatus" class="select select-bordered w-full max-w-xs">
|
||||
<option value="active">Activo</option>
|
||||
<option value="inactive">Inactivo</option>
|
||||
<option value="suspended">Suspendido</option>
|
||||
</select>
|
||||
@error('userStatus') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════
|
||||
3. CONTACTO
|
||||
══════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Contacto
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
|
||||
{{-- Empresa --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Empresa <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<select wire:model.live="companyId" class="select select-bordered w-full">
|
||||
<option value="">— Seleccionar empresa —</option>
|
||||
@foreach($companies as $company)
|
||||
<option value="{{ $company->id }}">
|
||||
{{ $company->apodo ?: $company->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('companyId') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Dirección --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Dirección
|
||||
</label>
|
||||
<div class="flex-1 space-y-1.5">
|
||||
<textarea wire:model="address" rows="2"
|
||||
class="textarea textarea-bordered w-full"
|
||||
placeholder="Calle, número, ciudad, CP, país"></textarea>
|
||||
@if($companyId)
|
||||
<button type="button" wire:click="copyCompanyAddress"
|
||||
class="btn btn-xs btn-outline gap-1">
|
||||
<x-heroicon-o-document-duplicate class="w-3.5 h-3.5" />
|
||||
Copiar dirección de la empresa
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Teléfono --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Teléfono
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<x-heroicon-o-phone class="w-4 h-4 opacity-40 shrink-0" />
|
||||
<input type="tel" wire:model="phone" class="grow"
|
||||
placeholder="+34 600 123 456" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Email --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Email <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<x-heroicon-o-envelope class="w-4 h-4 opacity-40 shrink-0" />
|
||||
<input type="email" wire:model="email" class="grow"
|
||||
placeholder="ana@empresa.com" />
|
||||
</label>
|
||||
@error('email') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════
|
||||
4. PERMISOS
|
||||
══════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Permisos
|
||||
</h3>
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Rol <span class="text-error">*</span>
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">Define los permisos del usuario</p>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<select wire:model="formRole" class="select select-bordered w-full max-w-xs">
|
||||
@foreach($roles as $role)
|
||||
<option value="{{ $role->name }}">{{ $role->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('formRole') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════
|
||||
5. NOTAS
|
||||
══════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Notas internas
|
||||
</h3>
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Notas
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">Solo visible para administradores</p>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<textarea wire:model="notes" rows="4"
|
||||
class="textarea textarea-bordered w-full"
|
||||
placeholder="Observaciones, historial, información relevante…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Botones ───────────────────────────────────────────── --}}
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<a href="{{ route('admin.users') }}" class="btn btn-outline gap-1" wire:navigate>
|
||||
<x-heroicon-o-x-mark class="w-4 h-4" />
|
||||
Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary gap-2"
|
||||
wire:loading.attr="disabled" wire:target="save">
|
||||
<span wire:loading wire:target="save" class="loading loading-spinner loading-sm"></span>
|
||||
<x-heroicon-o-check class="w-4 h-4" />
|
||||
{{ $user ? 'Guardar cambios' : 'Crear usuario' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user