564b433a62
User's manual changes: header slots with New-user/New-company actions, wider max-w-7xl containers on /admin/users and /companies, plus tweaks to user-view and projects index views. All views compile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
338 lines
20 KiB
PHP
338 lines
20 KiB
PHP
<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-7xl 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>
|