Files
construprogress/app/Livewire/LayerManager.php
T

401 lines
15 KiB
PHP
Raw Normal View History

2026-05-07 23:31:33 +02:00
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\Attributes\Layout;
use App\Models\Project;
use App\Models\Phase;
use App\Models\Layer;
use App\Models\Feature;
use App\Models\InspectionTemplate;
use App\Services\SpatialFileConverter;
2026-05-07 23:31:33 +02:00
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
2026-05-07 23:31:33 +02:00
use Illuminate\Support\Facades\Storage;
#[Layout('layouts.app')]
class LayerManager extends Component
{
use WithFileUploads;
public Project $project;
public Phase $phase;
2026-05-07 23:31:33 +02:00
public $layers;
public $selectedLayer = null;
public $visibleLayers = [];
2026-05-07 23:31:33 +02:00
public $uploadFile = null;
public $layerName = '';
public $layerColor = '#3b82f6';
2026-05-07 23:31:33 +02:00
// Batch assign
public $templates = [];
public $batchTemplateId = null;
public $batchStatus = '';
2026-05-07 23:31:33 +02:00
public function mount(Project $project, Phase $phase)
{
$this->project = $project;
$this->phase = $phase;
if ($this->phase->project_id !== $this->project->id) abort(404);
$user = Auth::user();
if (!$user->hasRole('Admin') && !$project->users()->where('user_id', $user->id)->exists()) {
abort(403);
2026-05-07 23:31:33 +02:00
}
$this->templates = InspectionTemplate::where('project_id', $this->project->id)->get();
$this->loadLayers();
2026-05-07 23:31:33 +02:00
$this->visibleLayers = $this->layers->pluck('id')->toArray();
$this->emitInitialLayersData();
}
// ── Data loaders ──────────────────────────────────────────────────────────
2026-05-07 23:31:33 +02:00
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())
);
2026-05-07 23:31:33 +02:00
}
private function buildLayerPayload(Layer $layer): array
2026-05-07 23:31:33 +02:00
{
$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',
2026-05-07 23:31:33 +02:00
'features' => $features,
'style' => ['color' => $color],
],
];
}
2026-05-07 23:31:33 +02:00
private function emitInitialLayersData()
{
$this->layers->loadMissing('features');
2026-05-07 23:31:33 +02:00
$this->dispatch('initialLayersData', [
'layers' => $this->layers->map(fn($l) => $this->buildLayerPayload($l)),
'visibleLayers' => $this->visibleLayers,
2026-05-07 23:31:33 +02:00
'selectedLayerId' => $this->selectedLayer?->id,
]);
}
// ── Visibility ────────────────────────────────────────────────────────────
2026-05-07 23:31:33 +02:00
public function toggleLayerVisibility($layerId)
{
if ($this->selectedLayer && $this->selectedLayer->id == $layerId) {
$this->dispatch('notify', 'No puedes ocultar la capa que estás editando');
2026-05-07 23:31:33 +02:00
return;
}
if (in_array($layerId, $this->visibleLayers)) {
$this->visibleLayers = array_values(array_diff($this->visibleLayers, [$layerId]));
2026-05-07 23:31:33 +02:00
} else {
$this->visibleLayers[] = $layerId;
}
$this->dispatch('visibilityChanged', $this->visibleLayers);
}
// ── Select ────────────────────────────────────────────────────────────────
2026-05-07 23:31:33 +02:00
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);
2026-05-07 23:31:33 +02:00
$this->dispatch('layerSelectedForEdit', [
'layerId' => $layerId,
'geojson' => $payload['geojson'],
'color' => $payload['color'],
2026-05-07 23:31:33 +02:00
]);
$this->dispatch('notify', 'Editando: ' . $this->selectedLayer->name);
2026-05-07 23:31:33 +02:00
}
// ── Import file ───────────────────────────────────────────────────────────
2026-05-07 23:31:33 +02:00
public function importFile()
{
$user = Auth::user();
if (!$user->can('upload layers') && !$user->hasRole('Admin')) {
$this->dispatch('notify', 'Sin permisos para subir capas');
2026-05-07 23:31:33 +02:00
return;
}
$this->validate([
'uploadFile' => 'required|file|max:51200',
'layerName' => 'required|string|max:255',
2026-05-07 23:31:33 +02:00
'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));
2026-05-07 23:31:33 +02:00
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.');
2026-05-07 23:31:33 +02:00
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,
2026-05-07 23:31:33 +02:00
]);
$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;
2026-05-07 23:31:33 +02:00
}
$this->loadLayers();
$this->reset(['uploadFile', 'layerName']);
$this->emitInitialLayersData();
$this->dispatch('notify', 'Capa importada correctamente');
2026-05-07 23:31:33 +02:00
}
// ── Create empty layer ────────────────────────────────────────────────────
2026-05-07 23:31:33 +02:00
public function createEmptyLayer()
{
$user = Auth::user();
if (!$user->can('upload layers') && !$user->hasRole('Admin')) {
$this->dispatch('notify', 'Sin permisos para crear capas');
return;
}
2026-05-07 23:31:33 +02:00
$layer = Layer::create([
'project_id' => $this->project->id,
'phase_id' => $this->phase->id,
'name' => $this->layerName ?: 'Nueva capa',
'color' => $this->layerColor ?: '#3b82f6',
2026-05-07 23:31:33 +02:00
'original_file' => null,
'uploaded_by' => $user->id,
2026-05-07 23:31:33 +02:00
]);
2026-05-07 23:31:33 +02:00
$this->loadLayers();
$this->visibleLayers[] = $layer->id;
$this->selectLayer($layer->id);
$this->emitInitialLayersData();
$this->dispatch('notify', 'Capa vacía creada. Dibuja elementos en el mapa.');
2026-05-07 23:31:33 +02:00
}
// ── Save drawn GeoJSON ────────────────────────────────────────────────────
2026-05-07 23:31:33 +02:00
public function saveManualGeojson($geojsonString)
{
if (!$this->selectedLayer) {
$this->dispatch('notify', 'No hay capa seleccionada');
2026-05-07 23:31:33 +02:00
return;
}
2026-05-07 23:31:33 +02:00
$geojson = json_decode($geojsonString, true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($geojson['features'])) {
$this->dispatch('notify', 'GeoJSON inválido');
2026-05-07 23:31:33 +02:00
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;
2026-05-07 23:31:33 +02:00
}
$this->loadLayers();
$this->selectLayer($this->selectedLayer->id);
$this->emitInitialLayersData();
$this->dispatch('notify', count($geojson['features']) . ' elementos guardados');
2026-05-07 23:31:33 +02:00
}
// ── Delete layer ──────────────────────────────────────────────────────────
2026-05-07 23:31:33 +02:00
public function deleteLayer($layerId)
{
$user = Auth::user();
if (!$user->can('delete layers') && !$user->hasRole('Admin')) abort(403);
// Verify it belongs to this phase (prevents cross-project deletion)
$layer = Layer::where('id', $layerId)->where('phase_id', $this->phase->id)->first();
2026-05-07 23:31:33 +02:00
if (!$layer) return;
2026-05-07 23:31:33 +02:00
if ($layer->original_file) Storage::disk('public')->delete($layer->original_file);
$layer->features()->delete();
2026-05-07 23:31:33 +02:00
$layer->delete();
2026-05-07 23:31:33 +02:00
$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;
2026-05-07 23:31:33 +02:00
}
$count = $layer->features()->update($data);
$this->loadLayers();
2026-05-07 23:31:33 +02:00
$this->emitInitialLayersData();
$this->dispatch('notify', "$count elemento(s) actualizados");
2026-05-07 23:31:33 +02:00
}
// ── Cancel editing ────────────────────────────────────────────────────────
2026-05-07 23:31:33 +02:00
public function cancelEditing()
{
$this->selectedLayer = null;
$this->dispatch('layerSelectedForEdit', null);
}
public function render()
{
return view('livewire.layers.layer-manager');
}
}