updates to document handling and code editing features

This commit is contained in:
2025-12-03 23:27:08 +01:00
parent 88e526cf6c
commit 7b00887372
29 changed files with 20851 additions and 1114 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,8 @@
<x-layouts.app :title="__('Show document')">
<div class="grid grid-cols-2 gap-x-4 h-screen bg-gray-100">
<div class="grid grid-cols-2 gap-x-0 h-screen bg-gray-100">
<!-- Zona izquierda - Visualizador de documentos -->
<div class="bg-gray-100 relative">
<div class="bg-gray-500 relative">
<div id="outerContainer">
<div id="sidebarContainer">
<div id="toolbarSidebar" class="toolbarHorizontalGroup">
<div id="toolbarSidebarLeft">
@@ -49,8 +48,6 @@
<div id="mainContainer">
<div class="toolbar">
<div id="toolbarContainer">
<div id="toolbarViewer" class="toolbarHorizontalGroup">
@@ -704,7 +701,46 @@
</div>
<!-- Zona derecha - Tabs de información -->
<div x-data="{ activeTab: 'properties' }" class="flex flex-col bg-white">
<div x-data="{
activeTab: 'properties',
// Estado para el treeview de versiones y revisiones
openVersion: null,
openRevision: {},
toggleVersion(versionNumber) {
this.openVersion = this.openVersion === versionNumber ? null : versionNumber;
},
toggleRevision(versionNumber, revisionNumber) {
const key = `v${versionNumber}r${revisionNumber}`;
this.openRevision[key] = !this.openRevision[key];
// Para asegurar la reactividad
this.openRevision = {...this.openRevision};
},
// Función para cargar PDF en el visor
loadPdfInViewer(pdfUrl) {
if (pdfUrl) {
// Usar PDFViewerApplication para cargar el nuevo PDF
PDFViewerApplication.open(pdfUrl).then(() => {
console.log('PDF cargado en el visor:', pdfUrl);
// Opcional: Mostrar un mensaje de éxito
this.showNotification('PDF cargado en el visor', 'success');
}).catch(error => {
console.error('Error al cargar el PDF:', error);
this.showNotification('Error al cargar el PDF', 'error');
});
}
},
// Función auxiliar para mostrar notificaciones
showNotification(message, type = 'info') {
// Puedes implementar un sistema de notificaciones toast aquí
// Por ahora usamos alert para simplificar
alert(message);
}
}" class="flex flex-col bg-white">
<!-- Tabs de navegación -->
<div class="border-b border-gray-200">
<nav class="flex space-x-4 px-4 pt-2">
@@ -752,20 +788,190 @@
<!-- Seguridad -->
<div x-show="activeTab === 'security'" x-cloak>
implementar
Implementar
</div>
<!-- Comentarios -->
<div x-show="activeTab === 'comments'" x-cloak>
Implementar
</div>
<!-- Historial -->
<div x-show="activeTab === 'history'" x-cloak>
<div class="space-y-4">
@foreach($document->versions as $version)
<x-version-item :version="$version" />
<div class="space-y-2">
@php
// Agrupar las versiones por número de versión
$groupedVersions = $document->versions->groupBy('version');
@endphp
@foreach($groupedVersions as $versionNumber => $versions)
<div class="border rounded-lg">
<!-- Cabecera de la Versión (Rama principal) -->
<div class="flex items-center justify-between p-4 bg-gray-50 cursor-pointer"
@click="toggleVersion({{ $versionNumber }})">
<div class="flex items-center space-x-3">
<svg :class="{'rotate-90': openVersion === {{ $versionNumber }}}"
class="w-4 h-4 transition-transform duration-200"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
<div class="flex items-center space-x-2">
<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
</svg>
<span class="font-medium text-gray-800">
Versión {{ $versionNumber }}
</span>
</div>
<span class="text-sm text-gray-500 bg-gray-200 px-2 py-1 rounded-full">
{{ $versions->count() }} revisión{{ $versions->count() > 1 ? 'es' : '' }}
</span>
</div>
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-500">
{{ $versions->sortBy('created_at')->first()->created_at_formatted }}
</span>
</div>
</div>
<!-- Contenido de la Versión - Revisiones como ramas hijas -->
<div x-show="openVersion === {{ $versionNumber }}" class="border-t bg-white">
<div class="space-y-0">
@foreach($versions->sortByDesc('review') as $revisionIndex => $revision)
<div class="border-b last:border-b-0">
<!-- Cabecera de la Revisión (Rama hija) -->
<div class="flex items-center justify-between p-4 pl-8 cursor-pointer hover:bg-gray-50"
@click="toggleRevision({{ $versionNumber }}, {{ $revision->review }})">
<div class="flex items-center space-x-3">
<svg :class="{'rotate-90': openRevision['v{{ $versionNumber }}r{{ $revision->review }}']}"
class="w-4 h-4 transition-transform duration-200"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
<div class="flex items-center space-x-2">
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<span class="font-medium text-gray-700">
Revisión {{ $revision->review }}
</span>
@if($revision->review == 0)
<span class="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">Original</span>
@endif
</div>
</div>
<div class="flex items-center space-x-4">
<button @click.stop="loadPdfInViewer('{{ $revision->file_url }}')"
class="inline-flex items-center px-3 py-1 text-sm bg-green-600 text-white rounded hover:bg-green-700"
title="Cargar en visor PDF">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
Cargar en Visor
</button>
<span class="text-sm text-gray-500">{{ $revision->created_at_formatted }}</span>
<span class="text-sm text-gray-400">{{ $revision->file_size_formatted }}</span>
</div>
</div>
<!-- Detalles de la Revisión (Contenido expandible) -->
<div x-show="openRevision['v{{ $versionNumber }}r{{ $revision->review }}']"
class="bg-gray-50 border-t">
<div class="p-4 pl-12 space-y-4">
<!-- Información básica -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<x-property-item label="ID" :value="$revision->id" />
<x-property-item label="Document ID" :value="$revision->document_id" />
<x-property-item label="Usuario" :value="$revision->user->name ?? 'Usuario no disponible'" />
</div>
<!-- Archivo -->
<div>
<div class="mb-2">
<span class="block text-sm font-medium text-gray-700">Archivo</span>
<div class="mt-1 flex items-center space-x-2">
<x-property-item label="Ruta" :value="$revision->file_path" />
<button @click="loadPdfInViewer('{{ $revision->file_url }}')"
class="inline-flex items-center px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
Cargar en Visor
</button>
<a href="{{ $revision->file_url }}" target="_blank"
class="inline-flex items-center px-3 py-1 text-sm bg-gray-600 text-white rounded hover:bg-gray-700">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
Descargar
</a>
</div>
</div>
</div>
<!-- Cambios -->
<div>
<span class="block text-sm font-medium text-gray-700 mb-2">Cambios realizados</span>
@if(!empty($revision->changes) && count($revision->changes) > 0)
<div class="bg-white rounded border p-3">
<ul class="list-disc list-inside text-sm text-gray-600 space-y-1">
@foreach($revision->changes as $change)
<li class="hover:text-gray-800">{{ $change }}</li>
@endforeach
</ul>
</div>
@else
<div class="bg-white rounded border p-3 text-sm text-gray-500">
No hay cambios registrados en esta revisión
</div>
@endif
</div>
<!-- Fechas -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2 border-t">
<x-property-item label="Fecha de creación" :value="$revision->created_at_formatted" />
<x-property-item label="Última actualización"
:value="$revision->updated_at ? \Carbon\Carbon::parse($revision->updated_at)->format('d/m/Y H:i') : 'N/A'" />
</div>
<!-- Información adicional del usuario -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2 border-t">
<div>
<span class="block text-sm font-medium text-gray-700">Creado por</span>
<div class="flex items-center space-x-2 mt-1">
@if(isset($revision->user->avatar))
<img src="{{ $revision->user->avatar }}" alt="{{ $revision->user->name }}" class="w-6 h-6 rounded-full">
@else
<div class="w-6 h-6 bg-gray-300 rounded-full flex items-center justify-center">
<span class="text-xs text-gray-600">{{ substr($revision->user->name ?? 'U', 0, 1) }}</span>
</div>
@endif
<span class="text-sm text-gray-600">{{ $revision->user->name ?? 'Usuario no disponible' }}</span>
</div>
</div>
<div>
<span class="block text-sm font-medium text-gray-700">Email</span>
<span class="text-sm text-gray-600">{{ $revision->user->email ?? 'No disponible' }}</span>
</div>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
@endforeach
@if($groupedVersions->isEmpty())
<div class="text-center py-8 text-gray-500">
<svg class="w-12 h-12 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<p class="mt-2">No hay historial de versiones disponible</p>
</div>
@endif
</div>
</div>
@@ -778,46 +984,183 @@
</div>
@push('scripts')
<script>
window.OptionKind = window.OptionKind || { VIEWER: 0, API: 1 };
<script>
var ae_post_url = "{{ route('documents.update-pdf', $document) }}";
window.OptionKind = window.OptionKind || { VIEWER: 0, API: 1 };
const PDF_URL = @json(Storage::url($document->file_path));
console.log("PDF URL:", PDF_URL);
const PDF_URL = @json(Storage::url($document->file_path));
const SAVE_URL = "{{ route('documents.update-pdf', $document) }}";
const CSRF_TOKEN = "{{ csrf_token() }}";
window.PDFViewerApplicationOptions = {
defaultUrl: {
value: encodeURI(PDF_URL),
kind: OptionKind.VIEWER
},
cMapUrl: '/js/pdfjs-5.2.133-dist/web/cmaps/',
cMapPacked: true
};
</script>
console.log("PDF URL:", PDF_URL);
@vite([
'resources/js/pdfjs-5.2.133-dist/build/pdf.mjs',
'resources/js/pdfjs-5.2.133-dist/web/viewer.mjs',
'resources/js/pdfjs-5.2.133-dist/web/viewer.css'
])
window.PDFViewerApplicationOptions = {
defaultUrl: {
value: encodeURI(PDF_URL),
kind: OptionKind.VIEWER
},
cMapUrl: '/js/pdfjs-5.2.133-dist/web/cmaps/',
cMapPacked: true
};
</script>
<script>
document.addEventListener('DOMContentLoaded', () => {
PDFViewerApplication.initializedPromise.then(() => {
// Pasar un OBJETO con { url: TU_URL }
PDFViewerApplication.open({
url: encodeURI(PDF_URL), // Propiedad requerida
originalUrl: PDF_URL // Opcional (para mostrar en la UI)
}).catch(error => {
console.error('Error cargando PDF:', error);
@vite([
'resources/js/pdfjs-5.2.133-dist/build/pdf.mjs',
'resources/js/pdfjs-5.2.133-dist/web/viewer.mjs',
'resources/js/pdfjs-5.2.133-dist/web/viewer.css',
'resources/js/pdfjs-5.2.133-dist/pdfjs-annotation-extension.js'
])
<script>
document.addEventListener('DOMContentLoaded', () => {
PDFViewerApplication.initializedPromise.then(() => {
// Pasar un OBJETO con { url: TU_URL }
PDFViewerApplication.open({
url: encodeURI(PDF_URL),
originalUrl: PDF_URL
}).then(() => {
// Una vez cargado el PDF, configurar los event listeners para guardar
setupSaveFunctionality();
}).catch(error => {
console.error('Error cargando PDF:', error);
});
});
function setupSaveFunctionality() {
// Interceptar el botón de descarga para guardar con anotaciones
const downloadButton = document.getElementById('downloadButton');
if (downloadButton) {
downloadButton.addEventListener('click', function(e) {
e.preventDefault();
savePdfWithAnnotations();
});
}
// También puedes agregar un botón personalizado para guardar
addCustomSaveButton();
}
function addCustomSaveButton() {
// Agregar botón de guardar personalizado en la toolbar
const toolbarRight = document.getElementById('toolbarViewerRight');
if (toolbarRight) {
const saveButton = document.createElement('button');
saveButton.id = 'saveWithAnnotations';
saveButton.className = 'toolbarButton';
saveButton.innerHTML = '<span>Guardar con anotaciones</span>';
saveButton.title = 'Guardar PDF con anotaciones, firmas y stamps';
saveButton.addEventListener('click', savePdfWithAnnotations);
// Insertar antes del botón de descarga
const downloadBtn = document.getElementById('downloadButton');
if (downloadBtn) {
toolbarRight.insertBefore(saveButton, downloadBtn);
} else {
toolbarRight.appendChild(saveButton);
}
}
}
async function savePdfWithAnnotations() {
try {
// Mostrar indicador de carga
showLoading('Guardando PDF con anotaciones...');
// Obtener el PDF modificado con anotaciones
const pdfDocument = PDFViewerApplication.pdfDocument;
const data = await pdfDocument.saveDocument();
const pdfData = await data.arrayBuffer();
// Convertir a base64
const base64Pdf = arrayBufferToBase64(pdfData);
// Enviar al servidor
const response = await fetch('{{ route("documents.update-pdf", $document) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
pdf_data: 'data:application/pdf;base64,' + base64Pdf,
annotations: getCurrentAnnotations(), // Si quieres guardar metadatos
signatures: getCurrentSignatures(),
stamps: getCurrentStamps()
})
});
const result = await response.json();
hideLoading();
if (result.success) {
showSuccess('PDF guardado correctamente con todas las anotaciones');
} else {
showError('Error al guardar: ' + result.message);
}
} catch (error) {
hideLoading();
console.error('Error saving PDF:', error);
showError('Error al guardar el PDF');
}
}
function arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
function getCurrentAnnotations() {
// Obtener anotaciones actuales del visor
// Esto depende de cómo la extensión almacena las anotaciones
if (window.pdfAnnotationExtension) {
return window.pdfAnnotationExtension.getAnnotations();
}
return [];
}
function getCurrentSignatures() {
// Obtener firmas actuales
if (window.pdfAnnotationExtension) {
return window.pdfAnnotationExtension.getSignatures();
}
return [];
}
function getCurrentStamps() {
// Obtener stamps actuales
if (window.pdfAnnotationExtension) {
return window.pdfAnnotationExtension.getStamps();
}
return [];
}
function showLoading(message) {
// Implementar un spinner o mensaje de carga
console.log('Loading:', message);
}
function hideLoading() {
// Ocultar spinner
console.log('Loading complete');
}
function showSuccess(message) {
alert(message); // Puedes usar un toast mejor
}
function showError(message) {
alert('Error: ' + message); // Puedes usar un toast mejor
}
});
});
</script>
</script>
<!-- ELIMINA EL SCRIPT versionTree() DE AQUÍ -->
@endpush
</x-layouts.app>
</x-layouts.app>

View File

@@ -1,331 +0,0 @@
<x-layouts.app :title="__('Show document')">
<div class="flex h-screen bg-gray-100"
@resize.window.debounce="renderPage(currentPage)">
<!-- Zona izquierda - Visualizador de documentos -->
<div class="w-1/2 bg-gray-800 p-4 relative">
<!-- Loading State -->
<template x-if="loading">
<div class="absolute inset-0 bg-white bg-opacity-90 flex items-center justify-center">
<div class="text-center">
<svg class="animate-spin h-12 w-12 text-blue-500 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p class="text-gray-600">Cargando documento...</p>
</div>
</div>
</template>
<!-- Error State -->
<template x-if="error">
<div class="absolute inset-0 bg-red-50 flex items-center justify-center p-4">
<div class="text-center text-red-600">
<svg class="h-12 w-12 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<p x-text="error"></p>
<p class="mt-2 text-sm">URL del documento: <span class="break-all" x-text="pdfUrl"></span></p>
</div>
</div>
</template>
<!-- Controles del PDF -->
<div class="flex items-center justify-between mb-4 bg-gray-700 p-2 rounded">
<div class="flex items-center space-x-4 text-white">
<button @click="previousPage" :disabled="currentPage <= 1" class="disabled:opacity-50">
<x-icons icon="chevron-left" class="w-6 h-6" />
</button>
<span>Página <span x-text="currentPage"></span> de <span x-text="totalPages"></span></span>
<button @click="nextPage" :disabled="currentPage >= totalPages" class="disabled:opacity-50">
<x-icons icon="chevron-right" class="w-6 h-6" />
</button>
</div>
<div class="text-white">
Zoom:
<select x-model="scale" @change="renderPage(currentPage)" class="bg-gray-600 rounded px-2 py-1">
<option value="0.25">25%</option>
<option value="0.5">50%</option>
<option value="0.75">75%</option>
<option value="1">100%</option>
<option value="1.5">150%</option>
<option value="2">200%</option>
</select>
</div>
</div>
<!-- Canvas del PDF con comentarios -->
<div class="flex-1 bg-white shadow-lg overflow-auto relative"
id="pdf-container"
@scroll.debounce="handleScroll">
<div class="pdf-page-container" :style="`width: ${pageWidth}px`">
<template x-for="(page, index) in renderedPages" :key="index">
<div class="pdf-page">
<canvas class="pdf-canvas"
:id="`canvas-${page.pageNumber}`"></canvas>
<div class="text-layer"
:id="`text-layer-${page.pageNumber}`"></div>
</div>
</template>
</div>
<!-- Marcadores de comentarios existentes -->
<template x-for="comment in comments" :key="comment.id">
<div
class="absolute w-4 h-4 bg-yellow-400 rounded-full cursor-pointer border-2 border-yellow-600"
:style="`left: ${comment.x * 100}%; top: ${comment.y * 100}%;`"
@click="scrollToComment(comment)"
x-tooltip="'Ver comentario'"
></div>
</template>
<!-- Formulario flotante para nuevos comentarios -->
<div
class="absolute bg-white p-4 rounded-lg shadow-xl w-64"
x-show="showCommentForm"
:style="`top: ${clickY}px; left: ${clickX}px`"
@click.outside="closeCommentForm"
>
<form @submit.prevent="submitComment">
<textarea
x-model="commentText"
class="w-full mb-2 p-2 border rounded"
placeholder="Escribe tu comentario..."
rows="3"
required
></textarea>
<button
type="submit"
class="btn btn-primary w-full flex items-center justify-center"
>
<x-icons icon="save" class="w-4 h-4 mr-2" /> Guardar
</button>
</form>
</div>
</div>
</div>
<!-- Zona derecha - Tabs de información -->
<div x-data="{ activeTab: 'properties' }" class="w-1/2 flex flex-col bg-white">
<!-- Tabs de navegación -->
<div class="border-b border-gray-200">
<nav class="flex space-x-4 px-4 pt-2">
<button @click="activeTab = 'properties'"
:class="activeTab === 'properties' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'"
class="py-4 px-1 border-b-2 font-medium">
Propiedades
</button>
<button @click="activeTab = 'security'"
:class="activeTab === 'security' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'"
class="py-4 px-1 border-b-2 font-medium">
Seguridad
</button>
<button @click="activeTab = 'comments'"
:class="activeTab === 'comments' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'"
class="py-4 px-1 border-b-2 font-medium">
Comentarios
</button>
<button @click="activeTab = 'history'"
:class="activeTab === 'history' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'"
class="py-4 px-1 border-b-2 font-medium">
Historial
</button>
<button @click="activeTab = 'relations'"
:class="activeTab === 'relations' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'"
class="py-4 px-1 border-b-2 font-medium">
Relaciones
</button>
</nav>
</div>
<!-- Contenido de los tabs -->
<div class="flex-1 overflow-y-auto p-4 space-y-6">
<!-- Propiedades -->
<div x-show="activeTab === 'properties'">
<div class="space-y-4">
<x-property-item label="Nombre" :value="$document->name" />
<x-property-item label="Tipo" :value="$document->type" />
<x-property-item label="Tamaño" :value="$document->size_for_humans" />
<x-property-item label="Creado por" :value="Storage::url($document->file_path)" />
<x-property-item label="Fecha creación" :value="$document->created_at->format('d/m/Y H:i')" />
<x-property-item label="Última modificación" :value="$document->updated_at->format('d/m/Y H:i')" />
</div>
</div>
<!-- Seguridad -->
<div x-show="activeTab === 'security'" x-cloak>
implementar
</div>
<!-- Comentarios -->
<div x-show="activeTab === 'comments'" x-cloak>
<template x-for="comment in comments" :key="comment.id">
<div class="p-4 mb-2 border rounded hover:bg-gray-50">
<div class="text-sm text-gray-500"
x-text="`Página ${comment.page} - ${new Date(comment.created_at).toLocaleString()}`"></div>
<div x-text="comment.text" class="mt-1"></div>
</div>
</template>
</div>
<!-- Historial -->
<div x-show="activeTab === 'history'" x-cloak>
<div class="space-y-4">
@foreach($document->versions as $version)
<x-version-item :version="$version" />
@endforeach
</div>
</div>
<!-- Relaciones -->
<div x-show="activeTab === 'relations'" x-cloak>
implementar
</div>
</div>
</div>
</div>
</x-layouts.app>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<script>
// Configuración del Visor PDF
var currentPage = 1;
var nextPage = 2;
const url = "{{ Storage::url($document->file_path) }}";
let pdfDoc = null,
pageNum = 1,
pageRendering = false,
pageNumPending = null,
scale = 0.8,
canvas = document.getElementById('pdf-canvas');
ctx = canvas.getContext('2d');
function renderPage(num) {
pageRendering = true;
pdfDoc.getPage(num).then(function(page) {
const viewport = page.getViewport({ scale: 1.5 });
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: ctx,
viewport: viewport
};
page.render(renderContext).promise.then(() => {
pageRendering = false;
loadCommentsForPage(num);
});
// Wait for rendering to finish
renderTask.promise.then(function() {
pageRendering = false;
if (pageNumPending !== null) {
// New page rendering is pending
renderPage(pageNumPending);
pageNumPending = null;
}
});
});
}
function handleCanvasClick(event) {
const rect = event.target.getBoundingClientRect();
this.clickX = event.clientX - rect.left;
this.clickY = event.clientY - rect.top;
this.showCommentForm = true;
}
// Cargar PDF
pdfjsLib.getDocument(url).promise.then(function(pdf) {
pdfDoc = pdf;
renderPage(pageNum);
});
/**
* If another page rendering in progress, waits until the rendering is
* finised. Otherwise, executes rendering immediately.
*/
function queueRenderPage(num) {
if (pageRendering) {
pageNumPending = num;
} else {
renderPage(num);
}
}
/**
* Displays previous page.
*/
function onPrevPage() {
if (pageNum <= 1) {
return;
}
pageNum--;
queueRenderPage(pageNum);
}
document.getElementById('prev').addEventListener('click', onPrevPage);
/**
* Displays next page.
*/
function onNextPage() {
if (pageNum >= pdfDoc.numPages) {
return;
}
pageNum++;
queueRenderPage(pageNum);
}
document.getElementById('next').addEventListener('click', onNextPage);
/**
* Asynchronously downloads PDF.
*/
pdfjsLib.getDocument(url).promise.then(function(pdfDoc_) {
pdfDoc = pdfDoc_;
document.getElementById('page_count').textContent = pdfDoc.numPages;
// Initial/first page rendering
renderPage(pageNum);
});
</script>
<style>
.pdf-page-container {
margin: 0 auto;
padding: 20px 0;
}
.pdf-page {
position: relative;
margin: 0 auto 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.text-layer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
opacity: 0.2;
line-height: 1;
}
.text-layer span {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
}
.text-layer ::selection {
background: rgba(0,0,255,0.2);
}
</style>

