diff --git a/README.md b/README.md index 0165a77..8c673ea 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,45 @@ -

Laravel Logo

+# ConstruProgress -

-Build Status -Total Downloads -Latest Stable Version -License -

+Sistema de gestión de proyectos de construcción con mapas interactivos, control de progreso, inspecciones y soporte offline. -## About Laravel +## Características -Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: +- **Mapas interactivos** — Visualización de proyectos sobre mapa con capas (GeoJSON/KML) y elementos editables +- **Gestión de fases** — Proyectos organizados en fases con progreso porcentual y seguimiento histórico +- **Capas y elementos** — Subida de archivos GeoJSON/KML, capas vacías editables con color personalizado +- **Inspecciones** — Plantillas de inspección por proyecto, asignables a elementos del mapa +- **Progreso** — Seguimiento visual del progreso por fase y global del proyecto +- **Sincronización offline** — Endpoints para trabajadores en campo, sincronización diferida +- **Permisos** — Roles y permisos granulares (Spatie Permission) +- **Dashboard** — Estadísticas globales, proyectos recientes, inspecciones -- [Simple, fast routing engine](https://laravel.com/docs/routing). -- [Powerful dependency injection container](https://laravel.com/docs/container). -- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. -- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). -- Database agnostic [schema migrations](https://laravel.com/docs/migrations). -- [Robust background job processing](https://laravel.com/docs/queues). -- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). +## Requisitos -Laravel is accessible, powerful, and provides tools required for large, robust applications. +- PHP 8.2+ +- MySQL/MariaDB +- Composer +- Node.js + NPM -## Learning Laravel +## Instalación -Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application. +```bash +git clone construprogress +cd construprogress +composer install +npm install && npm run build +cp .env.example .env +# Editar .env con credenciales de base de datos +php artisan key:generate +php artisan migrate +php artisan db:seed --class=RolePermissionSeeder # si existe +php artisan serve +``` -If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. +## Stack técnico -## Laravel Sponsors - -We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). - -### Premium Partners - -- **[Vehikl](https://vehikl.com)** -- **[Tighten Co.](https://tighten.co)** -- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** -- **[64 Robots](https://64robots.com)** -- **[Curotec](https://www.curotec.com/services/technologies/laravel)** -- **[DevSquad](https://devsquad.com/hire-laravel-developers)** -- **[Redberry](https://redberry.international/laravel-development)** -- **[Active Logic](https://activelogic.com)** - -## Contributing - -Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). - -## Code of Conduct - -In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). - -## Security Vulnerabilities - -If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. - -## License - -The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). +- **Framework:** Laravel 11 +- **Frontend:** Tailwind CSS + DaisyUI + Leaflet.js +- **Mapas:** Leaflet + Leaflet Draw (editor gráfico) +- **Componentes:** Livewire 3 +- **Base de datos:** MySQL/MariaDB +- **Autenticación:** Laravel Breeze \ No newline at end of file diff --git a/app/Livewire/LayerUpload.php b/app/Livewire/LayerUpload.php index 655c12c..9a40ca1 100644 --- a/app/Livewire/LayerUpload.php +++ b/app/Livewire/LayerUpload.php @@ -3,11 +3,122 @@ namespace App\Livewire; use Livewire\Component; +use Livewire\WithFileUploads; +use App\Models\Project; +use App\Models\Phase; +use App\Models\Layer; +use App\Models\Feature; +use App\Services\SpatialFileConverter; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Storage; class LayerUpload extends Component { + use WithFileUploads; + + public $projectId; + public $phaseId; + public $uploadFile = null; + public $layerName = ''; + public $layerColor = '#3b82f6'; + + protected $rules = [ + 'uploadFile' => 'required|file|max:51200', + 'layerName' => 'required|string|max:255', + 'layerColor' => 'nullable|string|size:7', + ]; + + public function mount($projectId = null, $phaseId = null) + { + $this->projectId = $projectId; + $this->phaseId = $phaseId; + } + + public function upload() + { + $user = Auth::user(); + if (!$user->can('upload layers') && !$user->hasRole('Admin')) { + session()->flash('error', 'Sin permisos.'); + return; + } + + $this->validate(); + + if (!$this->projectId || !$this->phaseId) { + session()->flash('error', 'Faltan datos del proyecto/fase.'); + return; + } + + $project = Project::findOrFail($this->projectId); + $phase = Phase::findOrFail($this->phaseId); + + $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', + 'application/xml', + ]; + + if (!in_array($extension, $allowedExtensions) && !in_array($mime, $allowedMimes)) { + session()->flash('error', 'Tipo de archivo no permitido.'); + return; + } + + $projectDir = "uploads/projects/{$project->id}/layers"; + $originalPath = $this->uploadFile->store($projectDir, 'public'); + $geojson = SpatialFileConverter::convertToGeoJson($this->uploadFile); + + if (!$geojson) { + session()->flash('error', 'Conversión fallida.'); + return; + } + + $layerColor = $this->layerColor ?: '#3b82f6'; + $geojson['style'] = ['color' => $layerColor]; + + $layer = Layer::create([ + 'project_id' => $project->id, + 'phase_id' => $phase->id, + 'name' => $this->layerName, + 'color' => $layerColor, + 'original_file' => $originalPath, + 'uploaded_by' => $user->id, + ]); + + 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->reset(['uploadFile', 'layerName']); + session()->flash('message', "Capa '{$layer->name}' importada correctamente con " . count($geojson['features'] ?? []) . ' elementos.'); + $this->dispatch('layerUploaded', projectId: $project->id); + } + public function render() { - return view('livewire.layer-upload'); + $projects = Project::accessibleBy(Auth::user())->get(); + $phases = $this->projectId ? Phase::where('project_id', $this->projectId)->orderBy('order')->get() : collect(); + + return view('livewire.layer-upload', compact('projects', 'phases')); } -} +} \ No newline at end of file diff --git a/app/Livewire/PhaseProgress.php b/app/Livewire/PhaseProgress.php index 3f171b0..33c4e9e 100644 --- a/app/Livewire/PhaseProgress.php +++ b/app/Livewire/PhaseProgress.php @@ -13,7 +13,7 @@ class PhaseProgress extends Component public function mount(Phase $phase) { - $this->phase = $phase; + $this->phase = $phase->load('progressUpdates'); $this->progress = $phase->progress_percent; } diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 66028f2..97a1596 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -7,11 +7,113 @@
-
-
- {{ __("You're logged in!") }} + {{-- Stats cards --}} +
+
+
Proyectos activos
+
{{ $stats['active_projects'] }}
+
+
+
Proyectos totales
+
{{ $stats['total_projects'] }}
+
+
+
Fases totales
+
{{ $stats['total_phases'] }}
+
+
+
Elementos (features)
+
{{ $stats['total_features'] }}
+ + {{-- Barra de progreso global --}} +
+

