2026-05-07 23:31:33 +02:00
|
|
|
<div>
|
2026-06-17 16:12:20 +02:00
|
|
|
<div class="max-w-7xl mx-auto p-4">
|
2026-06-17 13:42:42 +02:00
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
|
|
<h1 class="text-2xl font-bold">{{ $project ? __('Edit Project') : __('New Project') }}</h1>
|
|
|
|
|
<a href="{{ route('projects.index') }}" class="btn btn-ghost btn-sm gap-1" wire:navigate>
|
|
|
|
|
<x-heroicon-o-arrow-left class="w-4 h-4" /> {{ __('Back') }}
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@if($project)
|
|
|
|
|
{{-- Editor con pestañas para el resto de parámetros del proyecto --}}
|
|
|
|
|
<div x-data="{ tab: 'data' }">
|
|
|
|
|
<div class="flex flex-wrap gap-1 mb-4">
|
|
|
|
|
<button type="button" @click="tab='data'" :class="tab==='data' ? 'btn-primary' : 'btn-ghost'" class="btn btn-sm">{{ __('Project Data') }}</button>
|
|
|
|
|
<button type="button" @click="tab='phases'" :class="tab==='phases' ? 'btn-primary' : 'btn-ghost'" class="btn btn-sm">{{ __('Phases') }}</button>
|
|
|
|
|
<button type="button" @click="tab='users'" :class="tab==='users' ? 'btn-primary' : 'btn-ghost'" class="btn btn-sm">{{ __('Users') }}</button>
|
|
|
|
|
<button type="button" @click="tab='companies'" :class="tab==='companies' ? 'btn-primary' : 'btn-ghost'" class="btn btn-sm">{{ __('Companies') }}</button>
|
2026-05-27 20:28:44 +02:00
|
|
|
</div>
|
2026-06-17 13:42:42 +02:00
|
|
|
|
|
|
|
|
<div x-show="tab==='data'">
|
|
|
|
|
@include('livewire.projects.partials.project-data-form')
|
2026-05-27 20:28:44 +02:00
|
|
|
</div>
|
2026-06-17 13:42:42 +02:00
|
|
|
<div x-show="tab==='phases'" x-cloak>
|
|
|
|
|
<livewire:phase-list :project="$project" :key="'phases-'.$project->id" />
|
2026-05-27 20:28:44 +02:00
|
|
|
</div>
|
2026-06-17 13:42:42 +02:00
|
|
|
<div x-show="tab==='users'" x-cloak>
|
|
|
|
|
<livewire:project-users :project="$project" :key="'users-'.$project->id" />
|
2026-05-27 20:28:44 +02:00
|
|
|
</div>
|
2026-06-17 13:42:42 +02:00
|
|
|
<div x-show="tab==='companies'" x-cloak>
|
|
|
|
|
<livewire:project-companies :project="$project" :key="'companies-'.$project->id" />
|
2026-05-27 20:28:44 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-06-17 13:42:42 +02:00
|
|
|
@else
|
|
|
|
|
{{-- Alta de proyecto: solo el formulario de datos --}}
|
|
|
|
|
@include('livewire.projects.partials.project-data-form')
|
2026-05-27 20:28:44 +02:00
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-06-17 13:42:42 +02:00
|
|
|
@push('scripts')
|
|
|
|
|
<script>
|
|
|
|
|
(function () {
|
|
|
|
|
let pmap = null, pmarker = null;
|
|
|
|
|
|
|
|
|
|
function setStatus(msg) {
|
|
|
|
|
const s = document.getElementById('geocode-status');
|
|
|
|
|
if (s) s.textContent = msg || '';
|
2026-05-27 20:28:44 +02:00
|
|
|
}
|
2026-06-17 13:42:42 +02:00
|
|
|
|
|
|
|
|
function placeMarker(lat, lng) {
|
|
|
|
|
if (!pmap) return;
|
|
|
|
|
if (pmarker) {
|
|
|
|
|
pmarker.setLatLng([lat, lng]);
|
2026-05-27 20:28:44 +02:00
|
|
|
} else {
|
2026-06-17 13:42:42 +02:00
|
|
|
pmarker = L.marker([lat, lng], { draggable: true }).addTo(pmap);
|
|
|
|
|
pmarker.on('dragend', () => {
|
|
|
|
|
const p = pmarker.getLatLng();
|
|
|
|
|
pickLocation(p.lat, p.lng);
|
2026-05-27 20:28:44 +02:00
|
|
|
});
|
|
|
|
|
}
|
2026-06-17 13:42:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function pickLocation(lat, lng) {
|
|
|
|
|
setStatus('{{ __('Loading...') }}');
|
|
|
|
|
let address = '', country = '';
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`);
|
|
|
|
|
if (r.ok) {
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
address = d.display_name || '';
|
|
|
|
|
country = (d.address && d.address.country_code) ? d.address.country_code : '';
|
2026-05-27 20:28:44 +02:00
|
|
|
}
|
2026-06-17 13:42:42 +02:00
|
|
|
} catch (e) { /* geocoding optional */ }
|
|
|
|
|
@this.setLocation(String(lat), String(lng), address, country);
|
|
|
|
|
setStatus('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function searchLocation(q) {
|
|
|
|
|
if (!q || !q.trim() || !pmap) return;
|
|
|
|
|
setStatus('{{ __('Searching...') }}');
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch(`https://nominatim.openstreetmap.org/search?format=json&limit=1&addressdetails=1&q=${encodeURIComponent(q)}`);
|
|
|
|
|
const arr = await r.json();
|
|
|
|
|
if (arr && arr.length) {
|
|
|
|
|
const lat = parseFloat(arr[0].lat), lng = parseFloat(arr[0].lon);
|
|
|
|
|
pmap.setView([lat, lng], 16);
|
|
|
|
|
placeMarker(lat, lng);
|
|
|
|
|
const address = arr[0].display_name || '';
|
|
|
|
|
const country = (arr[0].address && arr[0].address.country_code) ? arr[0].address.country_code : '';
|
|
|
|
|
@this.setLocation(String(lat), String(lng), address, country);
|
|
|
|
|
setStatus('');
|
|
|
|
|
} else {
|
|
|
|
|
setStatus('{{ __('No results') }}');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setStatus('{{ __('No results') }}');
|
2026-05-27 20:28:44 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-17 13:42:42 +02:00
|
|
|
|
|
|
|
|
function initProjectLocationMap() {
|
|
|
|
|
const el = document.getElementById('project-location-map');
|
|
|
|
|
if (!el || el._leafletInit) return;
|
|
|
|
|
el._leafletInit = true;
|
|
|
|
|
|
|
|
|
|
const dLat = parseFloat(el.dataset.lat);
|
|
|
|
|
const dLng = parseFloat(el.dataset.lng);
|
|
|
|
|
const hasCoords = !isNaN(dLat) && !isNaN(dLng) && (dLat !== 0 || dLng !== 0);
|
|
|
|
|
const lat = hasCoords ? dLat : 40.4168;
|
|
|
|
|
const lng = hasCoords ? dLng : -3.7038;
|
|
|
|
|
|
|
|
|
|
pmap = L.map('project-location-map').setView([lat, lng], hasCoords ? 16 : 5);
|
|
|
|
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
|
|
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> & CartoDB'
|
|
|
|
|
}).addTo(pmap);
|
|
|
|
|
|
|
|
|
|
if (hasCoords) placeMarker(lat, lng);
|
|
|
|
|
|
|
|
|
|
pmap.on('click', (e) => {
|
|
|
|
|
placeMarker(e.latlng.lat, e.latlng.lng);
|
|
|
|
|
pickLocation(e.latlng.lat, e.latlng.lng);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('map-search-input');
|
|
|
|
|
const btn = document.getElementById('map-search-btn');
|
|
|
|
|
if (btn) btn.addEventListener('click', () => searchLocation(input ? input.value : ''));
|
|
|
|
|
if (input) input.addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Enter') { e.preventDefault(); searchLocation(input.value); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setTimeout(() => pmap.invalidateSize(), 200);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener('livewire:navigated', initProjectLocationMap);
|
|
|
|
|
document.addEventListener('DOMContentLoaded', initProjectLocationMap);
|
|
|
|
|
setTimeout(initProjectLocationMap, 300);
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
@endpush
|
|
|
|
|
</div>
|