View File

@@ -0,0 +1,55 @@
@aware(['component'])
<div class="w-full mb-4 md:w-auto md:mb-0">
<div
x-data="{ open: false }"
@keydown.window.escape="open = false"
x-on:click.away="open = false"
class="relative z-10 inline-block w-full text-left md:w-auto"
wire:key="my-dropdown-button-"
>
<div>
<span class="rounded-md shadow-sm">
<button
x-on:click="open = !open"
type="button"
class="inline-flex justify-center w-full px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:hover:bg-gray-600"
aria-haspopup="true"
x-bind:aria-expanded="open"
aria-expanded="true"
>
@lang('My Dropdown')
<svg class="w-5 h-5 ml-2 -mr-1" x-description="Heroicon name: chevron-down" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
</span>
</div>
<div
x-cloak
x-show="open"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="absolute right-0 z-50 w-full mt-2 origin-top-right bg-white divide-y divide-gray-100 rounded-md shadow-lg md:w-48 ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div class="bg-white rounded-md shadow-xs dark:bg-gray-700 dark:text-white">
<div class="py-1" role="menu" aria-orientation="vertical">
<button
wire:key="my-dropdown-my-action-"
type="button"
class="flex items-center block w-full px-4 py-2 space-x-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900 dark:text-white dark:hover:bg-gray-600"
role="menuitem"
>
<span>Dummy Action ({{ $param1 }})</span>
</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,102 @@
<div class="max-w-[280px] mx-auto">
<!-- Controles de configuración -->
<div class="mb-4 p-1 bg-gray-50 rounded-lg flex items-center space-x-2 justify-left" style="width: 280px">
<flux:input
placeholder="Nombre"
size="sm"
style="width: 170px"
wire:model="name"
wire:change="updateName"
wire:blur="updateName"
/>
<flux:input
type="number"
size="sm"
style="width: 65px"
wire:model="maxLength"
wire:change="updateMaxLength"
min="2"
max="12"
/>
<flux:tooltip content="(2 - 12 caracteres)" position="right">
<flux:button icon="information-circle" size="sm" variant="ghost" />
</flux:tooltip>
</div>
<!-- Inputs para agregar nuevos tipos -->
<flux:input.group style="width: 255px">
<!-- Selector de tamaño máximo -->
<!-- Input de código -->
<flux:input
placeholder="Código"
size="sm"
style="width: 80px"
wire:model="codeInput"
wire:keydown.enter="addCode"
maxlength="{{ $maxLength }}"
title="Máximo {{ $maxLength }} caracteres"
/>
<!-- Input de label -->
<flux:input
placeholder="Label"
size="sm"
style="width: 180px"
wire:model="labelInput"
wire:keydown.enter="addLabel"
x-ref="labelInput"
/>
<!-- Botón agregar -->
<flux:button
icon="plus"
size="sm"
wire:click="addField"
title="Agregar nuevo tipo"
></flux:button>
</flux:input.group>
<!-- Información de ayuda -->
<div class="mt-1 text-xs text-gray-500">
{{ __('Tamaño: ') . $maxLength . __(' caracteres.') }}
@if($codeInput)
<span class="ml-3">
Longitud: {{ strlen($codeInput) }}/{{ $maxLength }}
</span>
@endif
</div>
<!-- Lista de tipos de documento -->
<div class="mt-3">
@if($this->getTotalDocumentTypesProperty() > 0)
<div class="text-sm font-medium text-gray-700 mb-2">
Tipos de documento ({{ $this->getTotalDocumentTypesProperty() }})
</div>
<ul class="space-y-2">
@foreach($documentTypes as $index => $documentType)
<li class="flex items-center justify-between bg-white border border-gray-200 px-3 py-2 rounded-lg shadow-sm">
<div class="flex items-center space-x-3">
<span class="font-mono bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs">
{{ $documentType['code'] }}
</span>
<span class="text-gray-800">{{ $documentType['label'] }}</span>
</div>
<flux:button
size="xs"
variant="danger"
icon="trash"
wire:click="removeField({{ $index }})"
title="Eliminar"
></flux:button>
</li>
@endforeach
</ul>
@else
<div class="text-center py-4 text-gray-500 text-xs">
No hay tipos de documento agregados
</div>
@endif
</div>
</div>

