Merge branch 'main' of https://homehud.duckdns.org/javier/Nexora
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
2025-05-07 00:21:14 +02:00
11 changed files with 378 additions and 0 deletions

View File

@@ -57,8 +57,12 @@ class UserController extends Controller
'end_date' => 'nullable|date|after_or_equal:start_date',
'email' => 'required|email|unique:users',
'phone' => 'nullable|string|max:20',
<<<<<<< HEAD
'address' => 'nullable|string|max:255',
'profile_photo_path' => 'nullable|string' // Ruta de la imagen subida por Livewire
=======
'address' => 'nullable|string|max:255'
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
]);
// Creación del usuario
@@ -73,12 +77,20 @@ class UserController extends Controller
'address' => $validated['address'],
'access_start' => $validated['start_date'],
'access_end' => $validated['end_date'],
<<<<<<< HEAD
'is_active' => true,
'profile_photo_path' => $validated['profile_photo_path'] ?? null
]);
if ($request->hasFile('image_path')) {
$path = $request->file('image_path')->store('public/photos');
=======
'is_active' => true
]);
if ($request->hasFile('photo')) {
$path = $request->file('photo')->store('public/photos');
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
$user->profile_photo_path = basename($path);
$user->save();
}
@@ -130,11 +142,17 @@ class UserController extends Controller
Rule::unique('users')->ignore($user->id)
],
'phone' => 'nullable|string|max:20',
<<<<<<< HEAD
'address' => 'nullable|string|max:255',
'profile_photo_path' => 'nullable|string', // Añadido para la ruta de la imagen
//'is_active' => 'nullable|boolean' // Añadido para el estado activo
]);
=======
'address' => 'nullable|string|max:255'
]);
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
// Preparar datos para actualización
$updateData = [
'title' => $validated['title'],
@@ -146,14 +164,21 @@ class UserController extends Controller
'address' => $validated['address'],
'access_start' => $validated['start_date'],
'access_end' => $validated['end_date'],
<<<<<<< HEAD
'is_active' => $validated['is_active'] ?? false,
'profile_photo_path' => $validated['profile_photo_path'] ?? $user->profile_photo_path
];
=======
'is_active' => $request->has('is_active') // Si usas un checkbox
];
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
// Actualizar contraseña solo si se proporciona
if (!empty($validated['password'])) {
$updateData['password'] = Hash::make($validated['password']);
}
<<<<<<< HEAD
// Eliminar imagen anterior si se está actualizando
if (isset($validated['profile_photo_path']) && $user->profile_photo_path) {
@@ -171,6 +196,32 @@ class UserController extends Controller
return redirect()->back()->withErrors($e->validator)->withInput();
} catch (QueryException $e) {
=======
if ($request->hasFile('photo')) {
// Eliminar foto anterior si existe
if ($user->prfile_photo_path) {
Storage::delete('public/photos/'.$user->profile_photo_path);
}
$path = $request->file('photo')->store('public/photos');
$user->update(['profile_photo_path' => basename($path)]);
}
// Actualizar el usuario
$user->update($updateData);
// Redireccionar con mensaje de éxito
return redirect()->route('users.show', $user)
->with('success', 'Usuario actualizado exitosamente');
} catch (ValidationException $e) {
// Redireccionar con errores de validación
return redirect()->back()->withErrors($e->validator)->withInput();
} catch (QueryException $e) {
// Manejar errores de base de datos
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
$errorCode = $e->errorInfo[1];
$errorMessage = 'Error al actualizar el usuario: ';
@@ -179,11 +230,20 @@ class UserController extends Controller
} else {
$errorMessage .= 'Error en la base de datos';
}
<<<<<<< HEAD
Log::error("Error actualizando usuario ID {$user->id}: " . $e->getMessage());
return redirect()->back()->with('error', $errorMessage)->withInput();
} catch (\Exception $e) {
=======
Log::error("Error actualizando usuario ID {$user->id}: " . $e->getMessage());
return redirect()->back()->with('error', $errorMessage)->withInput();
} catch (\Exception $e) {
// Manejar otros errores
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
Log::error("Error general actualizando usuario ID {$user->id}: " . $e->getMessage());
return redirect()->back()->with('error', 'Ocurrió un error inesperado al actualizar el usuario')->withInput();
}
@@ -193,13 +253,20 @@ class UserController extends Controller
{
$previousUser = User::where('id', '<', $user->id)->latest('id')->first();
$nextUser = User::where('id', '>', $user->id)->oldest('id')->first();
<<<<<<< HEAD
$permissionGroups = $this->getPermissionGroups($user);
=======
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
return view('users.show', [
'user' => $user,
'previousUser' => $previousUser,
'nextUser' => $nextUser,
<<<<<<< HEAD
'permissionGroups' => $permissionGroups,
=======
'permissionGroups' => Permission::all()->groupBy('group')
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
]);
}

