feat: implementar modal gestión capas y limpieza de stubs duplicados
This commit is contained in:
@@ -1,210 +0,0 @@
|
|||||||
<?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\Services\SpatialFileConverter;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
|
|
||||||
#[Layout('layouts.app')]
|
|
||||||
class LayerManager extends Component
|
|
||||||
{
|
|
||||||
use WithFileUploads;
|
|
||||||
|
|
||||||
public Project $project;
|
|
||||||
public Phase $phase;
|
|
||||||
public $layers;
|
|
||||||
public $selectedLayer = null;
|
|
||||||
public $visibleLayers = []; // IDs de capas visibles
|
|
||||||
|
|
||||||
public $uploadFile = null;
|
|
||||||
public $layerName = '';
|
|
||||||
public $layerColor = '#3b82f6';
|
|
||||||
public $manualGeojson = null;
|
|
||||||
public $drawingMode = false;
|
|
||||||
|
|
||||||
protected $rules = [
|
|
||||||
'uploadFile' => 'required|file|mimes:geojson,kmz,kml,shp,dwg,zip|max:51200',
|
|
||||||
'layerName' => 'required|string|max:255',
|
|
||||||
'layerColor' => 'nullable|string|size:7',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function mount(Project $project, Phase $phase)
|
|
||||||
{
|
|
||||||
$this->project = $project;
|
|
||||||
$this->phase = $phase;
|
|
||||||
$this->loadLayers();
|
|
||||||
if ($this->phase->project_id !== $this->project->id) {
|
|
||||||
abort(404);
|
|
||||||
}
|
|
||||||
// Por defecto todas visibles
|
|
||||||
$this->visibleLayers = $this->layers->pluck('id')->toArray();
|
|
||||||
$this->emitInitialLayersData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function loadLayers()
|
|
||||||
{
|
|
||||||
$this->layers = Layer::where('phase_id', $this->phase->id)->latest()->get();
|
|
||||||
// Eliminar de visibles las que ya no existen
|
|
||||||
$this->visibleLayers = array_intersect($this->visibleLayers, $this->layers->pluck('id')->toArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
private function emitInitialLayersData()
|
|
||||||
{
|
|
||||||
$layersData = $this->layers->map(function($layer) {
|
|
||||||
return [
|
|
||||||
'id' => $layer->id,
|
|
||||||
'geojson' => $layer->geojson_data,
|
|
||||||
'color' => $layer->geojson_data['style']['color'] ?? '#3b82f6',
|
|
||||||
];
|
|
||||||
});
|
|
||||||
$this->dispatch('initialLayersData', [
|
|
||||||
'layers' => $layersData,
|
|
||||||
'visibleLayers' => $this->visibleLayers,
|
|
||||||
'selectedLayerId' => $this->selectedLayer?->id,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggleLayerVisibility($layerId)
|
|
||||||
{
|
|
||||||
if ($this->selectedLayer && $this->selectedLayer->id == $layerId) {
|
|
||||||
session()->flash('info', 'No puedes ocultar la capa que estás editando.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (in_array($layerId, $this->visibleLayers)) {
|
|
||||||
$this->visibleLayers = array_diff($this->visibleLayers, [$layerId]);
|
|
||||||
} else {
|
|
||||||
$this->visibleLayers[] = $layerId;
|
|
||||||
}
|
|
||||||
$this->dispatch('visibilityChanged', $this->visibleLayers);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function selectLayer($layerId)
|
|
||||||
{
|
|
||||||
$this->selectedLayer = Layer::find($layerId);
|
|
||||||
if (!$this->selectedLayer) return;
|
|
||||||
// Asegurar que la capa seleccionada está visible
|
|
||||||
if (!in_array($layerId, $this->visibleLayers)) {
|
|
||||||
$this->visibleLayers[] = $layerId;
|
|
||||||
$this->dispatch('visibilityChanged', $this->visibleLayers);
|
|
||||||
}
|
|
||||||
$geojson = $this->selectedLayer->geojson_data;
|
|
||||||
$this->dispatch('layerSelectedForEdit', [
|
|
||||||
'layerId' => $layerId,
|
|
||||||
'geojson' => $geojson,
|
|
||||||
'color' => $geojson['style']['color'] ?? '#3b82f6',
|
|
||||||
]);
|
|
||||||
session()->flash('info', 'Editando capa: ' . $this->selectedLayer->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function importFile()
|
|
||||||
{
|
|
||||||
$this->validate();
|
|
||||||
$user = Auth::user();
|
|
||||||
if (!$user->can('upload layers') && !$user->hasRole('Admin')) {
|
|
||||||
session()->flash('error', 'Sin permisos.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$projectDir = "uploads/projects/{$this->project->id}/layers";
|
|
||||||
$originalPath = $this->uploadFile->store($projectDir, 'public');
|
|
||||||
$geojson = SpatialFileConverter::convertToGeoJson($this->uploadFile);
|
|
||||||
if (!$geojson) {
|
|
||||||
session()->flash('error', 'Conversión fallida.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$geojson['style'] = ['color' => $this->layerColor ?: '#3b82f6'];
|
|
||||||
|
|
||||||
$layer = Layer::create([
|
|
||||||
'project_id' => $this->project->id,
|
|
||||||
'phase_id' => $this->phase->id,
|
|
||||||
'name' => $this->layerName,
|
|
||||||
'geojson_data' => $geojson,
|
|
||||||
'original_file' => $originalPath,
|
|
||||||
'uploaded_by' => $user->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->loadLayers();
|
|
||||||
$this->visibleLayers[] = $layer->id;
|
|
||||||
$this->reset(['uploadFile', 'layerName']);
|
|
||||||
$this->emitInitialLayersData();
|
|
||||||
session()->flash('message', 'Capa importada.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createEmptyLayer()
|
|
||||||
{
|
|
||||||
$user = Auth::user();
|
|
||||||
$emptyGeojson = [
|
|
||||||
'type' => 'FeatureCollection',
|
|
||||||
'features' => [],
|
|
||||||
'style' => ['color' => $this->layerColor ?: '#3b82f6']
|
|
||||||
];
|
|
||||||
$layer = Layer::create([
|
|
||||||
'project_id' => $this->project->id,
|
|
||||||
'phase_id' => $this->phase->id,
|
|
||||||
'name' => $this->layerName ?: 'Nueva capa',
|
|
||||||
'geojson_data' => $emptyGeojson,
|
|
||||||
'original_file' => null,
|
|
||||||
'uploaded_by' => $user->id,
|
|
||||||
]);
|
|
||||||
$this->loadLayers();
|
|
||||||
$this->visibleLayers[] = $layer->id;
|
|
||||||
$this->selectLayer($layer->id);
|
|
||||||
$this->emitInitialLayersData();
|
|
||||||
session()->flash('message', 'Capa vacía creada y seleccionada.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function saveManualGeojson($geojsonString)
|
|
||||||
{
|
|
||||||
if (!$this->selectedLayer) {
|
|
||||||
session()->flash('error', 'No hay capa seleccionada.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$geojson = json_decode($geojsonString, true);
|
|
||||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
||||||
session()->flash('error', 'GeoJSON inválido.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$geojson['style'] = ['color' => $this->layerColor ?: ($this->selectedLayer->geojson_data['style']['color'] ?? '#3b82f6')];
|
|
||||||
$this->selectedLayer->geojson_data = $geojson;
|
|
||||||
$this->selectedLayer->save();
|
|
||||||
|
|
||||||
$this->loadLayers(); // recargar por si acaso
|
|
||||||
$this->selectLayer($this->selectedLayer->id);
|
|
||||||
$this->emitInitialLayersData();
|
|
||||||
session()->flash('message', 'Capa guardada.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function deleteLayer($layerId)
|
|
||||||
{
|
|
||||||
$user = Auth::user();
|
|
||||||
if (!$user->can('delete layers') && !$user->hasRole('Admin')) abort(403);
|
|
||||||
$layer = Layer::find($layerId);
|
|
||||||
if (!$layer) return;
|
|
||||||
if ($layer->original_file) Storage::disk('public')->delete($layer->original_file);
|
|
||||||
$layer->delete();
|
|
||||||
$this->loadLayers();
|
|
||||||
if ($this->selectedLayer && $this->selectedLayer->id == $layerId) {
|
|
||||||
$this->selectedLayer = null;
|
|
||||||
}
|
|
||||||
$this->emitInitialLayersData();
|
|
||||||
session()->flash('message', 'Capa eliminada.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function cancelEditing()
|
|
||||||
{
|
|
||||||
$this->selectedLayer = null;
|
|
||||||
$this->dispatch('layerSelectedForEdit', null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render()
|
|
||||||
{
|
|
||||||
return view('livewire.layer-manager');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -58,6 +58,9 @@ class LayerManager extends Component
|
|||||||
private function emitInitialLayersData()
|
private function emitInitialLayersData()
|
||||||
{
|
{
|
||||||
$layersData = $this->layers->map(function($layer) {
|
$layersData = $this->layers->map(function($layer) {
|
||||||
|
// Usar el color guardado en BD o el color del formulario
|
||||||
|
$color = $layer->color ?: ($this->layerColor ?: '#3b82f6');
|
||||||
|
|
||||||
// Construir FeatureCollection a partir de los features de esta capa
|
// Construir FeatureCollection a partir de los features de esta capa
|
||||||
$features = $layer->features->map(function($feature) {
|
$features = $layer->features->map(function($feature) {
|
||||||
return [
|
return [
|
||||||
@@ -76,13 +79,13 @@ class LayerManager extends Component
|
|||||||
$geojson = [
|
$geojson = [
|
||||||
'type' => 'FeatureCollection',
|
'type' => 'FeatureCollection',
|
||||||
'features' => $features,
|
'features' => $features,
|
||||||
'style' => ['color' => $this->layerColor ?: '#3b82f6'] // Podrías guardar el color en la tabla layers
|
'style' => ['color' => $color]
|
||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $layer->id,
|
'id' => $layer->id,
|
||||||
'geojson' => $geojson,
|
'geojson' => $geojson,
|
||||||
'color' => $geojson['style']['color'],
|
'color' => $color,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,16 +135,17 @@ class LayerManager extends Component
|
|||||||
];
|
];
|
||||||
})->values()->toArray();
|
})->values()->toArray();
|
||||||
|
|
||||||
|
$color = $this->selectedLayer->color ?: ($this->layerColor ?: '#3b82f6');
|
||||||
$geojson = [
|
$geojson = [
|
||||||
'type' => 'FeatureCollection',
|
'type' => 'FeatureCollection',
|
||||||
'features' => $features,
|
'features' => $features,
|
||||||
'style' => ['color' => $this->layerColor ?: '#3b82f6']
|
'style' => ['color' => $color]
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->dispatch('layerSelectedForEdit', [
|
$this->dispatch('layerSelectedForEdit', [
|
||||||
'layerId' => $layerId,
|
'layerId' => $layerId,
|
||||||
'geojson' => $geojson,
|
'geojson' => $geojson,
|
||||||
'color' => $geojson['style']['color'],
|
'color' => $color,
|
||||||
]);
|
]);
|
||||||
session()->flash('info', 'Editando capa: ' . $this->selectedLayer->name);
|
session()->flash('info', 'Editando capa: ' . $this->selectedLayer->name);
|
||||||
}
|
}
|
||||||
@@ -192,13 +196,14 @@ class LayerManager extends Component
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$geojson['style'] = ['color' => $this->layerColor ?: '#3b82f6'];
|
$layerColor = $this->layerColor ?: '#3b82f6';
|
||||||
|
$geojson['style'] = ['color' => $layerColor];
|
||||||
|
|
||||||
$layer = Layer::create([
|
$layer = Layer::create([
|
||||||
'project_id' => $this->project->id,
|
'project_id' => $this->project->id,
|
||||||
'phase_id' => $this->phase->id,
|
'phase_id' => $this->phase->id,
|
||||||
'name' => $this->layerName,
|
'name' => $this->layerName,
|
||||||
//'geojson_data' => $geojson,
|
'color' => $layerColor,
|
||||||
'original_file' => $originalPath,
|
'original_file' => $originalPath,
|
||||||
'uploaded_by' => $user->id,
|
'uploaded_by' => $user->id,
|
||||||
]);
|
]);
|
||||||
@@ -232,9 +237,9 @@ class LayerManager extends Component
|
|||||||
'project_id' => $this->project->id,
|
'project_id' => $this->project->id,
|
||||||
'phase_id' => $this->phase->id,
|
'phase_id' => $this->phase->id,
|
||||||
'name' => $this->layerName ?: 'Nueva capa',
|
'name' => $this->layerName ?: 'Nueva capa',
|
||||||
|
'color' => $this->layerColor ?: '#3b82f6',
|
||||||
'original_file' => null,
|
'original_file' => null,
|
||||||
'uploaded_by' => $user->id,
|
'uploaded_by' => $user->id,
|
||||||
// Opcional: guarda el color en una columna 'color' de la tabla layers
|
|
||||||
]);
|
]);
|
||||||
$this->loadLayers();
|
$this->loadLayers();
|
||||||
$this->visibleLayers[] = $layer->id;
|
$this->visibleLayers[] = $layer->id;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class ProjectList extends Component
|
|||||||
if ($this->statusFilter) {
|
if ($this->statusFilter) {
|
||||||
$query->where('status', $this->statusFilter);
|
$query->where('status', $this->statusFilter);
|
||||||
}
|
}
|
||||||
$projects = $query->latest()->paginate(10);
|
$projects = $query->with('phases')->latest()->paginate(10);
|
||||||
return view('livewire.projects.project-list', ['projects' => $projects]);
|
return view('livewire.projects.project-list', ['projects' => $projects]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire;
|
|
||||||
|
|
||||||
use Livewire\Component;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
|
|
||||||
use App\Models\Project;
|
|
||||||
use App\Models\Phase;
|
|
||||||
use App\Models\Inspection;
|
|
||||||
use App\Models\InspectionTemplate;
|
|
||||||
use App\Models\Feature;
|
|
||||||
|
|
||||||
class ProjectMap extends Component
|
|
||||||
{
|
|
||||||
public Project $project;
|
|
||||||
public $phases;
|
|
||||||
public $activeLayers = []; // Array of phase IDs to show
|
|
||||||
|
|
||||||
// Editor properties
|
|
||||||
public $selectedFeature = null;
|
|
||||||
public $selectedPhaseId = null;
|
|
||||||
public $editProgress = 0;
|
|
||||||
public $editComment = '';
|
|
||||||
public $editResponsible = '';
|
|
||||||
public $editPhotos = [];
|
|
||||||
public $formFullscreen = false;
|
|
||||||
|
|
||||||
// Propiedades para templates e inspecciones
|
|
||||||
public $templates = [];
|
|
||||||
public $selectedTemplateId = null;
|
|
||||||
public $inspectionFormData = [];
|
|
||||||
public $inspectionHistory = [];
|
|
||||||
public $showInspectionForm = true;
|
|
||||||
|
|
||||||
public function mount(Project $project)
|
|
||||||
{
|
|
||||||
$this->project = $project;
|
|
||||||
$this->phases = $project->phases()->with('currentLayer')->get();
|
|
||||||
$this->activeLayers = $this->phases->pluck('id')->toArray();
|
|
||||||
|
|
||||||
$this->loadTemplates();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggleLayer($phaseId)
|
|
||||||
{
|
|
||||||
if (in_array($phaseId, $this->activeLayers)) {
|
|
||||||
$this->activeLayers = array_diff($this->activeLayers, [$phaseId]);
|
|
||||||
} else {
|
|
||||||
$this->activeLayers[] = $phaseId;
|
|
||||||
}
|
|
||||||
$this->dispatch('layersUpdated', $this->activeLayers);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateProgress($featureId, $newProgress, $comment = null)
|
|
||||||
{
|
|
||||||
$feature = Feature::findOrFail($featureId);
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (!$user->can('update progress') && !$user->hasRole('Admin')) {
|
|
||||||
$this->dispatch('notify', 'Sin permisos');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$feature->progress = min(100, max(0, $newProgress));
|
|
||||||
$feature->save();
|
|
||||||
|
|
||||||
// Actualizar progreso de la fase (sumar promedio)
|
|
||||||
$phase = Phase::find($feature->layer->phase_id);
|
|
||||||
$phase->progress_percent = $phase->features()->avg('progress');
|
|
||||||
$phase->save();
|
|
||||||
|
|
||||||
$phase->progressUpdates()->create([
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'progress_percent' => $phase->progress_percent,
|
|
||||||
'comment' => $comment,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->dispatch('progressUpdated', $featureId, $feature->progress);
|
|
||||||
$this->dispatch('notify', 'Progreso actualizado');
|
|
||||||
$this->editProgress = $feature->progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function loadTemplates()
|
|
||||||
{
|
|
||||||
$this->templates = InspectionTemplate::where('project_id', $this->project->id)->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function selectFeature($featureId, $featureProps)
|
|
||||||
{
|
|
||||||
$feature = Feature::with('template')->find($featureId);
|
|
||||||
if (!$feature) return;
|
|
||||||
|
|
||||||
$this->selectedFeature = $feature;
|
|
||||||
$this->selectedPhaseId = $feature->layer->phase_id;
|
|
||||||
$this->editProgress = $feature->progress;
|
|
||||||
$this->editResponsible = $feature->responsible;
|
|
||||||
$this->selectedTemplateId = $feature->template_id;
|
|
||||||
|
|
||||||
$this->loadInspectionHistory();
|
|
||||||
$this->resetInspectionForm();
|
|
||||||
|
|
||||||
$this->dispatch('featureSelected', $featureId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function saveFeatureProgress()
|
|
||||||
{
|
|
||||||
if (!$this->selectedFeature || !$this->selectedPhaseId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$this->updateProgress($this->selectedPhaseId, $this->editProgress, $this->editComment);
|
|
||||||
$this->editComment = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function resetInspectionForm()
|
|
||||||
{
|
|
||||||
$this->inspectionFormData = [];
|
|
||||||
if ($this->selectedTemplateId) {
|
|
||||||
$template = InspectionTemplate::find($this->selectedTemplateId);
|
|
||||||
if ($template) {
|
|
||||||
foreach ($template->fields as $field) {
|
|
||||||
$this->inspectionFormData[$field['name']] = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function loadInspectionHistory()
|
|
||||||
{
|
|
||||||
if (!$this->selectedFeature || !$this->selectedPhaseId) {
|
|
||||||
$this->inspectionHistory = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$layer = Phase::find($this->selectedPhaseId)->currentLayer;
|
|
||||||
if ($layer) {
|
|
||||||
$this->inspectionHistory = Inspection::where('layer_id', $layer->id)
|
|
||||||
->where('feature_id', $this->selectedFeature['id'])
|
|
||||||
->with('user', 'template')
|
|
||||||
->orderBy('created_at', 'desc')
|
|
||||||
->get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function saveInspection()
|
|
||||||
{
|
|
||||||
if (!$this->selectedFeature || !$this->selectedPhaseId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$this->validate([
|
|
||||||
'selectedTemplateId' => 'required|exists:inspection_templates,id',
|
|
||||||
]);
|
|
||||||
$layer = Phase::find($this->selectedPhaseId)->currentLayer;
|
|
||||||
if (!$layer) return;
|
|
||||||
|
|
||||||
$inspection = Inspection::create([
|
|
||||||
'project_id' => $this->project->id,
|
|
||||||
'layer_id' => $this->selectedFeature->layer_id,
|
|
||||||
'feature_id' => $this->selectedFeature->id,
|
|
||||||
'template_id' => $this->selectedTemplateId,
|
|
||||||
'user_id' => auth()->id(),
|
|
||||||
'data' => $this->inspectionFormData,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Opcional: actualizar el progreso del elemento en el GeoJSON
|
|
||||||
if (isset($this->inspectionFormData['progress'])) {
|
|
||||||
$this->updateProgress($this->selectedPhaseId, $this->inspectionFormData['progress'], 'Inspección registrada');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->loadInspectionHistory();
|
|
||||||
$this->resetInspectionForm();
|
|
||||||
$this->dispatch('notify', 'Inspección guardada');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateFeatureTemplate($templateId)
|
|
||||||
{
|
|
||||||
// Actualizar el template asociado al elemento (podrías guardarlo en la capa GeoJSON)
|
|
||||||
if ($this->selectedFeature && $this->selectedPhaseId) {
|
|
||||||
$layer = Phase::find($this->selectedPhaseId)->currentLayer;
|
|
||||||
if ($layer) {
|
|
||||||
$geojson = $layer->geojson_data;
|
|
||||||
foreach ($geojson['features'] as &$feature) {
|
|
||||||
if ($feature['properties']['id'] == $this->selectedFeature['id']) {
|
|
||||||
$feature['properties']['template_id'] = $templateId;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$layer->geojson_data = $geojson;
|
|
||||||
$layer->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$this->selectedTemplateId = $templateId;
|
|
||||||
$this->resetInspectionForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggleFullscreen()
|
|
||||||
{
|
|
||||||
$this->formFullscreen = !$this->formFullscreen;
|
|
||||||
if (!$this->formFullscreen) {
|
|
||||||
$this->dispatch('mapResize');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añadir al final de la clase ProjectMap
|
|
||||||
public function refreshTemplates()
|
|
||||||
{
|
|
||||||
$this->templates = InspectionTemplate::where('project_id', $this->project->id)->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asignar template ahora actualiza el campo template_id
|
|
||||||
public function assignTemplateToFeature($templateId)
|
|
||||||
{
|
|
||||||
if (!$this->selectedFeature) return;
|
|
||||||
$this->selectedFeature->template_id = $templateId;
|
|
||||||
$this->selectedFeature->save();
|
|
||||||
$this->selectedTemplateId = $templateId;
|
|
||||||
$this->resetInspectionForm();
|
|
||||||
$this->dispatch('notify', 'Template asignado');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render()
|
|
||||||
{
|
|
||||||
return view('livewire.projects.project-map', [
|
|
||||||
'project' => $this->project,
|
|
||||||
'phases' => $this->phases,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@ class ProjectMap extends Component
|
|||||||
public Project $project;
|
public Project $project;
|
||||||
public $phases;
|
public $phases;
|
||||||
public $activeLayers = [];
|
public $activeLayers = [];
|
||||||
|
public $showLayerModal = false;
|
||||||
|
|
||||||
// Editor properties
|
// Editor properties
|
||||||
public $selectedFeature = null; // será instancia de Feature
|
public $selectedFeature = null; // será instancia de Feature
|
||||||
@@ -57,6 +58,16 @@ class ProjectMap extends Component
|
|||||||
$this->dispatch('layersUpdated', $this->activeLayers);
|
$this->dispatch('layersUpdated', $this->activeLayers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function openLayerModal()
|
||||||
|
{
|
||||||
|
$this->showLayerModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeLayerModal()
|
||||||
|
{
|
||||||
|
$this->showLayerModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actualizar el progreso de un Feature y recalcular el progreso de la fase.
|
* Actualizar el progreso de un Feature y recalcular el progreso de la fase.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
class Layer extends Model
|
class Layer extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'project_id', 'phase_id', 'name', 'geojson_data', 'original_file', 'uploaded_by'
|
'project_id', 'phase_id', 'name', 'color', 'geojson_data', 'original_file', 'uploaded_by'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<div>
|
||||||
|
@if(session()->has('message'))
|
||||||
|
<div class="alert alert-success mb-2">{{ session('message') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">Subir capa a proyecto</h2>
|
||||||
|
<form wire:submit.prevent="upload">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">Archivo GeoJSON / KML</label>
|
||||||
|
<input type="file" wire:model="file" class="file-input file-input-bordered" accept=".geojson,.kml,.kmz,.zip" />
|
||||||
|
@error('file') <span class="text-error">{{ $message }}</span> @enderror
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary mt-4">Subir</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
<div class="flex flex-col h-screen">
|
|
||||||
{{-- Cabecera fija --}}
|
|
||||||
<div class="flex justify-between items-center mb-4 px-4 pt-4 flex-shrink-0">
|
|
||||||
<h1 class="text-2xl font-bold">Gestión de elementos - {{ $phase->name }}</h1>
|
|
||||||
<a href="{{ route('projects.map', $project) }}" class="btn btn-outline btn-sm">← Volver al mapa</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 overflow-hidden px-4 pb-4">
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2 h-full">
|
|
||||||
{{-- Columna izquierda --}}
|
|
||||||
<div class="space-y-4 overflow-y-auto h-full pr-2">
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title">Importar archivo</h2>
|
|
||||||
<form wire:submit.prevent="importFile">
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">Nombre de capa</label>
|
|
||||||
<input type="text" wire:model="layerName" class="input input-bordered" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">Color</label>
|
|
||||||
<input type="color" wire:model="layerColor" class="input input-bordered">
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">Archivo (GeoJSON, KML, Shapefile.zip, DWG)</label></br>
|
|
||||||
<input type="file" wire:model="uploadFile" class="file-input file-input-bordered">
|
|
||||||
@error('uploadFile') <span class="text-error">{{ $message }}</span> @enderror
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary w-full mt-2">Subir y convertir</button>
|
|
||||||
</form>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<button wire:click="createEmptyLayer" class="btn btn-secondary w-full">Crear capa vacía</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title">Capas existentes</h2>
|
|
||||||
<div class="space-y-2">
|
|
||||||
@foreach($layers as $layer)
|
|
||||||
<div class="flex justify-between items-center p-2 border rounded">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="w-4 h-4 rounded-full" style="background: {{ $layer->geojson_data['style']['color'] ?? '#ccc' }}"></span>
|
|
||||||
{{ $layer->name }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button wire:click="selectLayer({{ $layer->id }})" class="btn btn-xs btn-info"><x-heroicon-s-trash />Editar</button>
|
|
||||||
<button wire:click="deleteLayer({{ $layer->id }})" class="btn btn-xs btn-error"><x-heroicon-s-trash />Eliminar</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
@if($layers->isEmpty())
|
|
||||||
<p class="text-center">Sin capas. Crea una o importa.</p>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Columna derecha: mapa PERSISTENTE --}}
|
|
||||||
<div class="lg:col-span-2 flex flex-col h-full">
|
|
||||||
<div class="card bg-base-100 shadow-xl flex-1 flex flex-col">
|
|
||||||
<div class="card-body flex-1 flex flex-col p-2">
|
|
||||||
<h2 class="card-title">Editor gráfico</h2>
|
|
||||||
@if($selectedLayer)
|
|
||||||
<div class="mt-3 flex gap-2">
|
|
||||||
<button id="saveDrawingBtn" class="btn btn-primary">💾 Guardar cambios</button>
|
|
||||||
<button wire:click="cancelEditing" class="btn btn-outline">Cancelar edición</button>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
<div id="permanentMap" style="flex: 1; min-height: 500px; width: 100%; background: #e2e8f0;" wire:ignore></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@push('scripts')
|
|
||||||
<script>
|
|
||||||
// Variables globales
|
|
||||||
let mapInitialized = false;
|
|
||||||
let mapInstance = null;
|
|
||||||
let drawnItems = null; // Grupo que contiene todos los elementos editables
|
|
||||||
let currentColor = '#3b82f6';
|
|
||||||
|
|
||||||
function initializeMap() {
|
|
||||||
if (mapInitialized) return;
|
|
||||||
const container = document.getElementById('permanentMap');
|
|
||||||
if (!container) {
|
|
||||||
setTimeout(initializeMap, 200);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const center = [{{ $project->lat }}, {{ $project->lng }}];
|
|
||||||
mapInstance = L.map('permanentMap').setView(center, 16);
|
|
||||||
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(mapInstance);
|
|
||||||
|
|
||||||
// Grupo que contendrá todos los elementos (sin anidar)
|
|
||||||
drawnItems = L.featureGroup().addTo(mapInstance);
|
|
||||||
|
|
||||||
const drawControl = new L.Control.Draw({
|
|
||||||
edit: { featureGroup: drawnItems },
|
|
||||||
draw: {
|
|
||||||
polygon: true,
|
|
||||||
polyline: true,
|
|
||||||
marker: true,
|
|
||||||
circle: false,
|
|
||||||
rectangle: false,
|
|
||||||
circlemarker: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
mapInstance.addControl(drawControl);
|
|
||||||
|
|
||||||
// Cuando se dibuja un nuevo elemento, lo añadimos al grupo
|
|
||||||
mapInstance.on(L.Draw.Event.CREATED, (e) => {
|
|
||||||
const layer = e.layer;
|
|
||||||
// Asignar propiedades por defecto (necesario para guardar)
|
|
||||||
if (!layer.feature) {
|
|
||||||
layer.feature = {
|
|
||||||
type: 'Feature',
|
|
||||||
properties: { name: 'Nuevo elemento', progress: 0, responsible: '' }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
drawnItems.addLayer(layer);
|
|
||||||
});
|
|
||||||
|
|
||||||
mapInitialized = true;
|
|
||||||
console.log('Mapa inicializado correctamente');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limpia todas las capas actuales
|
|
||||||
function clearAllLayers() {
|
|
||||||
if (drawnItems) {
|
|
||||||
drawnItems.clearLayers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cargar una capa GeoJSON haciendo cada feature editable individualmente
|
|
||||||
function loadGeoJSONLayer(geojson, color) {
|
|
||||||
console.log('Cargando capa GeoJSON:', geojson);
|
|
||||||
if (!mapInstance) return;
|
|
||||||
clearAllLayers();
|
|
||||||
currentColor = color;
|
|
||||||
|
|
||||||
if (!geojson || !geojson.features || geojson.features.length === 0) {
|
|
||||||
console.warn('GeoJSON sin features');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
geojson.features.forEach(feature => {
|
|
||||||
// Crear capa GeoJSON para este feature individual
|
|
||||||
const tempLayer = L.geoJSON(feature, {
|
|
||||||
style: { color: color, weight: 3, opacity: 0.8, fillOpacity: 0.3 },
|
|
||||||
onEachFeature: (f, l) => {
|
|
||||||
l.feature = f; // guardar propiedades originales
|
|
||||||
const props = f.properties;
|
|
||||||
const content = `<b>${props.name || 'Elemento'}</b><br>
|
|
||||||
Progreso: ${props.progress || 0}%<br>
|
|
||||||
Responsable: ${props.responsible || '-'}`;
|
|
||||||
l.bindPopup(content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// L.geoJSON devuelve un FeatureGroup, extraemos cada capa hija
|
|
||||||
tempLayer.eachLayer(subLayer => {
|
|
||||||
subLayer.feature = feature;
|
|
||||||
drawnItems.addLayer(subLayer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ajustar zoom a los elementos
|
|
||||||
if (drawnItems.getLayers().length > 0) {
|
|
||||||
const bounds = drawnItems.getBounds();
|
|
||||||
if (bounds.isValid()) {
|
|
||||||
mapInstance.fitBounds(bounds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guardar todos los elementos (modificados y nuevos)
|
|
||||||
function saveAllLayers() {
|
|
||||||
if (!drawnItems) return;
|
|
||||||
const features = [];
|
|
||||||
drawnItems.eachLayer(layer => {
|
|
||||||
const feature = extractFeatureFromLayer(layer);
|
|
||||||
if (feature) features.push(feature);
|
|
||||||
});
|
|
||||||
if (features.length === 0) {
|
|
||||||
alert('No hay elementos para guardar.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const finalGeojson = {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: features,
|
|
||||||
style: { color: document.querySelector('input[type="color"]')?.value || '#3b82f6' }
|
|
||||||
};
|
|
||||||
console.log('Guardando GeoJSON:', finalGeojson);
|
|
||||||
@this.saveManualGeojson(JSON.stringify(finalGeojson));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraer geometría y propiedades de una capa Leaflet
|
|
||||||
function extractFeatureFromLayer(layer) {
|
|
||||||
let geojson = layer.toGeoJSON();
|
|
||||||
if (geojson && geojson.geometry) {
|
|
||||||
// Si la capa tiene propiedades asociadas, las conservamos
|
|
||||||
if (!geojson.properties && layer.feature?.properties) {
|
|
||||||
geojson.properties = layer.feature.properties;
|
|
||||||
} else if (!geojson.properties) {
|
|
||||||
geojson.properties = { name: 'Elemento', progress: 0, responsible: '' };
|
|
||||||
}
|
|
||||||
return geojson;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupSaveButton() {
|
|
||||||
const btn = document.getElementById('saveDrawingBtn');
|
|
||||||
if (btn && !btn.hasAttribute('data-listener')) {
|
|
||||||
btn.setAttribute('data-listener', 'true');
|
|
||||||
btn.addEventListener('click', saveAllLayers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicialización con Livewire
|
|
||||||
document.addEventListener('livewire:init', () => {
|
|
||||||
console.log('Livewire inicializado');
|
|
||||||
initializeMap();
|
|
||||||
setupSaveButton();
|
|
||||||
// Escuchar selección de capa (Livewire v3)
|
|
||||||
Livewire.on('layerSelectedForEdit', (data) => {
|
|
||||||
const payload = Array.isArray(data) ? data[0] : data;
|
|
||||||
console.log('Evento layerSelectedForEdit recibido:', payload);
|
|
||||||
if (!mapInitialized) {
|
|
||||||
setTimeout(() => Livewire.dispatch('layerSelectedForEdit', payload), 200);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (payload && payload.geojson) {
|
|
||||||
loadGeoJSONLayer(payload.geojson, payload.color);
|
|
||||||
} else {
|
|
||||||
clearAllLayers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Para navegación SPA
|
|
||||||
document.addEventListener('livewire:navigated', () => {
|
|
||||||
if (!mapInitialized) initializeMap();
|
|
||||||
setupSaveButton();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@endpush
|
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
wire:change="toggleLayerVisibility({{ $layer->id }})"
|
wire:change="toggleLayerVisibility({{ $layer->id }})"
|
||||||
@if(in_array($layer->id, $visibleLayers)) checked @endif
|
@if(in_array($layer->id, $visibleLayers)) checked @endif
|
||||||
class="checkbox checkbox-sm" />
|
class="checkbox checkbox-sm" />
|
||||||
<span class="w-4 h-4 rounded-full" style="background: {{ $layer->geojson_data['style']['color'] ?? '#ccc' }}"></span>
|
<span class="w-4 h-4 rounded-full" style="background: {{ $layer->color ?? '#ccc' }}"></span>
|
||||||
<span class="{{ $selectedLayer && $selectedLayer->id == $layer->id ? 'font-bold text-primary' : '' }}">
|
<span class="{{ $selectedLayer && $selectedLayer->id == $layer->id ? 'font-bold text-primary' : '' }}">
|
||||||
{{ $layer->name }}
|
{{ $layer->name }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,3 +1,60 @@
|
|||||||
<div>
|
<div>
|
||||||
{{-- If you look to others for fulfillment, you will never truly be fulfilled. --}}
|
@if(session()->has('message'))
|
||||||
|
<div class="alert alert-success mb-2">{{ session('message') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">Progreso de fase: {{ $phase->name }}</h2>
|
||||||
|
<p class="text-sm opacity-70">{{ $phase->project->name ?? '' }}</p>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-4 mb-2">
|
||||||
|
<div class="bg-primary h-4 rounded-full transition-all duration-500" style="width: {{ $phase->progress_percent }}%"></div>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-bold text-center">{{ $phase->progress_percent }}%</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form wire:submit.prevent="updateProgressManual" class="mt-6 space-y-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">Nuevo porcentaje de progreso</label>
|
||||||
|
<input type="range" min="0" max="100" wire:model.live="progress" class="range range-primary" />
|
||||||
|
<div class="flex justify-between text-xs px-2">
|
||||||
|
<span>0%</span><span>25%</span><span>50%</span><span>75%</span><span>100%</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-1 font-bold">{{ $progress }}%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">Comentario (opcional)</label>
|
||||||
|
<textarea wire:model="comment" rows="3" class="textarea textarea-bordered" placeholder="Notas sobre el progreso..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-full">Actualizar progreso</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@if($phase->progressUpdates->count() > 0)
|
||||||
|
<div class="mt-6">
|
||||||
|
<h3 class="font-semibold mb-2">Historial</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
@foreach($phase->progressUpdates()->latest()->take(10)->get() as $update)
|
||||||
|
<div class="border rounded p-2 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="font-medium">{{ $update->progress_percent }}%</span>
|
||||||
|
<span class="text-xs opacity-60">{{ $update->created_at->diffForHumans() }}</span>
|
||||||
|
</div>
|
||||||
|
@if($update->comment)
|
||||||
|
<p class="text-xs mt-1">{{ $update->comment }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{{ url()->previous() }}" class="btn btn-outline btn-sm">← Volver</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,19 +19,32 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button wire:click="$dispatch('centerMap')" class="btn btn-sm btn-outline w-full">
|
<button wire:click="openLayerModal" class="btn btn-sm btn-primary w-full">
|
||||||
|
📂 Gestión de capas
|
||||||
|
</button>
|
||||||
|
<button wire:click="$dispatch('centerMap')" class="btn btn-sm btn-outline w-full mt-2">
|
||||||
📍 Centrar en proyecto
|
📍 Centrar en proyecto
|
||||||
</button>
|
</button>
|
||||||
<button onclick="getUserLocation()" class="btn btn-sm btn-outline w-full mt-2">
|
<button onclick="getUserLocation()" class="btn btn-sm btn-outline w-full mt-2">
|
||||||
🧭 Mi ubicación
|
🧭 Mi ubicación
|
||||||
</button>
|
</button>
|
||||||
<a href="{{ route('layers.manage', ['project' => $project->id, 'phase' => $phase->id]) }}" class="btn btn-sm btn-info w-full mt-2">
|
|
||||||
✏️ Gestión de capas y elementos
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Gestión de Capas -->
|
||||||
|
@if($showLayerModal)
|
||||||
|
<div class="fixed inset-0 bg-black bg-opacity-50 z-[2000] flex items-center justify-center p-4">
|
||||||
|
<div class="bg-base-100 p-6 rounded-box shadow-2xl w-full max-w-lg">
|
||||||
|
<h3 class="font-bold text-lg mb-4">Gestión de capas</h3>
|
||||||
|
<p class="text-sm mb-4">Configura las capas y elementos visibles del proyecto.</p>
|
||||||
|
<div class="modal-action">
|
||||||
|
<button wire:click="closeLayerModal" class="btn btn-sm btn-primary">Cerrar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<!-- Columna derecha: Editor de progreso -->
|
<!-- Columna derecha: Editor de progreso -->
|
||||||
<div class="w-full lg:w-1/3 transition-all duration-300" :class="{'lg:w-full': formFullscreen}">
|
<div class="w-full lg:w-1/3 transition-all duration-300" :class="{'lg:w-full': formFullscreen}">
|
||||||
<div class="card bg-base-100 shadow-xl h-full flex flex-col">
|
<div class="card bg-base-100 shadow-xl h-full flex flex-col">
|
||||||
|
|||||||
Reference in New Issue
Block a user