View File

@@ -0,0 +1,33 @@
<div class="flex flex-wrap items-center gap-4 p-4 bg-gray-50 rounded-lg mb-4">
<div class="flex-1 min-w-[200px]">
@include('livewire-tables::components.tools.filters.text-field', [
'filter' => $this->getFilterByKey('code'),
'placeholder' => 'Buscar código',
])
</div>
<div class="flex-1 min-w-[200px]">
@include('livewire-tables::components.tools.filters.text-field', [
'filter' => $this->getFilterByKey('name'),
'placeholder' => 'Buscar nombre',
])
</div>
<div class="flex-1 min-w-[150px]">
@include('livewire-tables::components.tools.filters.dropdown', [
'filter' => $this->getFilterByKey('status'),
])
</div>
<div class="flex-1 min-w-[150px]">
@include('livewire-tables::components.tools.filters.dropdown', [
'filter' => $this->getFilterByKey('discipline'),
])
</div>
<div class="flex items-center gap-2">
<button wire:click="clearFilters" class="px-3 py-2 text-sm text-gray-600 bg-white border border-gray-300 rounded hover:bg-gray-50">
Limpiar
</button>
</div>
</div>

View File

@@ -0,0 +1,239 @@
<div class="max-w-full mx-auto p-6">
<!-- Header con contador y botón de agregar -->
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-gray-800">
Codificación de los documentos del proyecto
</h2>
<button
type="button"
wire:click="addComponent"
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition-colors flex items-center space-x-2"
>
<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="M12 4v16m8-8H4"></path>
</svg>
<span>Agregar Componente</span>
</button>
</div>
<!-- Label con nombres de componentes -->
<div class="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div class="flex items-center space-x-2">
<span class="text-sm font-medium text-blue-800">Código:</span>
<div class="flex flex-wrap items-center gap-2">
<span ><flux:badge color="green">SOGOS0001</flux:badge>-</span>
@foreach($components as $index => $component)
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-white text-blue-700 border border-blue-200">
{{ $component['headerLabel'] }}
@if(isset($component['data']['documentTypes']) && count($component['data']['documentTypes']) > 0)
<span class="ml-1 bg-blue-100 text-blue-800 px-1.5 py-0.5 rounded-full">
{{ count($component['data']['documentTypes']) }}
</span>
@endif
</span>
@if(!$loop->last)
<span class="text-blue-400">-</span>
@endif
@endforeach
</div>
</div>
</div>
<!-- Contenedor horizontal para drag and drop -->
<div
x-data="{
draggedComponent: null,
init() {
const container = this.$refs.componentsContainer;
if (container.children.length > 0) {
// Inicializar Sortable para horizontal
new Sortable(container, {
animation: 150,
ghostClass: 'bg-blue-50',
chosenClass: 'bg-blue-100',
dragClass: 'bg-blue-200',
direction: 'horizontal',
onStart: (evt) => {
this.draggedComponent = evt.item.getAttribute('data-component-id');
},
onEnd: (evt) => {
const orderedIds = Array.from(container.children).map(child => {
return parseInt(child.getAttribute('data-component-id'));
});
// Enviar el nuevo orden a Livewire
this.$wire.updateComponentOrder(orderedIds);
this.draggedComponent = null;
}
});
}
}
}"
x-ref="componentsContainer"
class="flex space-x-4 overflow-x-auto pb-4 min-h-80"
>
<!-- Lista de componentes en horizontal -->
@foreach($components as $component)
<div
data-component-id="{{ $component['id'] }}"
class="component-item bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 flex-shrink-0 w-80"
wire:key="component-{{ $component['id'] }}"
>
<!-- Header del componente -->
<div class="flex justify-between items-center p-3 border-b border-gray-100 bg-gray-50 rounded-t-lg">
<div class="flex items-center space-x-2">
<!-- Handle para drag -->
<!--
<div class="cursor-move text-gray-400 hover:text-gray-600">
<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="M4 8h16M4 16h16"></path>
</svg>
</div>
-->
<span class="text-xs font-medium text-gray-700">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-white text-blue-700 border border-blue-200">{{ $loop->iteration }}</span>
{{ $component['headerLabel'] }}
</span>
<!-- Contador de tipos en este componente -->
@if(isset($component['data']['documentTypes']) && count($component['data']['documentTypes']) > 0)
<span class="bg-blue-100 text-blue-800 text-xs px-1.5 py-0.5 rounded-full">
{{ count($component['data']['documentTypes']) }}
</span>
@endif
</div>
<div class="flex items-center space-x-1">
<!-- Botones de orden -->
@if(!$loop->first)
<button
type="button"
wire:click="moveComponentUp({{ $component['id'] }})"
title="Mover hacia arriba"
class="p-1 text-blue-600 hover:text-blue-800 hover:bg-blue-100 rounded transition-colors"
>
<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="M15 19l-7-7 7-7"></path>
</svg>
</button>
@else
<div class="w-6"></div> <!-- Espacio para mantener alineación -->
@endif
@if(!$loop->last)
<button
type="button"
wire:click="moveComponentDown({{ $component['id'] }})"
title="Mover hacia abajo"
class="p-1 text-blue-600 hover:text-blue-800 hover:bg-blue-100 rounded transition-colors"
>
<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="M9 5l7 7-7 7"></path>
</svg>
</button>
@else
<div class="w-6"></div> <!-- Espacio para mantener alineación -->
@endif
<!-- Botón eliminar -->
<button
type="button"
wire:click="removeComponent({{ $component['id'] }})"
title="Eliminar este componente"
class="p-1 text-red-600 hover:text-red-800 hover:bg-red-100 rounded transition-colors"
>
<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="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"></path>
</svg>
</button>
</div>
</div>
<!-- Componente hijo -->
<div class="p-3">
<livewire:code-edit
:key="'document-manager-' . $component['id']"
:component-id="$component['id']"
:initial-name="$component['headerLabel']"
/>
</div>
</div>
@endforeach
</div>
<!-- Mensaje cuando no hay componentes -->
@if($this->componentsCount === 0)
<div class="text-center py-12 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
<svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<p class="text-gray-500 mb-4">No hay componentes de tipos de documento</p>
<button
type="button"
wire:click="addComponent"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
>
Agregar el primer componente
</button>
</div>
@endif
<!-- Incluir Sortable.js -->
<style>
.component-item {
cursor: default;
min-width: 320px;
}
.component-item .cursor-move {
cursor: grab;
}
.component-item .cursor-move:active {
cursor: grabbing;
}
.sortable-ghost {
opacity: 0.4;
background-color: #dbeafe;
}
.sortable-chosen {
background-color: #eff6ff;
transform: rotate(2deg);
}
.sortable-drag {
background-color: #dbeafe;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
}
.overflow-x-auto {
scrollbar-width: thin;
scrollbar-color: #cbd5e0 #f7fafc;
}
.overflow-x-auto::-webkit-scrollbar {
height: 8px;
}
.overflow-x-auto::-webkit-scrollbar-track {
background: #f7fafc;
border-radius: 4px;
}
.overflow-x-auto::-webkit-scrollbar-thumb {
background: #cbd5e0;
border-radius: 4px;
}
.overflow-x-auto::-webkit-scrollbar-thumb:hover {
background: #a0aec0;
}
</style>
</div>