View File

@@ -10,6 +10,7 @@ class ImageUploader extends Component
{
use WithFileUploads;
<<<<<<< HEAD
public $image;
public $imagePath;
public $fieldName; // Nombre del campo para el formulario
@@ -46,10 +47,41 @@ class ImageUploader extends Component
field: $this->fieldName,
path: $this->imagePath
);
=======
public $photo;
public $currentImage;
public $fieldName;
public $placeholder;
public $storagePath = 'tmp/uploads';
protected $rules = [
'photo' => 'nullable|image|max:2048', // 2MB Max
];
public function mount($fieldName = 'photo', $currentImage = null, $placeholder = null)
{
$this->fieldName = $fieldName;
$this->currentImage = $currentImage;
$this->placeholder = $placeholder ?? asset('images/default-avatar.png');
}
public function updatedPhoto()
{
$this->validate([
'photo' => 'image|max:2048', // 2MB Max
]);
}
public function removePhoto()
{
$this->photo = null;
$this->currentImage = null;
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
}
public function save()
{
<<<<<<< HEAD
$this->validate([
'image' => 'required|image|max:2048',
]);
@@ -79,6 +111,38 @@ class ImageUploader extends Component
$this->hover = $status;
}
=======
$this->validate();
if ($this->photo) {
$path = $this->photo->store($this->storagePath);
if ($this->model) {
// Eliminar imagen anterior si existe
if ($this->model->{$this->fieldName}) {
Storage::delete($this->model->{$this->fieldName});
}
$this->model->{$this->fieldName} = $path;
$this->model->save();
}
$this->currentUrl = Storage::url($path);
$this->showSavedMessage = true;
$this->photo = null; // Limpiar el input de subida
}
}
protected function getCurrentImageUrl()
{
if ($this->model && $this->model->{$this->fieldName}) {
return Storage::url($this->model->{$this->fieldName});
}
return $this->placeholder;
}
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
public function render()
{
return view('livewire.image-uploader');

View File

@@ -240,6 +240,7 @@ class ProjectShow extends Component
public function render()
{
<<<<<<< HEAD
return view('livewire.project.show')->layout('components.layouts.documents', [
'title' => __('Project: :name', ['name' => $this->project->name]),
'breadcrumbs' => [
@@ -247,5 +248,8 @@ class ProjectShow extends Component
['name' => $this->project->name, 'url' => route('projects.show', $this->project)],
],
]);
=======
return view('livewire.project-show');
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
}
}

View File

@@ -1,3 +1,4 @@
<<<<<<< HEAD
<div x-data="{ isUploading: false }"
x-on:livewire-upload-start="isUploading = true"
x-on:livewire-upload-finish="isUploading = false"
@@ -54,4 +55,49 @@
<div class="alert alert-danger mt-2">{{ $message }}</div>
@enderror
</div>
=======
<div>
<!-- Preview de imagen -->
<div class="relative mb-4">
<img src="{{ $photo ? $photo->temporaryUrl() : ($currentImage ?? $placeholder) }}"
alt="Preview"
class="w-32 h-32 rounded-full object-cover border-2 border-gray-300">
@if($photo || $currentImage)
<button type="button"
wire:click="removePhoto"
class="absolute top-0 right-0 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition"
title="Eliminar foto">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
@endif
</div>
<!-- Input de archivo -->
<label class="cursor-pointer bg-white px-4 py-2 rounded-md shadow-sm border border-gray-300 hover:bg-gray-50 inline-block">
<span class="text-sm font-medium text-gray-700">
{{ $photo || $currentImage ? 'Cambiar imagen' : 'Seleccionar imagen' }}
</span>
<input type="file"
wire:model="photo"
class="hidden"
accept="image/*"
name="{{ $fieldName }}_input"> <!-- Input oculto para Livewire -->
<!-- Input real para el formulario -->
@if($photo)
<input type="hidden" name="{{ $fieldName }}" value="{{ $photo->getFilename() }}">
@elseif($currentImage)
<input type="hidden" name="{{ $fieldName }}" value="current">
@endif
</label>
@error('photo')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
<p class="mt-1 text-xs text-gray-500">PNG, JPG o JPEG (Max. 2MB)</p>
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
</div>

View File

