586 lines
27 KiB
PHP
586 lines
27 KiB
PHP
<!-- resources/views/projects/create.blade.php -->
|
|
|
|
<x-layouts.app title="{{ (isset($project) && $project->id) ? __('Edit User') : __('Create User') }}"
|
|
:showSidebar={{ $showSidebar }}>
|
|
|
|
<!-- Header -->
|
|
<div class="mb-8">
|
|
<div class="flex items-center gap-4 mb-4">
|
|
<svg class="h-10 w-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<!-- Base abstract shape -->
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
d="M12 4.5L20 8v8l-8 3.5L4 16V8l8-3.5z"/>
|
|
|
|
<!-- Progress indicator -->
|
|
<path stroke-linecap="round"
|
|
d="M12 6v12"
|
|
class="text-blue-500"/>
|
|
|
|
<!-- Connection dots -->
|
|
<circle cx="12" cy="6" r="1" fill="currentColor"/>
|
|
<circle cx="12" cy="18" r="1" fill="currentColor"/>
|
|
|
|
<!-- Decorative elements -->
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
d="M8 10l4 2 4-2"
|
|
class="text-green-500"/>
|
|
</svg>
|
|
<h1 class="text-3xl font-bold text-gray-800">
|
|
{{ (isset($project) && $project->id) ? 'Editar Proyecto' : 'Nuevo Proyecto'}}
|
|
</h1>
|
|
</div>
|
|
|
|
<p class="text-gray-600 text-sm">
|
|
@if(isset($project) && $project->id)
|
|
Modifique los campos necesarios para actualizar la información del usuario.
|
|
@else
|
|
Complete todos los campos obligatorios para registrar un nuevo usuario en el sistema.
|
|
@endisset
|
|
</p>
|
|
</div>
|
|
|
|
@if(session('error'))
|
|
<div id="error-message" class="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded">
|
|
{{ session('error') }}
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Formulario -->
|
|
<form method="POST" action="{{ (isset($project) && $project->id) ? route('projects.update', $project) : route('projects.store') }}" enctype="multipart/form-data">
|
|
@csrf
|
|
@if(isset($project) && $project->id)
|
|
@method('PUT')
|
|
@endif
|
|
|
|
<!-- Separador -->
|
|
<div class="relative">
|
|
<div class="absolute inset-0 flex items-center">
|
|
<div class="w-full border-t border-gray-300"></div>
|
|
</div>
|
|
<div class="relative flex justify-center">
|
|
<span class="px-4 bg-white text-sm text-gray-500">Datos Generales</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white py-6">
|
|
<table class="w-full mb-8">
|
|
<tbody>
|
|
<!-- Empresa -->
|
|
<tr>
|
|
<td class="w-1/4 py-3 pr-4 align-top">
|
|
<x-label for="company_id" :value="__('Empresa propietaria del Proyecto')" />
|
|
</td>
|
|
<td class="py-3">
|
|
<select id="company_id" name="company_id"
|
|
class="w-[250px] border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none" required>
|
|
<option value="">Seleccione una empresa...</option>
|
|
@foreach($companies as $company)
|
|
<option value="{{ $company->id }}"
|
|
{{ old('company_id', $project->company_id ?? '') == $company->id ? 'selected' : '' }}>
|
|
{{ $company->name }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
@error('company_id')
|
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
|
@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"
|
|
value="{{ old('reference', $project->reference ?? '') }}"
|
|
class="w-[250px] border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none"
|
|
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" />
|
|
</flux:tooltip>
|
|
|
|
@error('reference')
|
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Nombre -->
|
|
<tr>
|
|
<td class="w-1/4 py-3 pr-4 align-top">
|
|
<x-label for="name" :value="__('Nombre')" />
|
|
</td>
|
|
<td class="py-3">
|
|
<input type="text" name="name"
|
|
value="{{ old('name', $project->name ?? '') }}"
|
|
class="w-[500px] border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none"
|
|
required>
|
|
|
|
@error('name')
|
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Estado -->
|
|
<tr>
|
|
<td class="w-1/4 py-3 pr-4 align-top">
|
|
<x-label for="status" :value="__('Estado del Proyecto')" />
|
|
</td>
|
|
<td class="py-3">
|
|
<select id="status" name="status"
|
|
class="w-[150px] block mt-1 border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none">
|
|
<option :value="active" {{ old('status') == 'active' ? 'selected' : '' }}>Activo</option>
|
|
<option :value="inactive" {{ old('status') == 'inactive' ? 'selected' : '' }}>Inactivo</option>
|
|
</select>
|
|
@error('status')
|
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Descripción Rich Editor -->
|
|
<tr>
|
|
<td class="w-1/4 py-3 pr-4 align-top">
|
|
<x-label for="description" :value="__('Descripción')" />
|
|
</td>
|
|
<td class="py-3">
|
|
<!-- Editor Container -->
|
|
<div id="rich-editor" style="height: 120px;">
|
|
{!! old('description', $project->description ?? '') !!}
|
|
</div>
|
|
|
|
<!-- Campo oculto para enviar el contenido -->
|
|
<input type="hidden" id="description" name="description"
|
|
value="{{ old('description', $project->description ?? '') }}">
|
|
|
|
@error('description')
|
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Fechas Importantes -->
|
|
<tr>
|
|
<td class="w-1/4 py-3 pr-4 align-top">
|
|
<x-label for="start_date" :value="__('Fechas')" />
|
|
</td>
|
|
<td class="py-3">
|
|
<div class="flex gap-4">
|
|
<span class="text-gray-700">de</span>
|
|
<input type="date"
|
|
id="start_date"
|
|
name="start_date"
|
|
value="{{ old('start_date') }}"
|
|
class="w-[150px] border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none">
|
|
<span class="text-gray-700">a</span>
|
|
<input type="date"
|
|
id="deadline"
|
|
name="deadline"
|
|
value="{{ old('end_date') }}"
|
|
class="w-[150px] border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none">
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
@livewire('project-name-coder')
|
|
</div>
|
|
|
|
<div class="relative">
|
|
<div class="absolute inset-0 flex items-center">
|
|
<div class="w-full border-t border-gray-300"></div>
|
|
</div>
|
|
<div class="relative flex justify-center">
|
|
<span class="px-4 bg-white text-sm text-gray-500">Ubicación </span>
|
|
</div>
|
|
</div>
|
|
|
|
<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">
|
|
<x-label :value="__('Dirección')" />
|
|
</td>
|
|
<td class="py-3">
|
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-1">
|
|
<div>
|
|
<textarea id="address"
|
|
name="address"
|
|
rows="3"
|
|
class="w-full border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none">{{ old('address') }}
|
|
|
|
</textarea>
|
|
|
|
@error('address')
|
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div>
|
|
<input type="text"
|
|
id="postal_code"
|
|
name="postal_code"
|
|
value="{{ old('postal_code') }}"
|
|
placeholder="Código Postal"
|
|
class="w-[120px] border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none">
|
|
@error('postal_code')
|
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div>
|
|
<input type="text"
|
|
id="province"
|
|
name="province"
|
|
value="{{ old('province') }}"
|
|
placeholder="Provincia"
|
|
class="w-[300px] border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none">
|
|
@error('province')
|
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div>
|
|
<livewire:country-select :initialCountry="old('country')" />
|
|
|
|
@error('country')
|
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="relative">
|
|
<div class="absolute inset-0 flex items-center">
|
|
<div class="w-full border-t border-gray-300"></div>
|
|
</div>
|
|
<div class="relative flex justify-center">
|
|
<span class="px-4 bg-white text-sm text-gray-500">Otros datos</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white py-6">
|
|
<table class="w-full mb-8">
|
|
<tbody>
|
|
<!-- Imagen de Referencia -->
|
|
<tr>
|
|
<td class="w-1/4 py-3 pr-4 align-top">
|
|
<x-label for="reference_image" :value="__('Imagen de Referencia')" />
|
|
</td>
|
|
<td class="py-3">
|
|
@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="project_image_path" id="PhotoPathInput"
|
|
value="{{ old('project_image_path', optional($project)->project_image_path ?? '') }}">
|
|
|
|
@error('project_image')
|
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Categorías -->
|
|
<tr>
|
|
<td class="w-1/4 py-3 pr-4 align-top">
|
|
<x-label :value="__('Identificación Visual')" />
|
|
</td>
|
|
<td class="py-3">
|
|
<div>
|
|
<x-label for="categories" :value="__('Categorías')" />
|
|
<x-multiselect
|
|
class="w-[150px] border-b-1 border-gray-300 focus:border-blue-500 focus:outline-none"
|
|
name="categories[]"
|
|
:options="$categories"
|
|
:selected="old('categories', [])"
|
|
placeholder="Seleccione categorías"
|
|
/>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Miembros del Equipo -->
|
|
<tr>
|
|
<td class="w-1/4 py-3 pr-4 align-top">
|
|
<x-label :value="__('Miembros del Equipo')" />
|
|
</td>
|
|
<td class="py-3">
|
|
<div class="grid grid-cols-1 mt-2 gap-y-2 gap-x-4 sm:grid-cols-2">
|
|
@foreach($users as $user)
|
|
<label class="flex items-center space-x-2">
|
|
<input type="checkbox"
|
|
name="team[]"
|
|
value="{{ $user->id }}"
|
|
{{ in_array($user->id, old('team', [])) ? 'checked' : '' }}
|
|
class="rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500">
|
|
<span class="text-sm text-gray-700">{{ $user->first_name}} {{ $user->last_name}}</span>
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
@error('team')
|
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Botones de Acción -->
|
|
<div class="flex justify-end mt-8 space-x-4">
|
|
<a href="{{ route('projects.index') }}"
|
|
class="px-4 py-2 text-gray-600 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50">
|
|
{{ __('Cancelar') }}
|
|
</a>
|
|
<button type="submit"
|
|
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">
|
|
{{ (isset($project) && $project->id) ? 'Actualizar' : 'Crear' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
@push('scripts')
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
|
|
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
|
|
crossorigin=""/>
|
|
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
|
|
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
|
|
crossorigin=""></script>
|
|
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet" />
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.min.js"></script>
|
|
@endpush
|
|
|
|
|
|
<script>
|
|
// Editor Quill
|
|
const toolbarOptions = [
|
|
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
|
['blockquote', 'code-block'],
|
|
['link', 'formula'],
|
|
|
|
//[{ 'header': 1 }, { 'header': 2 }], // custom button :values
|
|
[{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }],
|
|
[{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript
|
|
[{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent
|
|
[{ 'direction': 'rtl' }], // text direction
|
|
|
|
//[{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown
|
|
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
|
|
|
|
[{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
|
|
[{ 'font': [] }],
|
|
[{ 'align': [] }],
|
|
|
|
['clean'] // remove formatting button
|
|
];
|
|
|
|
const quill = new Quill('#rich-editor', {
|
|
theme: 'snow',
|
|
modules: {
|
|
toolbar: toolbarOptions,
|
|
},
|
|
placeholder: 'Escribe la descripción del proyecto...'
|
|
});
|
|
|
|
// Actualizar el input hidden con el contenido HTML
|
|
quill.on('text-change', function() {
|
|
document.getElementById('description').value = quill.root.innerHTML;
|
|
});
|
|
|
|
// Inicializar con contenido existente si hay
|
|
quill.clipboard.dangerouslyPasteHTML(
|
|
document.getElementById('description').value
|
|
);
|
|
</script>
|
|
|
|
<script>
|
|
// Escuchar el evento de Livewire y actualizar el campo oculto
|
|
document.addEventListener('imageUploaded', (event) => {
|
|
document.getElementById('PhotoPathInput').value = event.detail.path;
|
|
});
|
|
|
|
document.addEventListener('imageRemoved', (event) => {
|
|
document.getElementById('PhotoPathInput').value = '';
|
|
});
|
|
|
|
</script>
|
|
|
|
<script>
|
|
let map;
|
|
let marker;
|
|
|
|
function initMap() {
|
|
// Obtener valores iniciales de los inputs o usar defaults
|
|
const initialLat = parseFloat(document.getElementById('latitude').value) || 40.4168;
|
|
const initialLng = parseFloat(document.getElementById('longitude').value) || -3.7038;
|
|
|
|
map = L.map('map').setView([initialLat, initialLng], 13);
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
|
|
|
|
// Marcador inicial si hay valores válidos
|
|
if (!isNaN(initialLat) && !isNaN(initialLng)) {
|
|
marker = L.marker([initialLat, initialLng]).addTo(map);
|
|
}
|
|
|
|
// Actualizar inputs al hacer clic en el mapa
|
|
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);
|
|
});
|
|
}
|
|
|
|
function updateMarker(latlng) {
|
|
if (marker) {
|
|
marker.setLatLng(latlng);
|
|
} else {
|
|
marker = L.marker(latlng).addTo(map);
|
|
}
|
|
map.panTo(latlng);
|
|
}
|
|
|
|
function updateInputs(lat, lng) {
|
|
document.getElementById('latitude').value = lat.toFixed(6);
|
|
document.getElementById('longitude').value = lng.toFixed(6);
|
|
}
|
|
|
|
function updateMapFromInputs() {
|
|
const lat = parseFloat(document.getElementById('latitude').value);
|
|
const lng = parseFloat(document.getElementById('longitude').value);
|
|
|
|
if (!isNaN(lat) && !isNaN(lng) &&
|
|
lat >= -90 && lat <= 90 &&
|
|
lng >= -180 && lng <= 180) {
|
|
|
|
const newLatLng = L.latLng(lat, lng);
|
|
updateMarker(newLatLng);
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
|
|
// Escuchar cambios en los inputs
|
|
document.getElementById('latitude').addEventListener('input', updateMapFromInputs);
|
|
document.getElementById('longitude').addEventListener('input', updateMapFromInputs);
|
|
};
|
|
</script>
|
|
|
|
<!-- Sidebar menu -->
|
|
@push('sidebar-menu')
|
|
<flux:navlist variant="outline">
|
|
<!-- Sección de Proyectos -->
|
|
<flux:navlist.group :heading="__('Projects')">
|
|
<flux:navlist.item
|
|
icon="folder"
|
|
:href="route('projects.index')"
|
|
wire:navigate
|
|
>
|
|
{{ __('List Projects') }}
|
|
</flux:navlist.item>
|
|
|
|
<flux:navlist.item
|
|
icon="plus"
|
|
:href="route('projects.create')"
|
|
wire:navigate
|
|
>
|
|
{{ __('Create Project') }}
|
|
</flux:navlist.item>
|
|
</flux:navlist.group>
|
|
</flux:navlist>
|
|
|
|
@endpush
|
|
|
|
</x-layouts.app> |