new functions
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
2025-12-14 23:59:32 +01:00
parent e42ce8b092
commit 047e155238
11 changed files with 1324 additions and 481 deletions

View File

@@ -0,0 +1,348 @@
<div class="max-w-full">
<!-- Header -->
<div class="flex justify-between items-center mb-6">
<div>
<h3 class="text-lg font-medium text-gray-900">Estados de Documentos</h3>
<p class="text-sm text-gray-600">Configura los estados disponibles para los documentos de este proyecto</p>
</div>
<button type="button"
wire:click="openForm"
class="btn btn-primary">
<x-icons icon = "plus" class="w-4 h-4 mr-2" /> Nuevo Estado
</button>
</div>
<!-- Lista de estados -->
<div class="bg-white rounded-lg shadow overflow-hidden mb-6">
@if(false)
@if(count($statuses) > 0)
<div class="divide-y divide-gray-200">
@foreach($statuses as $status)
<div class="p-4 hover:bg-gray-50 transition-colors"
wire:key="status-{{ $status['id'] }}">
<div class="flex items-center justify-between">
<!-- Información del estado -->
<div class="flex items-center space-x-4">
<!-- Badge de color -->
<div class="flex items-center">
<div class="w-10 h-10 rounded-full border flex items-center justify-center"
style="background-color: {{ $status['color'] }}; color: {{ $status['text_color'] }}"
title="{{ $status['name'] }}">
<span class="font-bold text-sm">
{{ substr($status['name'], 0, 2) }}
</span>
</div>
</div>
<!-- Detalles -->
<div>
<div class="flex items-center space-x-2">
<span class="font-medium text-gray-900">{{ $status['name'] }}</span>
@if($status['is_default'])
<span class="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
Por defecto
</span>
@endif
</div>
@if($status['description'])
<p class="text-sm text-gray-600 mt-1">{{ $status['description'] }}</p>
@endif
<!-- Permisos -->
<div class="flex flex-wrap gap-2 mt-2">
@if($status['allow_upload'])
<span class="inline-flex items-center px-2 py-1 rounded text-xs bg-green-50 text-green-700">
<x-icons icon = "upload" class="w-3 h-3 mr-1" /> Subir
</span>
@endif
@if($status['allow_edit'])
<span class="inline-flex items-center px-2 py-1 rounded text-xs bg-blue-50 text-blue-700">
<x-icons icon = "pencil" class="w-3 h-3 mr-1" /> Editar
</span>
@endif
@if($status['allow_delete'])
<span class="inline-flex items-center px-2 py-1 rounded text-xs bg-red-50 text-red-700">
<x-icons icon = "trash" class="w-3 h-3 mr-1" /> Eliminar
</span>
@endif
@if($status['requires_approval'])
<span class="inline-flex items-center px-2 py-1 rounded text-xs bg-yellow-50 text-yellow-700">
<x-icons icon = "shield-check" class="w-3 h-3 mr-1" /> Aprobación
</span>
@endif
</div>
</div>
</div>
<!-- Acciones -->
<div class="flex items-center space-x-2">
<!-- Botones de orden -->
<div class="flex space-x-1">
<button type="button"
wire:click="moveUp({{ $status['id'] }})"
{{ $loop->first ? 'disabled' : '' }}
class="p-1 {{ $loop->first ? 'text-gray-300 cursor-not-allowed' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded' }}"
title="Mover hacia arriba">
<x-icons icon = "arrow-up" class="w-4 h-4" />
</button>
<button type="button"
wire:click="moveDown({{ $status['id'] }})"
{{ $loop->last ? 'disabled' : '' }}
class="p-1 {{ $loop->last ? 'text-gray-300 cursor-not-allowed' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded' }}"
title="Mover hacia abajo">
<x-icons icon = "arrow-down" class="w-4 h-4" />
</button>
</div>
<!-- Botón editar -->
<button type="button"
wire:click="openForm({{ $status['id'] }})"
class="p-1 text-yellow-600 hover:text-yellow-900 hover:bg-yellow-50 rounded"
title="Editar estado">
<x-icons icon = "pencil" class="w-4 h-4" />
</button>
<!-- Botón eliminar -->
@if(!$status['is_default'])
<button type="button"
wire:click="confirmDelete({{ $status['id'] }})"
class="p-1 text-red-600 hover:text-red-900 hover:bg-red-50 rounded"
title="Eliminar estado">
<x-icons icon = "trash" class="w-4 h-4" />
</button>
@endif
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="p-8 text-center text-gray-500">
<x-icons icon = "document-text" class="w-12 h-12 mx-auto text-gray-300 mb-4" />
<p class="mb-4">No hay estados configurados para este proyecto.</p>
<button type="button"
wire:click="openForm"
class="btn btn-primary">
<x-icons icon = "plus" class="w-4 h-4 mr-2" /> Crear primer estado
</button>
</div>
@endif
</div>
@endif
<!-- Formulario para crear/editar estado -->
<div x-data="{ showForm: @entangle('showForm') }"
x-show="showForm"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 transform scale-100"
x-transition:leave-end="opacity-0 transform scale-95"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[90vh] overflow-y-auto">
<!-- Header del modal -->
<div class="border-b px-6 py-4">
<h3 class="text-lg font-medium text-gray-900">
{{ $formData['id'] ? 'Editar Estado' : 'Crear Nuevo Estado' }}
</h3>
</div>
<!-- Formulario -->
<form wire:submit.prevent="saveStatus" class="p-6">
<!-- Nombre -->
<div class="mb-4">
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">
Nombre del Estado *
</label>
<input type="text"
id="name"
wire:model="formData.name"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
@error('formData.name')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Colores -->
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Color de fondo
</label>
<div class="flex items-center space-x-2">
<input type="color"
wire:model="formData.color"
class="w-10 h-10 p-1 border border-gray-300 rounded">
<input type="text"
wire:model="formData.color"
maxlength="7"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
@error('formData.color')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Color del texto
</label>
<div class="flex items-center space-x-2">
<input type="color"
wire:model="formData.text_color"
class="w-10 h-10 p-1 border border-gray-300 rounded">
<input type="text"
wire:model="formData.text_color"
maxlength="7"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
@error('formData.text_color')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
<!-- Previsualización -->
<div class="mb-4 p-3 bg-gray-50 rounded-lg">
<p class="text-sm font-medium text-gray-700 mb-2">Previsualización:</p>
<div class="flex items-center space-x-2">
<span class="px-3 py-1 rounded-full text-sm font-medium"
style="background-color: {{ $formData['color'] }}; color: {{ $formData['text_color'] ?: '#ffffff' }}">
{{ $formData['name'] ?: 'Nombre del estado' }}
</span>
<span class="text-sm text-gray-500">(Esto es cómo se verá)</span>
</div>
</div>
<!-- Descripción -->
<div class="mb-4">
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">
Descripción
</label>
<textarea id="description"
wire:model="formData.description"
rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"></textarea>
@error('formData.description')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Permisos -->
<div class="mb-4">
<p class="text-sm font-medium text-gray-700 mb-2">Permisos:</p>
<div class="grid grid-cols-2 gap-3">
<label class="flex items-center space-x-2">
<input type="checkbox"
wire:model="formData.allow_upload"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">Permitir subida</span>
</label>
<label class="flex items-center space-x-2">
<input type="checkbox"
wire:model="formData.allow_edit"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">Permitir edición</span>
</label>
<label class="flex items-center space-x-2">
<input type="checkbox"
wire:model="formData.allow_delete"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">Permitir eliminación</span>
</label>
<label class="flex items-center space-x-2">
<input type="checkbox"
wire:model="formData.requires_approval"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">Requiere aprobación</span>
</label>
</div>
</div>
<!-- Estado por defecto -->
<div class="mb-6">
<label class="flex items-center space-x-2">
<input type="checkbox"
wire:model="formData.is_default"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">Estado por defecto para nuevos documentos</span>
</label>
<p class="mt-1 text-sm text-gray-500">
Solo puede haber un estado por defecto. Si marcas este, se desmarcará automáticamente el anterior.
</p>
</div>
<!-- Botones -->
<div class="flex justify-end space-x-3">
<button type="button"
wire:click="closeForm"
class="btn btn-secondary">
Cancelar
</button>
<button type="submit"
class="btn btn-primary">
{{ $formData['id'] ? 'Actualizar Estado' : 'Crear Estado' }}
</button>
</div>
</form>
</div>
</div>
<!-- Modal de confirmación para eliminar -->
<div x-data="{ showDeleteModal: @entangle('showDeleteModal') }"
x-show="showDeleteModal"
x-transition
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md">
<div class="border-b px-6 py-4">
<h3 class="text-lg font-medium text-gray-900">Confirmar eliminación</h3>
</div>
<div class="p-6">
<p class="text-gray-700 mb-4">
¿Estás seguro de que deseas eliminar el estado
<strong>"{{ $statusToDelete->name ?? '' }}"</strong>?
</p>
@if($statusToDelete && $statusToDelete->documents()->exists())
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded">
<p class="text-sm text-red-700">
<x-icons icon = "exclamation" class="w-4 h-4 inline mr-1" />
No se puede eliminar porque hay documentos asociados a este estado.
</p>
</div>
@endif
<div class="flex justify-end space-x-3">
<button type="button"
wire:click="closeDeleteModal"
class="btn btn-secondary">
Cancelar
</button>
@if(!($statusToDelete && $statusToDelete->documents()->exists()))
<button type="button"
wire:click="deleteStatus"
class="btn btn-danger">
Eliminar Estado
</button>
@endif
</div>
</div>
</div>
</div>
</div>