@@ -0,0 +1,117 @@
<div>
<!-- Header y Breadcrumbs -->
<div class="p-4 bg-white border-b">
<div class="flex items-center justify-between">
<div>
<nav class="flex space-x-2 text-sm">
<a wire:click="currentFolder = null" class="cursor-pointer text-gray-600 hover:text-blue-600">
Inicio
</a>
@foreach($this->breadcrumbs as $folder)
<span class="text-gray-400">/</span>
<a wire:click="selectFolder({{ $folder->id }})"
class="cursor-pointer text-gray-600 hover:text-blue-600">
{{ $folder->name }}
</a>
@endforeach
</nav>
<h1 class="mt-2 text-2xl font-bold">{{ $project->name }}</h1>
</div>
<a href="{{ route('projects.edit', $project) }}"
class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700">
Editar Proyecto
</a>
</div>
</div>
<!-- Barra de Herramientas y Contenido -->
<div class="grid grid-cols-1 gap-6 p-4 lg:grid-cols-4">
<!-- Treeview de Carpetas -->
<div class="col-span-1 bg-white rounded-lg shadow">
<div class="p-4 border-b">
<div class="flex items-center justify-between">
<h3 class="font-medium">Carpetas</h3>
<button wire:click="createFolder" class="p-1 text-gray-600 hover:bg-gray-100 rounded">
<x-icons icon="plus" class="w-5 h-5" />
</button>
</div>
<input type="text"
wire:model.live="folderName"
placeholder="Nueva carpeta"
class="w-full p-2 mt-2 border rounded"
@keyup.enter="createFolder">
</div>
<div class="p-4">
<ul class="space-y-2">
@foreach($project->rootFolders as $folder)
<x-folder-item
:folder="$folder"
:currentFolder="$currentFolder"
:expandedFolders="$expandedFolders"
wire:key="folder-{{ $folder->id }}"
/>
@endforeach
</ul>
</div>
</div>
<!-- Documentos y Subida de Archivos -->
<div class="col-span-3">
<div class="bg-white rounded-lg shadow">
<div class="p-4 border-b">
<div class="flex items-center justify-between">
<h3 class="font-medium">Documentos</h3>
<input type="file"
wire:model="files"
multiple
class="hidden"
id="file-upload">
<label for="file-upload" class="px-4 py-2 text-sm text-white bg-green-600 rounded-lg cursor-pointer hover:bg-green-700">
Subir Archivos
</label>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Nombre</th>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Versiones</th>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Última Actualización</th>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Estado</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($this->documents as $document)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<x-icons icon="document" class="w-5 h-5 mr-2 text-gray-400" />
{{ $document->name }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{{ $document->versions_count }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
{{ $document->updated_at->diffForHumans() }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<x-status-badge :status="$document->status" />
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-6 py-4 text-center text-gray-500">
No se encontraron documentos
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -75,7 +75,11 @@
<!-- Foto del usuario -->
@if($user->profile_photo_path)
<div class="mr-3 flex-shrink-0">
<<<<<<< HEAD
<img src="{{ asset('storage/' . $user->profile_photo_path) }}"
=======
<img src="{{ asset($user->profile_photo_path) }}"
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
alt="{{ $user->full_name }}"
class="w-8 h-8 rounded-full object-cover transition-transform group-hover:scale-110">
</div>

View File

@@ -197,9 +197,15 @@
<div>
<x-label for="longitude" :value="__('Longitud')" />
<input type="number"
<<<<<<< HEAD
id="longitude"
name="longitude"
value="{{ old('longitude') }}"
=======
id="latitude"
name="latitude"
value="{{ old('latitude') }}"
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
step="any"
class="border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none">
</div>
@@ -308,7 +314,11 @@
<template x-if="files.length === 0">
<div>
<<<<<<< HEAD
<x-icons icon="upload" class="mx-auto h-12 w-12 text-gray-400" />
=======
<x-icons.upload class="mx-auto h-12 w-12 text-gray-400" />
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
<p class="mt-1 text-sm text-gray-600">
Arrastra archivos o haz clic para subir
</p>

View File

@@ -17,7 +17,10 @@
</div>
<!-- Barra de herramientas principal -->
<<<<<<< HEAD
javi
=======
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
<!-- Componente principal Livewire -->

View File

@@ -291,12 +291,21 @@
</td>
<td class="py-2">
<div class="mb-6">
<<<<<<< HEAD
@livewire('image-uploader', [
'fieldName' => 'image_path', // Nombre del campo en la BD
'label' => 'Imagen principal' // Etiqueta personalizada
])
<!-- Campo oculto para la ruta de la imagen -->
<input type="hidden" name="profile_photo_path" id="profilePhotoPathInput" value="{{ old('profile_photo_path', $user->profile_photo_path) }}">
=======
@livewire('image-uploader', [
'fieldName' => 'profile_photo_path',
'currentImage' => $user->profile_photo_path ?? null,
'placeholder' => asset('images/default-user.png')
])
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
</div>
</td>
</tr>php
@@ -317,6 +326,7 @@
<script>
<<<<<<< HEAD
// Escuchar el evento de Livewire y actualizar el campo oculto
document.addEventListener('imageUploaded', (event) => {
document.getElementById('profilePhotoPathInput').value = event.detail.path;
@@ -326,6 +336,8 @@
document.getElementById('profilePhotoPathInput').value = '';
});
=======
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
function generatePassword() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()';
let password = '';

View File

@@ -6,8 +6,14 @@
<!-- User Info Left -->
<div class="flex items-center space-x-6">
<!-- User Photo -->
<<<<<<< HEAD
<img src="{{ $user->profile_photo_path ? asset('storage/' . $user->profile_photo_path) : 'https://via.placeholder.com/150' }}"
class="w-24 h-24 rounded-full object-cover border-2 border-blue-100">
=======
<img src="{{ $user->photo_url ?? 'https://via.placeholder.com/150' }}"
class="w-24 h-24 rounded-full object-cover border-4 border-blue-100">
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
<!-- User Details -->
<div>
<h1 class="text-2xl font-bold text-gray-700">
@@ -127,9 +133,15 @@
</div>
<!-- Action Buttons -->
<<<<<<< HEAD
<div class="mt-6 flex justify-end space-x-4">
<a href="{{ route('users.edit', $user) }}"
class="w-[150px] px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center justify-center">
=======
<div class="mt-6 flex space-x-4">
<a href="{{ route('users.edit', $user) }}"
class="px-4 py-2 w-[150px] bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center">
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
</svg>
@@ -141,7 +153,11 @@
@csrf
@method('PUT') <!-- Important! -->
<button type="submit"
<<<<<<< HEAD
class="w-[150px] px-4 py-2 bg-yellow-500 hover:bg-yellow-600 text-white rounded-lg flex items-center justify-center">
=======
class="px-4 py-2 w-[150px] {{ $user->is_active ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700' }} text-white rounded-lg flex items-center">
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
</svg>
@@ -155,7 +171,11 @@
@method('DELETE') <!-- Important! -->
<button type="submit"
onclick="return confirm('¿Estás seguro de querer eliminar este usuario?')"
<<<<<<< HEAD
class="px-4 py-2 w-[150px] bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center justify-center">
=======
class="px-4 py-2 w-[150px] bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center">
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
@@ -166,6 +186,7 @@
</div>
<!-- Permissions Tab -->
<<<<<<< HEAD
<div class="space-y-4">
@foreach($permissionGroups as $groupName => $group)
<div class="border rounded-lg overflow-hidden">
@@ -225,6 +246,22 @@
wire:change="togglePermission('{{ $permission['name'] }}')"
class="sr-only peer">
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
=======
<div x-show="activeTab === 'permissions'" x-cloak>
<div class="space-y-6">
@foreach($permissionGroups as $group => $permissions)
<div class="border rounded-lg p-4">
<h3 class="text-lg font-semibold mb-4">{{ ucfirst($group) }}</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@foreach($permissions as $permission)
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<span class="text-sm">{{ $permission->name }}</span>
<label class="switch">
<input type="checkbox"
@if($user->hasPermissionTo($permission)) checked @endif
wire:change="togglePermission('{{ $permission->name }}')">
<span class="slider round"></span>
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
</label>
</div>
@endforeach
@@ -234,7 +271,11 @@
</div>
</div>
<<<<<<< HEAD
<!-- Pestaña de Proyectos -->
=======
<!-- Nueva Pestaña de Proyectos -->
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
<div x-show="activeTab === 'projects'" x-cloak>
<div class="space-y-6">
<!-- Proyectos Actuales -->
@@ -300,6 +341,7 @@
transform: translateX(26px);
}
</style>
<<<<<<< HEAD
<script>
document.addEventListener('alpine:init', () => {
@@ -318,4 +360,6 @@
});
</script>
=======
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
</x-layouts.app>

View File

@@ -49,9 +49,16 @@ Route::middleware(['auth', 'verified'])->group(function () {
// Proyectos
Route::resource('projects', ProjectController::class);
<<<<<<< HEAD
//Route::get('/projects/{project}', ProjectController::class)->name('projects.show');
//Route::get('/projects/{project}', ProjectController::class)->name('projects.show')->middleware('can:view,project'); // Opcional: política de acceso
Route::get('/projects/{project}', ProjectShow::class)->name('projects.show')->middleware('auth');
=======
Route::get('/projects/{project}', ProjectController::class)->name('projects.show');
//Route::get('/projects/{project}', ProjectController::class)->name('projects.show')->middleware('can:view,project'); // Opcional: política de acceso
//Route::get('/projects/{project}', ProjectShow::class)->name('projects.show');
>>>>>>> f97a7a84985ea300216ba3ea2ab4c31306e1659a
// Documentos
Route::resource('documents', DocumentController::class);