Progreso global

+
+
+
+

{{ $stats['global_progress'] }}%

+
+ + {{-- Proyectos recientes --}} +
+
+

Proyectos recientes

+ Ver todos +
+
+ + + + + + + + + + + + @forelse($recentProjects as $project) + + + + + + + + @empty + + @endforelse + +
NombreEstadoFasesProgreso
{{ $project->name }} + @php + $badgeClass = match($project->status) { + 'planning' => 'badge-ghost', + 'in_progress' => 'badge-primary', + 'paused' => 'badge-warning', + 'completed' => 'badge-success', + default => 'badge-ghost' + }; + @endphp + {{ match($project->status) { + 'planning' => 'Planificación', + 'in_progress' => 'En obra', + 'paused' => 'Pausado', + 'completed' => 'Finalizado', + default => $project->status + } }} + {{ $project->phases_count }} + @php $avg = $project->phases->avg('progress_percent'); @endphp +
+
+
+
+ {{ round($avg) }}% +
+
+ Mapa +
No hay proyectos aún
+
+
+ + {{-- Inspecciones recientes --}} + @if($recentInspections->isNotEmpty()) +
+

Inspecciones recientes

+
+ @foreach($recentInspections as $inspection) +
+
+ {{ $inspection->template?->name ?? 'Inspección' }} + {{ $inspection->feature?->name }} +
+ {{ $inspection->created_at->diffForHumans() }} +
+ @endforeach +
+
+ @endif
- + \ No newline at end of file diff --git a/resources/views/livewire/layer-upload.blade.php b/resources/views/livewire/layer-upload.blade.php index a388320..16ea93d 100644 --- a/resources/views/livewire/layer-upload.blade.php +++ b/resources/views/livewire/layer-upload.blade.php @@ -2,17 +2,55 @@ @if(session()->has('message'))
{{ session('message') }}
@endif + @if(session()->has('error')) +
{{ session('error') }}
+ @endif
-

Subir capa a proyecto

-
+

Subir capa

+ +
- - - @error('file') {{ $message }} @enderror + +
- + +
+ + +
+ +
+ + + @error('layerName') {{ $message }} @enderror +
+ +
+ + +
+ +
+ + + @error('uploadFile') {{ $message }} @enderror +
+ +
diff --git a/routes/web.php b/routes/web.php index dc71e47..6f51335 100644 --- a/routes/web.php +++ b/routes/web.php @@ -34,7 +34,41 @@ Route::middleware(['auth'])->group(function () { // Dashboard principal (vista con estadísticas y lista de proyectos) Route::get('/dashboard', function () { - return view('dashboard'); + $user = \Illuminate\Support\Facades\Auth::user(); + + $projects = \App\Models\Project::accessibleBy($user) + ->withCount('phases') + ->with('phases') + ->latest() + ->take(5) + ->get(); + + $allProjects = \App\Models\Project::accessibleBy($user); + $activeProjects = (clone $allProjects)->where('status', 'in_progress'); + $totalPhases = \App\Models\Phase::whereIn('project_id', (clone $allProjects)->pluck('id'))->count(); + $totalFeatures = \App\Models\Feature::whereIn('layer_id', function($q) use ($allProjects) { + $q->select('id')->from('layers')->whereIn('project_id', (clone $allProjects)->pluck('id')); + })->count(); + + $globalProgress = \App\Models\Phase::whereIn('project_id', (clone $allProjects)->pluck('id'))->avg('progress_percent') ?? 0; + + $inspections = \App\Models\Inspection::whereIn('project_id', (clone $allProjects)->pluck('id')) + ->with(['template', 'feature']) + ->latest() + ->take(5) + ->get(); + + return view('dashboard', [ + 'stats' => [ + 'active_projects' => $activeProjects->count(), + 'total_projects' => $allProjects->count(), + 'total_phases' => $totalPhases, + 'total_features' => $totalFeatures, + 'global_progress' => round($globalProgress), + ], + 'recentProjects' => $projects, + 'recentInspections' => $inspections, + ]); })->name('dashboard'); // ------------------------------------------------------------