View File

@@ -18,6 +18,9 @@
<p class="text-sm text-gray-700">
{{ $project->reference }}
</p>
<p class="mx-2">
{{ $project['codingConfig'] }}
</p>
</div>
@if($project->phone)

View File

@@ -69,11 +69,6 @@
<div id="statuses">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900">Estados de Documentos</h3>
<button type="button"
onclick="openStatusModal()"
class="btn btn-primary">
<x-icons icon="plus" class="w-4 h-4 mr-1" /> Nuevo Estado
</button>
</div>
<div class="bg-white rounded-lg shadow overflow-hidden">
@@ -82,296 +77,9 @@
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal para crear/editar estado -->
<div id="status-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50" style="display: none;">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md">
<div class="border-b px-6 py-4">
<h3 class="text-lg font-medium text-gray-900" id="modal-title">Nuevo Estado</h3>
</div>
<form id="status-form" method="POST" class="p-6">
@csrf
<div id="method-field"></div>
<div class="space-y-4">
<!-- Nombre -->
<div>
<label for="modal-name" class="block text-sm font-medium text-gray-700">
Nombre del Estado *
</label>
<input type="text"
id="modal-name"
name="name"
required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
</div>
<!-- Color -->
<div class="grid grid-cols-2 gap-4">
<div>
<label for="modal-color" class="block text-sm font-medium text-gray-700">
Color de fondo
</label>
<div class="mt-1 flex items-center">
<input type="color"
id="modal-color"
name="color"
value="#6b7280"
class="h-10 w-16 rounded border">
<input type="text"
id="modal-color-text"
name="color_text"
value="#6b7280"
class="ml-2 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
</div>
</div>
<div>
<label for="modal-text-color" class="block text-sm font-medium text-gray-700">
Color del texto
</label>
<div class="mt-1 flex items-center">
<input type="color"
id="modal-text-color"
name="text_color"
value="#ffffff"
class="h-10 w-16 rounded border">
<input type="text"
id="modal-text-color-text"
name="text_color_text"
value="#ffffff"
class="ml-2 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
</div>
</div>
</div>
<!-- Descripción -->
<div>
<label for="modal-description" class="block text-sm font-medium text-gray-700">
Descripción
</label>
<textarea id="modal-description"
name="description"
rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
</div>
<!-- Permisos -->
<div class="grid grid-cols-2 gap-4">
<label class="flex items-center">
<input type="checkbox"
name="allow_upload"
value="1"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<span class="ml-2 text-sm text-gray-700">Permitir subida</span>
</label>
<label class="flex items-center">
<input type="checkbox"
name="allow_edit"
value="1"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<span class="ml-2 text-sm text-gray-700">Permitir edición</span>
</label>
<label class="flex items-center">
<input type="checkbox"
name="allow_delete"
value="1"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-indigo-200 focus:ring-opacity-50">
<span class="ml-2 text-sm text-gray-700">Permitir eliminación</span>
</label>
<label class="flex items-center">
<input type="checkbox"
name="requires_approval"
value="1"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<span class="ml-2 text-sm text-gray-700">Requiere aprobación</span>
</label>
</div>
<!-- Estado por defecto -->
<div>
<label class="flex items-center">
<input type="checkbox"
name="is_default"
value="1"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<span class="ml-2 text-sm text-gray-700">Estado por defecto para nuevos documentos</span>
</label>
<p class="mt-1 text-xs text-gray-500">
Solo puede haber un estado por defecto
</p>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button type="button" onclick="closeStatusModal()" class="btn btn-secondary">
Cancelar
</button>
<button type="submit" class="btn btn-primary">
Guardar Estado
</button>
</div>
</form>
</div>
</div>
<!-- Modal de confirmación para eliminar -->
<div id="delete-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50" style="display: none;">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md">
<div class="border-b px-6 py-4">
<h3 class="text-lg font-medium text-gray-900">Confirmar eliminación</h3>
</div>
<div class="p-6">
<p class="text-gray-700" id="delete-message">¿Estás seguro de que deseas eliminar este estado?</p>
<form id="delete-form" method="POST" class="mt-6">
@csrf
@method('DELETE')
<div class="flex justify-end space-x-3">
<button type="button" onclick="closeDeleteModal()" class="btn btn-secondary">
Cancelar
</button>
<button type="submit" class="btn btn-danger">
Eliminar Estado
</button>
</div>
</form>
</div>
</div>
</div>
@push('scripts')
<script>
// Variables globales
let currentStatusId = null;
const statusModal = document.getElementById('status-modal');
const deleteModal = document.getElementById('delete-modal');
// Funciones para el modal de estados
function openStatusModal(status = null) {
const form = document.getElementById('status-form');
const title = document.getElementById('modal-title');
const methodField = document.getElementById('method-field');
if (status) {
// Modo edición
title.textContent = 'Editar Estado';
form.action = `/projects/{{ $project->id }}/settings/statuses/${status.id}`;
methodField.innerHTML = '<input type="hidden" name="_method" value="PUT">';
// Llenar los campos con los datos del estado
document.getElementById('modal-name').value = status.name;
document.getElementById('modal-color').value = status.color;
document.getElementById('modal-color-text').value = status.color;
document.getElementById('modal-text-color').value = status.text_color;
document.getElementById('modal-text-color-text').value = status.text_color;
document.getElementById('modal-description').value = status.description || '';
// Checkboxes
document.querySelector('input[name="allow_upload"]').checked = status.allow_upload;
document.querySelector('input[name="allow_edit"]').checked = status.allow_edit;
document.querySelector('input[name="allow_delete"]').checked = status.allow_delete;
document.querySelector('input[name="requires_approval"]').checked = status.requires_approval;
document.querySelector('input[name="is_default"]').checked = status.is_default;
currentStatusId = status.id;
} else {
// Modo creación
title.textContent = 'Nuevo Estado';
form.action = `/projects/{{ $project->id }}/settings/statuses`;
methodField.innerHTML = '';
// Resetear campos
form.reset();
document.getElementById('modal-color').value = '#6b7280';
document.getElementById('modal-color-text').value = '#6b7280';
document.getElementById('modal-text-color').value = '#ffffff';
document.getElementById('modal-text-color-text').value = '#ffffff';
currentStatusId = null;
}
statusModal.style.display = 'flex';
}
function closeStatusModal() {
statusModal.style.display = 'none';
}
// Funciones para el modal de eliminación
function openDeleteModal(status) {
const message = document.getElementById('delete-message');
const form = document.getElementById('delete-form');
message.textContent = `¿Estás seguro de que deseas eliminar el estado "${status.name}"?`;
form.action = `/projects/{{ $project->id }}/settings/statuses/${status.id}`;
currentStatusId = status.id;
deleteModal.style.display = 'flex';
}
function closeDeleteModal() {
deleteModal.style.display = 'none';
}
// Sincronizar inputs de color
document.getElementById('modal-color').addEventListener('input', function(e) {
document.getElementById('modal-color-text').value = e.target.value;
});
document.getElementById('modal-color-text').addEventListener('input', function(e) {
document.getElementById('modal-color').value = e.target.value;
});
document.getElementById('modal-text-color').addEventListener('input', function(e) {
document.getElementById('modal-text-color-text').value = e.target.value;
});
document.getElementById('modal-text-color-text').addEventListener('input', function(e) {
document.getElementById('modal-text-color').value = e.target.value;
});
// Cerrar modales al hacer clic fuera
window.addEventListener('click', function(e) {
if (e.target === statusModal) {
closeStatusModal();
}
if (e.target === deleteModal) {
closeDeleteModal();
}
});
// Funcionalidad de drag & drop para reordenar estados
new Sortable(document.getElementById('statuses-list'), {
animation: 150,
handle: '.drag-handle',
onEnd: function() {
// Obtener el nuevo orden
const order = Array.from(document.querySelectorAll('.status-item')).map(item => {
return item.dataset.statusId;
});
// Enviar el nuevo orden al servidor
fetch(`/projects/{{ $project->id }}/settings/statuses/reorder`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({ order: order })
});
}
});
</script>
@endpush
</x-layouts.app>