Initial commit - construprogress app

This commit is contained in:
2026-05-07 23:31:33 +02:00
commit 156aa14bbb
157 changed files with 21654 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Livewire\Actions;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
class Logout
{
/**
* Log the current user out of the application.
*/
public function __invoke(): void
{
Auth::guard('web')->logout();
Session::invalidate();
Session::regenerateToken();
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace App\Livewire\Forms;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Validate;
use Livewire\Form;
class LoginForm extends Form
{
#[Validate('required|string|email')]
public string $email = '';
#[Validate('required|string')]
public string $password = '';
#[Validate('boolean')]
public bool $remember = false;
/**
* Attempt to authenticate the request's credentials.
*
* @throws ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only(['email', 'password']), $this->remember)) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'form.email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the authentication request is not rate limited.
*/
protected function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout(request()));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'form.email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the authentication rate limiting throttle key.
*/
protected function throttleKey(): string
{
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
}
}
+210
View File
@@ -0,0 +1,210 @@
<?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');
}
}
+307
View File
@@ -0,0 +1,307 @@
<?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 App\Models\Feature;
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::with('features')->where('phase_id', $this->phase->id)->latest()->get();
$this->visibleLayers = array_intersect($this->visibleLayers, $this->layers->pluck('id')->toArray());
}
private function emitInitialLayersData()
{
$layersData = $this->layers->map(function($layer) {
// Construir FeatureCollection a partir de los features de esta capa
$features = $layer->features->map(function($feature) {
return [
'type' => 'Feature',
'id' => $feature->id,
'geometry' => $feature->geometry,
'properties' => [
'name' => $feature->name,
'progress' => $feature->progress,
'responsible' => $feature->responsible,
'template_id' => $feature->template_id,
]
];
})->values()->toArray();
$geojson = [
'type' => 'FeatureCollection',
'features' => $features,
'style' => ['color' => $this->layerColor ?: '#3b82f6'] // Podrías guardar el color en la tabla layers
];
return [
'id' => $layer->id,
'geojson' => $geojson,
'color' => $geojson['style']['color'],
];
});
$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::with('features')->find($layerId);
if (!$this->selectedLayer) return;
if (!in_array($layerId, $this->visibleLayers)) {
$this->visibleLayers[] = $layerId;
$this->dispatch('visibilityChanged', $this->visibleLayers);
}
// Construir el GeoJSON desde los features de la capa seleccionada
$features = $this->selectedLayer->features->map(function($feature) {
return [
'type' => 'Feature',
'id' => $feature->id,
'geometry' => $feature->geometry,
'properties' => [
'name' => $feature->name,
'progress' => $feature->progress,
'responsible' => $feature->responsible,
'template_id' => $feature->template_id,
]
];
})->values()->toArray();
$geojson = [
'type' => 'FeatureCollection',
'features' => $features,
'style' => ['color' => $this->layerColor ?: '#3b82f6']
];
$this->dispatch('layerSelectedForEdit', [
'layerId' => $layerId,
'geojson' => $geojson,
'color' => $geojson['style']['color'],
]);
session()->flash('info', 'Editando capa: ' . $this->selectedLayer->name);
}
public function importFile()
{
$user = Auth::user();
if (!$user->can('upload layers') && !$user->hasRole('Admin')) {
session()->flash('error', 'Sin permisos.');
return;
}
// Validar campos obligatorios y tamaño máximo
$this->validate([
'uploadFile' => 'required|file|max:51200',
'layerName' => 'required|string|max:255',
'layerColor' => 'nullable|string|size:7',
]);
$extension = strtolower($this->uploadFile->getClientOriginalExtension());
$mime = $this->uploadFile->getMimeType();
$allowedExtensions = ['geojson', 'kmz', 'kml', 'shp', 'dwg', 'zip'];
$allowedMimes = [
'application/vnd.google-earth.kml+xml',
'application/vnd.google-earth.kmz',
'application/zip',
'application/x-zip-compressed',
'application/x-shapefile',
'image/vnd.dwg',
'application/acad',
'application/geo+json',
'text/xml', // ✅ Aceptar KML con text/xml
'application/xml', // ✅ Alternativa
];
if (!in_array($extension, $allowedExtensions) && !in_array($mime, $allowedMimes)) {
session()->flash('error', 'Tipo de archivo no permitido. Extensiones válidas: ' . implode(', ', $allowedExtensions));
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. Asegúrate de que el archivo sea válido (KML, GeoJSON, etc.).');
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,
]);
// Crear features a partir del GeoJSON
if (isset($geojson['features'])) {
foreach ($geojson['features'] as $featureData) {
Feature::create([
'layer_id' => $layer->id,
'name' => $featureData['properties']['name'] ?? null,
'geometry' => $featureData['geometry'],
'properties' => $featureData['properties'] ?? [],
'template_id' => $featureData['properties']['template_id'] ?? null,
'progress' => $featureData['properties']['progress'] ?? 0,
'responsible' => $featureData['properties']['responsible'] ?? null,
]);
}
}
$this->loadLayers();
$this->visibleLayers[] = $layer->id;
$this->reset(['uploadFile', 'layerName']);
$this->emitInitialLayersData();
session()->flash('message', 'Capa importada correctamente.');
}
public function createEmptyLayer()
{
$user = Auth::user();
$layer = Layer::create([
'project_id' => $this->project->id,
'phase_id' => $this->phase->id,
'name' => $this->layerName ?: 'Nueva capa',
'original_file' => null,
'uploaded_by' => $user->id,
// Opcional: guarda el color en una columna 'color' de la tabla layers
]);
$this->loadLayers();
$this->visibleLayers[] = $layer->id;
$this->selectLayer($layer->id);
$this->emitInitialLayersData();
session()->flash('message', 'Capa vacía creada. Usa el editor para añadir elementos.');
}
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 || !isset($geojson['features'])) {
session()->flash('error', 'GeoJSON inválido.');
return;
}
// Eliminar todos los features existentes de esta capa
$this->selectedLayer->features()->delete();
// Crear nuevos features a partir del GeoJSON
foreach ($geojson['features'] as $featureData) {
Feature::create([
'layer_id' => $this->selectedLayer->id,
'name' => $featureData['properties']['name'] ?? null,
'geometry' => $featureData['geometry'],
'properties' => $featureData['properties'] ?? [],
'template_id' => $featureData['properties']['template_id'] ?? null,
'progress' => $featureData['properties']['progress'] ?? 0,
'responsible' => $featureData['properties']['responsible'] ?? null,
]);
}
$this->loadLayers();
$this->selectLayer($this->selectedLayer->id);
$this->emitInitialLayersData();
session()->flash('message', 'Capa guardada con ' . count($geojson['features']) . ' elementos.');
}
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->features()->delete(); // opcional, si no usas cascade
$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.layers.layer-manager');
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Livewire;
use Livewire\Component;
class LayerUpload extends Component
{
public function render()
{
return view('livewire.layer-upload');
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Project;
use App\Models\Phase;
class PhaseList extends Component
{
public Project $project;
public $phases;
public function mount(Project $project)
{
$this->project = $project;
$this->phases = $project->phases;
}
public function addPhase()
{
$this->project->phases()->create([
'name' => 'Nueva fase',
'order' => $this->phases->count() + 1,
'color' => '#'.substr(md5(rand()), 0, 6)
]);
$this->phases = $this->project->phases()->get();
session()->flash('message', 'Fase agregada');
}
public function deletePhase($phaseId)
{
Phase::find($phaseId)->delete();
$this->phases = $this->project->phases()->get();
}
public function render()
{
return view('livewire.phase-list');
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Phase;
class PhaseProgress extends Component
{
public Phase $phase;
public $progress;
public $comment = '';
public function mount(Phase $phase)
{
$this->phase = $phase;
$this->progress = $phase->progress_percent;
}
public function updateProgressManual()
{
$this->validate(['progress' => 'required|integer|min:0|max:100']);
$this->phase->progress_percent = $this->progress;
$this->phase->save();
$this->phase->progressUpdates()->create([
'user_id' => auth()->id(),
'progress_percent' => $this->progress,
'comment' => $this->comment,
]);
$this->dispatch('progressUpdated', $this->phase->id, $this->progress);
session()->flash('message', 'Progreso actualizado');
}
public function render()
{
return view('livewire.phase-progress');
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Livewire;
use Livewire\Component;
class ProjectForm extends Component
{
public function render()
{
return view('livewire.projects.project-form');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\Project;
use Illuminate\Support\Facades\Auth;
class ProjectList extends Component
{
use WithPagination;
public $search = '';
public $statusFilter = '';
public function deleteProject($id)
{
$project = Project::findOrFail($id);
if (Auth::user()->can('delete projects')) {
$project->delete();
session()->flash('message', 'Proyecto eliminado');
}
}
public function render()
{
$query = Project::accessibleBy(Auth::user());
if ($this->search) {
$query->where('name', 'like', '%' . $this->search . '%');
}
if ($this->statusFilter) {
$query->where('status', $this->statusFilter);
}
$projects = $query->latest()->paginate(10);
return view('livewire.projects.project-list', ['projects' => $projects]);
}
}
+227
View File
@@ -0,0 +1,227 @@
<?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,
]);
}
}
+222
View File
@@ -0,0 +1,222 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Auth;
use App\Models\Project;
use App\Models\Phase;
use App\Models\Layer;
use App\Models\Feature;
use App\Models\Inspection;
use App\Models\InspectionTemplate;
class ProjectMap extends Component
{
public Project $project;
public $phases;
public $activeLayers = [];
// Editor properties
public $selectedFeature = null; // será instancia de Feature
public $selectedPhaseId = null;
public $editProgress = 0;
public $editComment = '';
public $editResponsible = '';
public $editPhotos = [];
public $formFullscreen = false;
// Templates e inspecciones
public $templates = [];
public $selectedTemplateId = null;
public $inspectionFormData = [];
public $inspectionHistory = [];
public function mount(Project $project)
{
$this->project = $project;
// Cargar fases con sus capas y los features de esas capas (para mostrarlos en el mapa)
$this->phases = $project->phases()->with(['layers.features'])->get();
// Por defecto mostrar todas las capas activas (todas las fases que tengan alguna capa con features)
$this->activeLayers = $this->phases->pluck('id')->toArray();
$this->loadTemplates();
}
public function loadTemplates()
{
$this->templates = InspectionTemplate::where('project_id', $this->project->id)->get();
}
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);
}
/**
* Actualizar el progreso de un Feature y recalcular el progreso de la fase.
*/
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;
}
$oldProgress = $feature->progress;
$feature->progress = min(100, max(0, $newProgress));
$feature->save();
// Recalcular el progreso de la fase (promedio de todos sus features)
$phase = Phase::find($feature->layer->phase_id);
$phase->progress_percent = $phase->features()->avg('progress') ?: 0;
$phase->save();
// Registrar la actualización en progress_updates
$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');
// Si el feature seleccionado es el mismo, actualizar la propiedad local
if ($this->selectedFeature && $this->selectedFeature->id == $featureId) {
$this->selectedFeature->progress = $feature->progress;
$this->editProgress = $feature->progress;
}
}
/**
* Seleccionar un Feature al hacer clic en el mapa.
*/
public function selectFeature($featureId)
{
$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->editPhotos = $feature->properties['photos'] ?? [];
$this->selectedTemplateId = $feature->template_id;
$this->loadInspectionHistory();
$this->resetInspectionForm();
$this->dispatch('featureSelected', $featureId);
}
/**
* Cargar el historial de inspecciones del feature seleccionado.
*/
public function loadInspectionHistory()
{
if (!$this->selectedFeature) {
$this->inspectionHistory = [];
return;
}
$this->inspectionHistory = Inspection::where('feature_id', $this->selectedFeature->id)
->with('user', 'template')
->orderBy('created_at', 'desc')
->get();
}
/**
* Reiniciar el formulario de inspección según el template seleccionado.
*/
public function resetInspectionForm()
{
$this->inspectionFormData = [];
if ($this->selectedTemplateId) {
$template = InspectionTemplate::find($this->selectedTemplateId);
if ($template) {
foreach ($template->fields as $field) {
$this->inspectionFormData[$field['name']] = '';
}
}
}
}
/**
* Guardar una nueva inspección.
*/
public function saveInspection()
{
if (!$this->selectedFeature || !$this->selectedTemplateId) {
$this->dispatch('notify', 'Selecciona un elemento y un template.');
return;
}
$this->validate([
'selectedTemplateId' => 'required|exists:inspection_templates,id',
]);
$template = InspectionTemplate::find($this->selectedTemplateId);
foreach ($template->fields as $field) {
if (($field['required'] ?? false) && empty($this->inspectionFormData[$field['name']])) {
$this->dispatch('notify', "El campo {$field['label']} es obligatorio.");
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,
]);
// Si el template tiene un campo llamado 'progress', actualizar el progreso del feature
if (isset($this->inspectionFormData['progress'])) {
$this->updateProgress($this->selectedFeature->id, (int)$this->inspectionFormData['progress'], 'Inspección registrada');
}
$this->loadInspectionHistory();
$this->resetInspectionForm();
$this->dispatch('notify', 'Inspección guardada correctamente');
}
/**
* Asignar un template al feature seleccionado.
*/
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 al elemento');
}
public function toggleFullscreen()
{
$this->formFullscreen = !$this->formFullscreen;
if (!$this->formFullscreen) {
$this->dispatch('mapResize');
}
}
public function render()
{
return view('livewire.projects.project-map', [
'project' => $this->project,
'phases' => $this->phases,
]);
}
}
+129
View File
@@ -0,0 +1,129 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\InspectionTemplate;
use App\Models\Project;
class TemplateManager extends Component
{
public $project;
public $templates;
public $editingTemplate = null;
public $showForm = false; // Controla si mostrar el formulario
public $form = [
'name' => '',
'description' => '',
'fields' => [],
];
public $fieldTypes = [
'text' => 'Texto corto',
'textarea' => 'Texto largo',
'integer' => 'Número entero',
'decimal' => 'Número decimal',
'percentage' => 'Porcentaje (0-100)',
'boolean' => 'Sí/No (checkbox)',
'date' => 'Fecha',
'select' => 'Lista desplegable',
];
public function mount(Project $project)
{
$this->project = $project;
$this->loadTemplates();
}
public function loadTemplates()
{
$this->templates = InspectionTemplate::where('project_id', $this->project->id)->get();
}
public function newTemplate()
{
$this->resetForm();
$this->editingTemplate = null;
$this->showForm = true;
}
public function editTemplate($id)
{
$template = InspectionTemplate::find($id);
$this->form = $template->only(['name', 'description', 'fields']);
$this->editingTemplate = $id;
$this->showForm = true;
}
public function cancelForm()
{
$this->showForm = false;
$this->resetForm();
}
public function resetForm()
{
$this->form = [
'name' => '',
'description' => '',
'fields' => [],
];
$this->editingTemplate = null;
}
public function addField()
{
$this->form['fields'][] = [
'name' => '',
'label' => '',
'type' => 'text',
'options' => [],
'required' => false,
'min' => null,
'max' => null,
'step' => null,
];
}
public function removeField($index)
{
unset($this->form['fields'][$index]);
$this->form['fields'] = array_values($this->form['fields']);
}
public function saveTemplate()
{
$this->validate([
'form.name' => 'required|string|max:255',
'form.fields' => 'array',
]);
if ($this->editingTemplate) {
$template = InspectionTemplate::find($this->editingTemplate);
$template->update($this->form);
session()->flash('message', 'Template actualizado');
} else {
InspectionTemplate::create([
'name' => $this->form['name'],
'description' => $this->form['description'],
'project_id' => $this->project->id,
'fields' => $this->form['fields'],
]);
session()->flash('message', 'Template creado');
}
$this->cancelForm();
$this->loadTemplates();
}
public function deleteTemplate($id)
{
InspectionTemplate::find($id)->delete();
$this->loadTemplates();
session()->flash('message', 'Template eliminado');
}
public function render()
{
return view('livewire.template-manager');
}
}