project = $project; $this->phase = $phase; if ($this->phase->project_id !== $this->project->id) abort(404); $user = Auth::user(); if (!$user->can('manage all') && !$project->users()->where('user_id', $user->id)->exists()) { abort(403); } $this->templates = InspectionTemplate::where('project_id', $this->project->id)->get(); $this->loadLayers(); $this->visibleLayers = $this->layers->pluck('id')->toArray(); $this->emitInitialLayersData(); } // ── Data loaders ────────────────────────────────────────────────────────── public function loadLayers() { $this->layers = Layer::withCount('features') ->withAvg('features', 'progress') ->where('phase_id', $this->phase->id) ->latest() ->get(); $this->visibleLayers = array_values( array_intersect($this->visibleLayers, $this->layers->pluck('id')->toArray()) ); } private function buildLayerPayload(Layer $layer): array { $color = $layer->color ?: '#3b82f6'; $features = ($layer->relationLoaded('features') ? $layer->features : $layer->features()->get()) ->map(fn($f) => [ 'type' => 'Feature', 'id' => $f->id, 'geometry' => $f->geometry, 'properties' => [ 'name' => $f->name ?? 'Elemento', 'progress' => $f->progress, 'status' => $f->status ?? 'planned', 'responsible' => $f->responsible, 'template_id' => $f->template_id, ], ])->values()->toArray(); return [ 'id' => $layer->id, 'color' => $color, 'geojson' => [ 'type' => 'FeatureCollection', 'features' => $features, 'style' => ['color' => $color], ], ]; } private function emitInitialLayersData() { $this->layers->loadMissing('features'); $this->dispatch('initialLayersData', [ 'layers' => $this->layers->map(fn($l) => $this->buildLayerPayload($l)), 'visibleLayers' => $this->visibleLayers, 'selectedLayerId' => $this->selectedLayer?->id, ]); } // ── Visibility ──────────────────────────────────────────────────────────── public function toggleLayerVisibility($layerId) { if ($this->selectedLayer && $this->selectedLayer->id == $layerId) { $this->dispatch('notify', 'No puedes ocultar la capa que estás editando'); return; } if (in_array($layerId, $this->visibleLayers)) { $this->visibleLayers = array_values(array_diff($this->visibleLayers, [$layerId])); } else { $this->visibleLayers[] = $layerId; } $this->dispatch('visibilityChanged', $this->visibleLayers); } // ── Select ──────────────────────────────────────────────────────────────── public function selectLayer($layerId) { $this->selectedLayer = Layer::with('features')->find($layerId); if (!$this->selectedLayer) return; if (!in_array($layerId, $this->visibleLayers)) { $this->visibleLayers[] = $layerId; $this->dispatch('visibilityChanged', $this->visibleLayers); } $payload = $this->buildLayerPayload($this->selectedLayer); $this->dispatch('layerSelectedForEdit', [ 'layerId' => $layerId, 'geojson' => $payload['geojson'], 'color' => $payload['color'], ]); $this->dispatch('notify', 'Editando: ' . $this->selectedLayer->name); } // ── Import file ─────────────────────────────────────────────────────────── public function importFile() { $user = Auth::user(); if (!$user->can('upload layers')) { $this->dispatch('notify', 'Sin permisos para subir capas'); return; } $this->validate([ 'uploadFile' => 'required|file|max:51200', 'layerName' => 'required|string|max:255', 'layerColor' => 'nullable|string|size:7', ]); $ext = strtolower($this->uploadFile->getClientOriginalExtension()); $allowed = ['geojson', 'json', 'kmz', 'kml', 'shp', 'dwg', 'zip']; if (!in_array($ext, $allowed)) { $this->dispatch('notify', 'Extensión no permitida. Válidas: ' . implode(', ', $allowed)); return; } $geojson = SpatialFileConverter::convertToGeoJson($this->uploadFile); if (!$geojson) { $this->dispatch('notify', 'No se pudo convertir el archivo. Comprueba que sea GeoJSON, KML o Shapefile válido.'); return; } $layerColor = $this->layerColor ?: '#3b82f6'; $layerName = $this->layerName; try { DB::transaction(function () use ($geojson, $layerColor, $layerName, $user) { $path = $this->uploadFile->store( "uploads/projects/{$this->project->id}/layers", 'public' ); $layer = Layer::create([ 'project_id' => $this->project->id, 'phase_id' => $this->phase->id, 'name' => $layerName, 'color' => $layerColor, 'original_file' => $path, 'uploaded_by' => $user->id, ]); $idx = 0; foreach ($geojson['features'] ?? [] as $fd) { $idx++; $name = trim($fd['properties']['name'] ?? ''); if ($name === '') $name = $layerName . ' — Elemento ' . $idx; Feature::create([ 'layer_id' => $layer->id, 'name' => $name, 'geometry' => $fd['geometry'], 'properties' => $fd['properties'] ?? [], 'template_id' => $fd['properties']['template_id'] ?? null, 'progress' => $fd['properties']['progress'] ?? 0, 'status' => in_array($fd['properties']['status'] ?? '', Feature::STATUSES) ? $fd['properties']['status'] : 'planned', 'responsible' => $fd['properties']['responsible'] ?? null, ]); } $this->visibleLayers[] = $layer->id; }); } catch (\Throwable $e) { $this->dispatch('notify', 'Error al importar: ' . $e->getMessage()); return; } $this->loadLayers(); $this->reset(['uploadFile', 'layerName']); $this->emitInitialLayersData(); $this->dispatch('notify', 'Capa importada correctamente'); } // ── Create empty layer ──────────────────────────────────────────────────── public function createEmptyLayer() { $user = Auth::user(); if (!$user->can('upload layers')) { $this->dispatch('notify', 'Sin permisos para crear capas'); return; } $layer = Layer::create([ 'project_id' => $this->project->id, 'phase_id' => $this->phase->id, 'name' => $this->layerName ?: 'Nueva capa', 'color' => $this->layerColor ?: '#3b82f6', 'original_file' => null, 'uploaded_by' => $user->id, ]); $this->loadLayers(); $this->visibleLayers[] = $layer->id; $this->selectLayer($layer->id); $this->emitInitialLayersData(); $this->dispatch('notify', 'Capa vacía creada. Dibuja elementos en el mapa.'); } // ── Save drawn GeoJSON ──────────────────────────────────────────────────── public function saveManualGeojson($geojsonString) { if (!$this->selectedLayer) { $this->dispatch('notify', 'No hay capa seleccionada'); return; } $geojson = json_decode($geojsonString, true); if (json_last_error() !== JSON_ERROR_NONE || !isset($geojson['features'])) { $this->dispatch('notify', 'GeoJSON inválido'); return; } $layerId = $this->selectedLayer->id; $layerName = $this->selectedLayer->name; try { DB::transaction(function () use ($geojson, $layerId, $layerName) { // forceDelete: reemplazamos completamente los elementos de la capa Feature::where('layer_id', $layerId)->forceDelete(); $idx = 0; foreach ($geojson['features'] as $fd) { $idx++; $name = trim($fd['properties']['name'] ?? ''); if ($name === '') $name = $layerName . ' — Elemento ' . $idx; Feature::create([ 'layer_id' => $layerId, 'name' => $name, 'geometry' => $fd['geometry'], 'properties' => $fd['properties'] ?? [], 'template_id' => $fd['properties']['template_id'] ?? null, 'progress' => $fd['properties']['progress'] ?? 0, 'status' => in_array($fd['properties']['status'] ?? '', Feature::STATUSES) ? $fd['properties']['status'] : 'planned', 'responsible' => $fd['properties']['responsible'] ?? null, ]); } }); } catch (\Throwable $e) { $this->dispatch('notify', 'Error al guardar: ' . $e->getMessage()); return; } $this->loadLayers(); $this->selectLayer($this->selectedLayer->id); $this->emitInitialLayersData(); $this->dispatch('notify', count($geojson['features']) . ' elementos guardados'); } // ── Delete layer ────────────────────────────────────────────────────────── public function deleteLayer($layerId) { $user = Auth::user(); if (!$user->can('delete layers')) abort(403); // Verify it belongs to this phase (prevents cross-project deletion) $layer = Layer::where('id', $layerId)->where('phase_id', $this->phase->id)->first(); if (!$layer) return; if ($layer->original_file) Storage::disk('public')->delete($layer->original_file); $layer->features()->delete(); $layer->delete(); $this->loadLayers(); if ($this->selectedLayer && $this->selectedLayer->id == $layerId) { $this->selectedLayer = null; $this->dispatch('layerSelectedForEdit', null); } $this->emitInitialLayersData(); $this->dispatch('notify', 'Capa eliminada'); } // ── Export GeoJSON ──────────────────────────────────────────────────────── public function exportLayer($layerId) { $layer = Layer::with('features') ->where('id', $layerId) ->where('phase_id', $this->phase->id) ->first(); if (!$layer) return; $fc = [ 'type' => 'FeatureCollection', 'name' => $layer->name, 'features' => $layer->features->map(fn($f) => [ 'type' => 'Feature', 'geometry' => $f->geometry, 'properties' => array_merge($f->properties ?? [], [ 'name' => $f->name, 'progress' => $f->progress, 'status' => $f->status, 'responsible' => $f->responsible, 'template_id' => $f->template_id, ]), ])->values()->toArray(), ]; $filename = preg_replace('/[^a-z0-9_\-]/i', '_', $layer->name) . '.geojson'; return response()->streamDownload(function () use ($fc) { echo json_encode($fc, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); }, $filename, ['Content-Type' => 'application/geo+json']); } // ── Batch assign template / status ──────────────────────────────────────── public function batchAssign($layerId) { $layer = Layer::where('id', $layerId)->where('phase_id', $this->phase->id)->first(); if (!$layer) return; $data = []; if ($this->batchStatus && in_array($this->batchStatus, Feature::STATUSES)) { $data['status'] = $this->batchStatus; } if ($this->batchTemplateId) { $data['template_id'] = (int) $this->batchTemplateId; } if (empty($data)) { $this->dispatch('notify', 'Selecciona un estado o template para asignar'); return; } $count = $layer->features()->update($data); $this->loadLayers(); $this->emitInitialLayersData(); $this->dispatch('notify', "$count elemento(s) actualizados"); } // ── Cancel editing ──────────────────────────────────────────────────────── public function cancelEditing() { $this->selectedLayer = null; $this->dispatch('layerSelectedForEdit', null); } public function render() { return view('livewire.layers.layer-manager'); } }