View File

@@ -1,506 +0,0 @@
<div>
<!-- Header y Breadcrumbs -->
<div class="bg-white mb-6 flex content-start justify-between">
<!-- User Info Left -->
<div class="flex space-x-6 content-start">
<!-- Icono -->
<flux:icon.bolt class="w-24 h-24 shadow-lg object-cover border-1 border-gray-600"/>
<!-- Project Details -->
<div class="flex flex-col content-start">
<h1 class="text-2xl font-bold text-gray-700">
{{ $project->name }}
</h1>
<!-- Contact Info -->
<div class="mt-2 ">
<div class="flex items-center text-gray-600">
<p class="text-sm text-gray-700">
{{ $project->reference }}
</p>
</div>
@if($project->phone)
<div class="flex items-center text-gray-600">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"/>
</svg>
<a href="tel:{{ $project->phone }}" class="hover:text-blue-600">
{{ $project->phone }}
</a>
</div>
@endif
</div>
</div>
</div>
<!-- Right Section -->
<div class="flex flex-col items-end space-y-4">
<!-- Navigation Toolbar -->
<div class="flex space-x-2">
<flux:button
href="{{ route('projects.index') }}"
icon:trailing="arrow-uturn-left"
variant="ghost"
>
</flux:button>
<flux:button
href="{{ route('projects.index') }}"
icon:trailing="chevron-left"
variant="ghost"
>
</flux:button>
<flux:button
href="{{ route('projects.index') }}"
icon:trailing="chevron-right"
variant="ghost"
>
</flux:button>
</div>
<!-- Status Badge -->
<span class="px-4 py-2 w-30 rounded-lg text-sm text-center font-semibold
{{ $project->is_active ? 'bg-green-600 text-white' : 'bg-red-100 text-red-800' }}">
{{ $project->is_active ? 'Activo' : 'Inactivo' }}
</span>
</div>
</div>
<div wire:loading wire:target="files" class="fixed top-0 right-0 p-4 bg-blue-100 text-blue-800">
Subiendo archivos...
</div>
<!-- Contend: -->
<div x-data="{ activeTab: 'info' }" class="bg-white rounded-lg shadow-md border-1">
<!-- Tab Headers -->
<div class="border-b border-gray-200">
<nav class="flex space-x-8 px-6">
<button @click="activeTab = 'info'"
:class="activeTab === 'info' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'"
class="py-4 px-1 border-b-2 font-medium">
Project
</button>
<button @click="activeTab = 'contacts'"
:class="activeTab === 'contacts' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'"
class="py-4 px-1 border-b-2 font-medium">
Contactos
</button>
<button @click="activeTab = 'documents'"
:class="activeTab === 'documents' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'"
class="py-4 px-1 border-b-2 font-medium">
Documentos
</button>
</nav>
</div>
<!-- Tab Content -->
<div class="p-6">
<!-- Info Tab -->
<div x-show="activeTab === 'info'">
<div class="flex flex-wrap gap-6">
<!-- Columna Izquierda - Información -->
<div class="w-full md:w-[calc(50%-12px)]">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<tbody class="bg-white divide-y divide-gray-200">
@foreach(['name' => 'Nombre', 'last_name' => 'Apellido', 'email' => 'Email', 'phone' => 'Teléfono', 'created_at' => 'Fecha Registro'] as $field => $label)
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ $label }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $project->$field ?? 'N/A' }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<!-- Columna Derecha - Descripción del Proyecto -->
<div class="w-full md:w-[calc(50%-12px)]"> <!-- 50% - gap/2 -->
<div class="bg-white p-6 rounded-lg shadow-sm">
<h3 class="whitespace-nowrap text-sm font-medium text-gray-900"">Descripción</h3>
<div class="py-2 prose max-w-none text-gray-500">
{!! $project->description ? $project->description : '<p class="text-gray-400">N/A</p>' !!}
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="mt-6 flex justify-end space-x-4">
<a href="{{ route('projects.edit', $project) }}"
class="w-[150px] px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center justify-center">
<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>
Editar
</a>
{{-- Formulario de Edición --}}
<form method="POST" action="{{ route('projects.update', $project) }}">
@csrf
@method('PUT') <!-- Important! -->
<button type="submit"
class="w-[150px] px-4 py-2 bg-yellow-500 hover:bg-yellow-600 text-white rounded-lg flex items-center justify-center">
<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>
{{ $project->is_active ? 'Desactivar' : 'Activar' }}
</button>
</form>
{{-- Formulario de Eliminación --}}
<form method="POST" action="{{ route('projects.destroy', $project) }}">
@csrf
@method('DELETE') <!-- Important! -->
<button type="submit"
onclick="return confirm('¿Estás seguro de querer eliminar este usuario?')"
class="px-4 py-2 w-[150px] bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center justify-center">
<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>
Eliminar
</button>
</form>
</div>
</div>
<!-- Contact Tab -->
<div x-show="activeTab === 'contacts'" x-cloak>
</div>
<!-- Permissions Tab -->
<div x-show="activeTab === 'documents'" x-cloak>
<!-- Contenedor principal con layout de explorador -->
<div class="flex flex-col h-full">
<!-- Barra de herramientas -->
<div class="flex items-center justify-between p-2 bg-white border-b">
<div class="flex items-center space-x-4">
<!-- Botón Nueva Carpeta -->
<button wire:click="showCreateFolderModal" class="flex items-center p-2 text-gray-600 hover:bg-gray-100 rounded">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
</svg>
<span class="text-sm">Nueva carpeta</span>
</button>
<!-- Botón Subir Archivos (versión con button) -->
<div class="relative">
<button
wire:click="openUploadModal"
class="flex items-center p-2 text-gray-600 hover:bg-gray-100 rounded">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0 3 3m-3-3-3 3M6.75 19.5a4.5 4.5 0 0 1-1.41-8.775 5.25 5.25 0 0 1 10.233-2.33 3 3 0 0 1 3.758 3.848A3.752 3.752 0 0 1 18 19.5H6.75Z" />
</svg>
<span class="text-sm">Subir archivos</span>
</button>
</div>
</div>
</div>
<!-- Contenido principal (treeview + documentos) -->
<div class="flex flex-1 overflow-hidden">
<!-- Treeview de Carpetas -->
<div class="w-64 h-full overflow-y-auto border bg-white">
<div class="">
<ul class="space-y-1">
@foreach($project->rootFolders as $folder)
<x-folder-item
:folder="$folder"
:currentFolder="$currentFolder"
:expandedFolders="$expandedFolders"
wire:key="folder-{{ $folder->id }}"
:itemsCount="$this->documents->count()"
/>
@endforeach
</ul>
</div>
</div>
<!-- Documentos -->
<div class="flex-1 overflow-auto bg-white">
<div class="p-2 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="flex cursor-pointer text-gray-600 hover:text-blue-600">
<flux:icon.home class="w-4 h-4"/> 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>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table id="listofdocuments" class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" class="text-gray-700 hover:text-gray-900 focus:outline-none">
Opciones
</button>
<div x-show="open" @click.away="open = false" class="absolute z-10 mt-2 bg-white border border-gray-300 rounded shadow-lg">
<ul class="py-1">
<template x-for="(column, index) in $store.columns.all" :key="index">
<li class="px-4 py-2">
<label class="inline-flex items-center">
<input type="checkbox" class="form-checkbox" :checked="$store.columns.visible.includes(column)" @change="$store.columns.toggle(column)">
<span class="ml-2" x-text="column"></span>
</label>
</li>
</template>
</ul>
</div>
</div>
<div>
Nombre
</div>
</th>
<template x-for="column in $store.columns.all" :key="column">
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase" x-show="$store.columns.visible.includes(column)" x-text="column"></th>
</template>
</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">
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<a href="{{ route('documents.show', $document) }}"
target="_blank"
class="flex items-center hover:text-blue-600 transition-colors">
@php
$type = App\Helpers\FileHelper::getFileType($document->name);
$iconComponent = "icons." . $type;
$iconClass = [
'pdf' => 'pdf text-red-500',
'word' => 'word text-blue-500',
'excel' => 'excel text-green-500',
][$type] ?? 'document text-gray-400';
@endphp
<x-dynamic-component
component="{{ $iconComponent }}"
class="w-5 h-5 mr-2 {{ explode(' ', $iconClass)[1] }}"
/>
{{ $document->name }}
</a>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Versiones')">
{{ $document->versions_count }}
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Última Actualización')">
{{ $document->updated_at->diffForHumans() }}
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Estado')">
<x-status-badge :status="$document->status" />
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Revisión')">
{{ $document->revision }}
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Disciplina')">
{{ $document->discipline }}
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Tipo de Documento')">
{{ $document->document_type }}
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Emisor')">
{{ $document->issuer }}
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Fecha de Entrada')">
{{ $document->entry_date }}
</td>
</tr>
@empty
<tr>
<td colspan="9" class="px-6 py-4 text-center text-gray-500">
No se encontraron documentos en esta carpeta
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal para crear carpeta -->
@if($showFolderModal)
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="w-full max-w-md p-6 bg-white rounded-lg shadow-xl">
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900">Crear nueva carpeta</h3>
<p class="mt-1 text-sm text-gray-500">Ingresa el nombre de la nueva carpeta</p>
</div>
<div class="mb-4">
<input
type="text"
wire:model="folderName"
placeholder="Nombre de la carpeta"
class="w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500"
autofocus
@keyup.enter="createFolder"
>
@error('folderName')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="flex justify-end space-x-3">
<button
wire:click="hideCreateFolderModal"
class="px-4 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Cancelar
</button>
<button
wire:click="createFolder"
class="px-4 py-2 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700">
Crear carpeta
</button>
</div>
</div>
</div>
@endif
<!-- Modal de subida -->
@if($showUploadModal)
<div
x-data="{ isDragging: false }"
x-on:drop.prevent="isDragging = false; $wire.selectFiles(Array.from($event.dataTransfer.files))"
x-on:dragover.prevent="isDragging = true"
x-on:dragleave.prevent="isDragging = false"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="w-full max-w-2xl p-6 bg-white rounded-lg shadow-xl">
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900">Subir archivos</h3>
<p class="mt-1 text-sm text-gray-500">
{{ $currentFolder ? "Carpeta destino: {$currentFolder->name}" : "Carpeta raíz del proyecto" }}
</p>
</div>
<!-- Área de drag & drop -->
<div
x-on:click="$refs.fileInput.click()"
:class="isDragging ? 'border-blue-500 bg-blue-50' : 'border-gray-300'"
class="flex flex-col items-center justify-center p-8 border-2 border-dashed rounded-lg cursor-pointer hover:bg-gray-50 transition-colors">
<input
type="file"
x-ref="fileInput"
wire:model="selectedFiles"
multiple
class="hidden"
wire:change="selectFiles($event.target.files)">
<svg xmlns="http://www.w3.org/2000/svg" class="w-12 h-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<div class="mt-4 text-center">
<p class="font-medium text-gray-900">
Arrastra archivos aquí o haz clic para seleccionar
</p>
<p class="text-sm text-gray-500">
Formatos soportados: PDF, DOCX, XLSX, JPG, PNG (Máx. 10MB)
</p>
</div>
</div>
<!-- Lista de archivos -->
<div class="mt-4 max-h-64 overflow-y-auto">
@if(count($selectedFiles) > 0)
<ul class="space-y-2">
@foreach($selectedFiles as $index => $file)
<li class="flex items-center justify-between p-2 bg-gray-50 rounded">
<div class="flex items-center truncate">
<x-icons icon="document" class="w-4 h-4 mr-2 text-gray-400" />
<span class="text-sm truncate">
{{ $file->getClientOriginalName() }} <!-- Ahora funciona -->
</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-xs text-gray-500">
{{ number_format($file->getSize() / 1024, 2) }} KB
</span>
<button
wire:click="removeFile({{ $index }})"
type="button"
class="p-1 text-gray-400 hover:text-red-600">
</button>
</div>
</li>
@endforeach
</ul>
@endif
</div>
<!-- Footer del modal -->
<div class="flex justify-end mt-6 space-x-3">
<button
wire:click="resetUpload"
type="button"
class="px-4 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Cancelar
</button>
<button
wire:click="uploadFiles"
:disabled="!selectedFiles.length"
class="px-4 py-2 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">
Subir archivos ({{ count($selectedFiles) }})
</button>
</div>
</div>
</div>
@endif
</div>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('columns', {
all: ['Versiones', 'Última Actualización', 'Estado', 'Revisión', 'Disciplina', 'Tipo de Documento', 'Emisor', 'Fecha de Entrada'],
visible: ['Versiones', 'Última Actualización', 'Estado', 'Revisión', 'Disciplina', 'Tipo de Documento', 'Emisor', 'Fecha de Entrada'],
toggle(column) {
if (this.visible.includes(column)) {
this.visible = this.visible.filter(c => c !== column);
} else {
this.visible.push(column);
}
}
});
});
</script>
<script>
document.addEventListener('livewire:init', () => {
Livewire.on('upload-progress', (name, progress) => {
// Actualizar progreso en el frontend
const progressBar = document.querySelector(`[data-file="${name}"] .progress-bar`);
if (progressBar) {
progressBar.style.width = `${progress}%`;
}
});
});
</script>

