Files
construprogress/resources/views/livewire/projects/project-form.blade.php
T
javier fe57388f05 feat(project-form): wire the rich data form (labels-left) + edit tabs
The edit/create project page used a stripped-down inline form. Rewired it
to the existing-but-orphaned pieces:
- Project Data uses the rich partial project-data-form (labels-left/field-right
  layout, sections Identification/Location/Planning, address search + Leaflet
  map with draggable marker + reverse/forward geocoding, country dropdown)
- When editing, tabs added for Phases / Users / Companies (nested Livewire
  components phase-list / project-users / project-companies)
- ProjectForm now provides $countryList (the partial's country dropdown needs it)
- Added the map JS the partial was missing: inits #project-location-map, search
  box, and calls $this->setLocation(lat,lng,address,country) so the wire:model
  fields update

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:42:42 +02:00

139 lines
6.3 KiB
PHP

<div>
<div class="max-w-4xl mx-auto p-4">
<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>
</div>
<div x-show="tab==='data'">
@include('livewire.projects.partials.project-data-form')
</div>
<div x-show="tab==='phases'" x-cloak>
<livewire:phase-list :project="$project" :key="'phases-'.$project->id" />
</div>
<div x-show="tab==='users'" x-cloak>
<livewire:project-users :project="$project" :key="'users-'.$project->id" />
</div>
<div x-show="tab==='companies'" x-cloak>
<livewire:project-companies :project="$project" :key="'companies-'.$project->id" />
</div>
</div>
@else
{{-- Alta de proyecto: solo el formulario de datos --}}
@include('livewire.projects.partials.project-data-form')
@endif
</div>
@push('scripts')
<script>
(function () {
let pmap = null, pmarker = null;
function setStatus(msg) {
const s = document.getElementById('geocode-status');
if (s) s.textContent = msg || '';
}
function placeMarker(lat, lng) {
if (!pmap) return;
if (pmarker) {
pmarker.setLatLng([lat, lng]);
} else {
pmarker = L.marker([lat, lng], { draggable: true }).addTo(pmap);
pmarker.on('dragend', () => {
const p = pmarker.getLatLng();
pickLocation(p.lat, p.lng);
});
}
}
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 : '';
}
} 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') }}');
}
}
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: '&copy; <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>