View File

@@ -60,8 +60,8 @@
<!-- Status Badge -->
<span class="px-4 py-2 w-30 rounded-lg text-sm text-center font-semibold
{{ $project->is_active ? 'bg-green-600 text-white' : 'bg-red-100 text-red-800' }}">
{{ $project->is_active ? 'Activo' : 'Inactivo' }}
{{ $project->status == "Activo" ? 'bg-green-600 text-white' : 'bg-red-100 text-red-800' }}">
{{ /*$project->is_active ? 'Activo' : 'Inactivo'*/ $project->status}}
</span>
</div>
</div>
@@ -73,7 +73,7 @@
<!-- Contend: -->
<div x-data="{ activeTab: 'info' }" class="bg-white rounded-lg shadow-md border-1">
<div x-data="{ activeTab: 'documents' }" class="bg-white rounded-lg shadow-md border-1">
<!-- Tab Headers -->
<div class="border-b border-gray-200">
<nav class="flex space-x-8 px-6">
@@ -244,97 +244,11 @@
</div>
<div class="overflow-x-auto">
<table id="listofdocuments" class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<div class="flex items-center gap-2">
<div class="relative" x-data="{ open: false }">
<flux:dropdown>
<flux:button icon="ellipsis-horizontal" variant="subtle" size="sm"/>
<flux:menu>
<template x-for="(column, index) in $store.columns.all" :key="index">
<li class="px-4 py-2">
<label class="inline-flex items-center">
<input type="checkbox" class="form-checkbox" :checked="$store.columns.visible.includes(column)" @change="$store.columns.toggle(column)">
<span class="ml-2" x-text="column"></span>
</label>
</li>
</template>
</flux:menu>
</flux:dropdown>
</div>
<span><flux:checkbox /></span>
</div>
</th>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
NOMBRE
</th>
<template x-for="column in $store.columns.all" :key="column">
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase" x-show="$store.columns.visible.includes(column)" x-text="column"></th>
</template>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($this->documents as $document)
<tr class="hover:bg-gray-50">
<td class="w-4"></td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<a href="{{ route('documents.show', $document) }}"
target="_blank"
class="flex items-center hover:text-blue-600 transition-colors">
@php
$type = App\Helpers\FileHelper::getFileType($document->name);
$iconComponent = "icons." . $type;
$iconClass = [
'pdf' => 'pdf text-red-500',
'word' => 'word text-blue-500',
'excel' => 'excel text-green-500',
][$type] ?? 'document text-gray-400';
@endphp
<x-dynamic-component
component="{{ $iconComponent }}"
class="w-5 h-5 mr-2 {{ explode(' ', $iconClass)[1] }}"
/>
{{ $document->name }}
</a>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Versiones')">
{{ $document->versions_count }}
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Última Actualización')">
{{ $document->updated_at->diffForHumans() }}
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Estado')">
<x-status-badge :status="$document->status" />
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Revisión')">
{{ $document->revision }}
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Disciplina')">
{{ $document->discipline }}
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Tipo de Documento')">
{{ $document->document_type }}
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Emisor')">
{{ $document->issuer }}
</td>
<td class="px-6 py-4 whitespace-nowrap" x-show="$store.columns.visible.includes('Fecha de Entrada')">
{{ $document->entry_date }}
</td>
</tr>
@empty
<tr>
<td colspan="9" class="px-6 py-4 text-center text-gray-500">
No se encontraron documentos en esta carpeta
</td>
</tr>
@endforelse
</tbody>
</table>
<livewire:project-document-list
:project-id="$project->id"
:folder-id="$currentFolder?->id"
wire:key="document-table-{{ $currentFolder?->id ?? 'root' }}-{{ now()->timestamp }}"
/>
</div>
</div>
</div>
@@ -435,7 +349,7 @@
<div class="flex items-center truncate">
<x-icons icon="document" class="w-4 h-4 mr-2 text-gray-400" />
<span class="text-sm truncate">
{{ $file->getClientOriginalName() }} <!-- Ahora funciona -->
{{ $file->getClientOriginalName() }}
</span>
</div>

View File

@@ -26,7 +26,7 @@
class="text-green-500"/>
</svg>
<h1 class="text-3xl font-bold text-gray-800">
{{ (isset($project) && $project->id) ? 'Editar Proyecto' : 'Nuevo Proyecto' }}
{{ (isset($project) && $project->id) ? 'Editar Proyecto' : 'Nuevo Proyecto'}}
</h1>
</div>
@@ -37,7 +37,6 @@
Complete todos los campos obligatorios para registrar un nuevo usuario en el sistema.
@endisset
</p>
</div>
@if(session('error'))
@@ -87,18 +86,19 @@
@enderror
</td>
</tr>
<!-- Referencia -->
<tr>
<td class="w-1/4 py-3 pr-4 align-top">
<x-label for="reference" :value="__('Referencia')" />
</td>
<td class="py-3">
<input type="text" name="reference" id="reference"
<input type="text" name="reference" id="reference"
value="{{ old('reference', $project->reference ?? '') }}"
class="w-[250px] border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none"
maxlength="12"
autofocus
maxlength="12"
autofocus
wire:model.live="projectCode"
required>
<flux:tooltip content="Máximo: 12 caracteres" position="right">
<flux:button icon="information-circle" size="sm" variant="ghost" />
@@ -189,6 +189,7 @@
</tr>
</tbody>
</table>
@livewire('project-name-coder')
</div>
<div class="relative">
@@ -203,6 +204,43 @@
<div class="bg-white py-6">
<table class="w-full mb-8">
<tbody>
<!-- Mapa para Coordenadas -->
<tr>
<td class="w-1/4 py-3 pr-4 align-top">
<x-label :value="__('Seleccione Ubicación')" />
</td>
<td class="py-3">
<div id="map" class="h-100 border-2 border-gray-200"></div>
<div class="grid grid-cols-2 gap-4 mt-2">
<div>
<x-label for="latitude" :value="__('Latitud')" />
<input type="number"
id="latitude"
name="latitude"
value="{{ old('latitude') }}"
step="any"
class="border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none">
</div>
<div>
<x-label for="longitude" :value="__('Longitud')" />
<input type="number"
id="longitude"
name="longitude"
value="{{ old('longitude') }}"
step="any"
class="border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none">
</div>
</div>
@error('latitude')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
@error('longitude')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</td>
</tr>
<!-- Dirección -->
<tr>
<td class="w-1/4 py-3 pr-4 align-top">
@@ -249,6 +287,7 @@
<div>
<livewire:country-select :initialCountry="old('country')" />
@error('country')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
@@ -256,42 +295,6 @@
</div>
</td>
</tr>
<!-- Mapa para Coordenadas -->
<tr>
<td class="w-1/4 py-3 pr-4 align-top">
<x-label :value="__('Seleccione Ubicación')" />
</td>
<td class="py-3">
<div id="map" class="h-80 border-2 border-gray-200"></div>
<div class="grid grid-cols-2 gap-4 mt-2">
<div>
<x-label for="latitude" :value="__('Latitud')" />
<input type="number"
id="latitude"
name="latitude"
value="{{ old('latitude') }}"
step="any"
class="border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none">
</div>
<div>
<x-label for="longitude" :value="__('Longitud')" />
<input type="number"
id="longitude"
name="longitude"
value="{{ old('longitude') }}"
step="any"
class="border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none">
</div>
</div>
@error('latitude')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
@error('longitude')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</td>
</tr>
</tbody>
</table>
</div>
@@ -476,6 +479,8 @@
map.on('click', function(e) {
updateInputs(e.latlng.lat, e.latlng.lng);
updateMarker(e.latlng);
// Realizar reverse geocoding
reverseGeocode(e.latlng.lat, e.latlng.lng);
});
}
@@ -506,6 +511,43 @@
}
}
// Función para reverse geocoding
function reverseGeocode(lat, lng) {
// Usar Nominatim API de OpenStreetMap para reverse geocoding
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`)
.then(response => response.json())
.then(data => {
if (data && data.address) {
// Actualizar dirección completa
const address = data.display_name || '';
document.getElementById('address').value = address;
// Actualizar código postal
const postalCode = data.address.postcode || '';
document.getElementById('postal_code').value = postalCode;
// Actualizar provincia
const province = data.address.state || data.address.region || data.address.county || '';
document.getElementById('province').value = province;
// Actualizar país
const country = data.address.country || '';
const countryInput = document.querySelector('input[name="country"]');
if (countryInput) {
countryInput.value = country;
// Disparar evento change para que Livewire lo detecte
countryInput.dispatchEvent(new Event('change', { bubbles: true }));
}
// También puedes actualizar otros campos si están disponibles
console.log('Datos de geocoding:', data.address);
}
})
.catch(error => {
console.error('Error en reverse geocoding:', error);
});
}
// Inicializar el mapa
window.onload = function() {
initMap();