diff --git a/app/Events/VehiculoRobadoDetectado.php b/app/Events/VehiculoRobadoDetectado.php new file mode 100644 index 0000000..b08a6d5 --- /dev/null +++ b/app/Events/VehiculoRobadoDetectado.php @@ -0,0 +1,59 @@ +alerta = $alerta; + } + + /** + * Canal público para todos los usuarios + */ + public function broadcastOn(): Channel + { + return new Channel('alertas-robos'); + } + + /** + * Nombre del evento en el frontend + */ + public function broadcastAs(): string + { + return 'vehiculo.robado.detectado'; + } + + /** + * Datos que se envían al frontend + */ + public function broadcastWith(): array + { + return [ + 'alerta_id' => $this->alerta->id, + 'fast_id' => $this->alerta->fast_id, + 'vin' => $this->alerta->vin, + 'placa' => $this->alerta->placa, + 'marca' => $this->alerta->marca, + 'modelo' => $this->alerta->modelo, + 'color' => $this->alerta->color, + 'arco_id' => $this->alerta->arco_id, + 'arco_nombre' => $this->alerta->arco_nombre, + 'antena' => $this->alerta->antena, + 'fecha_deteccion' => $this->alerta->fecha_deteccion->toIso8601String(), + 'mensaje' => "🚨 VEHÍCULO ROBADO DETECTADO: {$this->alerta->placa} en {$this->alerta->arco_nombre}", + ]; + } +} diff --git a/app/Http/Controllers/Api/AlertaRoboController.php b/app/Http/Controllers/Api/AlertaRoboController.php new file mode 100644 index 0000000..1f9ac03 --- /dev/null +++ b/app/Http/Controllers/Api/AlertaRoboController.php @@ -0,0 +1,133 @@ +recientes() + ->with(['arco', 'usuario']) + ->get(); + + return ApiResponse::OK->response([ + 'success' => true, + 'total' => $alertas->count(), + 'alertas' => $alertas + ]); + } + + /** + * Listar todas las alertas con filtros + * GET /api/alertas + */ + public function index(Request $request) + { + $query = AlertaRobo::query()->with(['arco', 'usuario']); + + // Filtro por estado (visto/no visto) + if ($request->has('visto')) { + $query->where('visto', $request->boolean('visto')); + } + + // Filtro por arco + if ($request->has('arco_id')) { + $query->where('arco_id', $request->arco_id); + } + + // Filtro por placa + if ($request->has('placa') && !empty($request->placa)) { + $query->where('placa', 'like', '%' . $request->placa . '%'); + } + + // Filtro por VIN + if ($request->has('vin') && !empty($request->vin)) { + $query->where('vin', 'like', '%' . $request->vin . '%'); + } + + // Filtro por rango de fechas + if ($request->has('fecha_desde')) { + $query->where('fecha_deteccion', '>=', $request->fecha_desde); + } + + if ($request->has('fecha_hasta')) { + $query->where('fecha_deteccion', '<=', $request->fecha_hasta); + } + + $alertas = $query->orderBy('fecha_deteccion', 'desc') + ->paginate($request->input('per_page', 15)); + + return ApiResponse::OK->response([ + 'success' => true, + 'alertas' => $alertas + ]); + } + + /** + * Ver una alerta específica + * GET /api/alertas/{id} + */ + public function show(int $id) + { + $alerta = AlertaRobo::with(['arco', 'usuario'])->find($id); + + if (!$alerta) { + return ApiResponse::NOT_FOUND->response([ + 'success' => false, + 'message' => 'Alerta no encontrada' + ]); + } + + return ApiResponse::OK->response([ + 'success' => true, + 'alerta' => $alerta + ]); + } + + /** + * Confirmar/marcar alerta como vista + * PUT /api/alertas/{id}/confirmar + */ + public function confirmar(Request $request, int $id) + { + $alerta = AlertaRobo::find($id); + + if (!$alerta) { + return ApiResponse::NOT_FOUND->response([ + 'success' => false, + 'message' => 'Alerta no encontrada' + ]); + } + + if ($alerta->visto) { + return ApiResponse::BAD_REQUEST->response([ + 'success' => false, + 'message' => 'Esta alerta ya fue confirmada anteriormente', + 'confirmada_por' => $alerta->usuario?->name, + 'fecha_confirmacion' => $alerta->fecha_confirmacion + ]); + } + + // Marcar como vista + $alerta->visto = true; + $alerta->usuario_id = auth()->id(); // Usuario autenticado que confirmó + $alerta->fecha_confirmacion = now(); + $alerta->save(); + + return ApiResponse::OK->response([ + 'success' => true, + 'message' => 'Alerta confirmada exitosamente', + 'alerta' => $alerta + ]); + } +} diff --git a/app/Models/AlertaRobo.php b/app/Models/AlertaRobo.php new file mode 100644 index 0000000..dfade8d --- /dev/null +++ b/app/Models/AlertaRobo.php @@ -0,0 +1,73 @@ + 'boolean', + 'fecha_deteccion' => 'datetime', + 'fecha_confirmacion' => 'datetime', + ]; + + /** + * Relación con el arco + */ + public function arco(): BelongsTo + { + return $this->belongsTo(Arco::class, 'arco_id'); + } + + /** + * Relación con el usuario que confirmó + */ + public function usuario(): BelongsTo + { + return $this->belongsTo(User::class, 'usuario_id'); + } + + /** + * Scope para obtener alertas pendientes (no vistas) + */ + public function scopePendientes($query) + { + return $query->where('visto', false); + } + + /** + * Scope para obtener alertas vistas + */ + public function scopeVistas($query) + { + return $query->where('visto', true); + } + + /** + * Scope para ordenar por más reciente + */ + public function scopeRecientes($query) + { + return $query->orderBy('fecha_deteccion', 'desc'); + } +} diff --git a/app/Services/VehicleService.php b/app/Services/VehicleService.php index badf570..d332682 100644 --- a/app/Services/VehicleService.php +++ b/app/Services/VehicleService.php @@ -2,7 +2,8 @@ namespace App\Services; -use App\Models\Vehicle; +use App\Events\VehiculoRobadoDetectado; +use App\Models\AlertaRobo; use App\Models\Detection; use App\Models\Arco; use Illuminate\Support\Facades\Redis; @@ -27,7 +28,7 @@ public function procesarDeteccion(string $fastId, int $arcoId, ?string $antena = if ($enRedis) { // Ya está marcado como robado, verificar si sigue así - $resultado = $this->verificarVehiculoRobado($fastId, json_decode($enRedis, true)); + $resultado = $this->verificarVehiculoRobado($fastId, json_decode($enRedis, true), $arcoId, $antena); $this->registrarDeteccion($fastId, $resultado, $arcoId, $antena); return $resultado; } @@ -87,7 +88,6 @@ public function consultarVehiculoPorTag(string $fastId) 'fast_id' => $fastId ]); return null; - } catch (\Exception $e) { Log::error('VehicleService: Error en consulta', [ 'fast_id' => $fastId, @@ -118,8 +118,6 @@ public function listarVehiculosRobados(): array /** * Listar todas las detecciones del día desde Redis - * @param string|null $fecha Fecha en formato Y-m-d (opcional, por defecto hoy) - * @return array */ public function listarDeteccionesDelDia(?string $fecha = null): array { @@ -136,7 +134,7 @@ public function listarDeteccionesDelDia(?string $fecha = null): array } // Ordenar por fecha de detección (más reciente primero) - usort($detecciones, function($a, $b) { + usort($detecciones, function ($a, $b) { return strcmp($b['fecha_deteccion'], $a['fecha_deteccion']); }); @@ -145,8 +143,6 @@ public function listarDeteccionesDelDia(?string $fecha = null): array /** * Obtener estadísticas de detecciones del día - * @param string|null $fecha Fecha en formato Y-m-d (opcional, por defecto hoy) - * @return array */ public function obtenerEstadisticasDelDia(?string $fecha = null): array { @@ -245,19 +241,22 @@ private function consultarNuevoVehiculo(string $fastId): array } /** - * Verificar vehículo robado (ya está en Redis) + * Verificar vehículo robado (si ya está en Redis) */ - private function verificarVehiculoRobado(string $fastId, array $datosRedis): array + private function verificarVehiculoRobado(string $fastId, array $datosRedis, ?int $arcoId = null, ?string $antena = null): array { + // Actualizar contador de detecciones $this->actualizarDeteccionRedis($fastId, $datosRedis); - Log::warning('Vehículo robado detectado nuevamente', [ + Log::warning('¡VEHÍCULO ROBADO DETECTADO!', [ 'fast_id' => $fastId, 'vin' => $datosRedis['vin'] ?? null, - 'placa' => $datosRedis['placa'] ?? null, - 'detecciones' => ($datosRedis['detecciones'] ?? 0) + 1 + 'placa' => $datosRedis['placa'] ?? null ]); + // Crear alerta en la base de datos + $this->crearAlertaRobo($fastId, $datosRedis, $arcoId, $antena); + return [ 'success' => true, 'tiene_reporte_robo' => true, @@ -267,6 +266,7 @@ private function verificarVehiculoRobado(string $fastId, array $datosRedis): arr ]; } + /** * Actualizar contador de detecciones en Redis */ @@ -363,4 +363,38 @@ private function registrarDeteccionDelDia(string $fastId, array $vehiculo, ?int // Establecer expiración automática al final del día Redis::expire($key, $segundosHastaFinDelDia); } + + /** + * Crear alerta de vehículo robado y disparar evento + */ + private function crearAlertaRobo(string $fastId, array $datosVehiculo, ?int $arcoId, ?string $antena): void + { + // Obtener nombre del arco + $arcoNombre = $arcoId ? Arco::find($arcoId)?->nombre : 'Desconocido'; + + // Crear alerta en la base de datos + $alerta = AlertaRobo::create([ + 'fast_id' => $fastId, + 'vin' => $datosVehiculo['vin'] ?? null, + 'placa' => $datosVehiculo['placa'] ?? null, + 'marca' => $datosVehiculo['marca'] ?? null, + 'modelo' => $datosVehiculo['modelo'] ?? null, + 'color' => $datosVehiculo['color'] ?? null, + 'arco_id' => $arcoId, + 'arco_nombre' => $arcoNombre, + 'antena' => $antena, + 'fecha_deteccion' => now(), + 'visto' => false, + ]); + + // Disparar evento para Laravel Reverb (WebSocket) + event(new VehiculoRobadoDetectado($alerta)); + + Log::info('Alerta de vehículo robado creada y evento disparado', [ + 'alerta_id' => $alerta->id, + 'fast_id' => $fastId, + 'placa' => $alerta->placa, + 'arco' => $arcoNombre + ]); + } } diff --git a/composer.json b/composer.json index e82bd60..3661fe7 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ "laravel/framework": "^12.0", "laravel/passport": "^12.4", "laravel/pulse": "^1.4", - "laravel/reverb": "^1.4", + "laravel/reverb": "^1.7", "laravel/tinker": "^2.10", "notsoweb/laravel-core": "dev-main", "spatie/laravel-permission": "^6.16", diff --git a/composer.lock b/composer.lock index 850dce6..ad57027 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0cdf26aa072a7f833793cfba72e94e81", + "content-hash": "2aee9315866597be53eadf5c315e8b69", "packages": [ { "name": "brick/math", @@ -1855,16 +1855,16 @@ }, { "name": "laravel/reverb", - "version": "v1.5.0", + "version": "v1.7.0", "source": { "type": "git", "url": "https://github.com/laravel/reverb.git", - "reference": "bf84766ad35d9174fb508147f956e8bcf2e46e91" + "reference": "4300fbbc3535f5cbbdf600b00edca1e4c515bfcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/reverb/zipball/bf84766ad35d9174fb508147f956e8bcf2e46e91", - "reference": "bf84766ad35d9174fb508147f956e8bcf2e46e91", + "url": "https://api.github.com/repos/laravel/reverb/zipball/4300fbbc3535f5cbbdf600b00edca1e4c515bfcf", + "reference": "4300fbbc3535f5cbbdf600b00edca1e4c515bfcf", "shasum": "" }, "require": { @@ -1884,8 +1884,8 @@ "symfony/http-foundation": "^6.3|^7.0" }, "require-dev": { - "orchestra/testbench": "^8.0|^9.0|^10.0", - "pestphp/pest": "^2.0|^3.0", + "orchestra/testbench": "^8.36|^9.15|^10.8", + "pestphp/pest": "^2.0|^3.0|^4.0", "phpstan/phpstan": "^1.10", "ratchet/pawl": "^0.4.1", "react/async": "^4.2", @@ -1931,9 +1931,9 @@ ], "support": { "issues": "https://github.com/laravel/reverb/issues", - "source": "https://github.com/laravel/reverb/tree/v1.5.0" + "source": "https://github.com/laravel/reverb/tree/v1.7.0" }, - "time": "2025-03-31T14:06:47+00:00" + "time": "2026-01-06T16:26:25+00:00" }, { "name": "laravel/serializable-closure", @@ -10494,5 +10494,5 @@ "php": "^8.3" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/database/migrations/2026_01_09_163539_create_alertas_robos_table.php b/database/migrations/2026_01_09_163539_create_alertas_robos_table.php new file mode 100644 index 0000000..e4645e8 --- /dev/null +++ b/database/migrations/2026_01_09_163539_create_alertas_robos_table.php @@ -0,0 +1,43 @@ +id(); + $table->string('fast_id'); + $table->string('vin')->nullable(); + $table->string('placa')->nullable(); + $table->string('marca')->nullable(); + $table->string('modelo')->nullable(); + $table->string('color')->nullable(); + $table->unsignedBigInteger('arco_id'); + $table->string('arco_nombre'); + $table->string('antena')->nullable(); + $table->timestamp('fecha_deteccion'); + $table->boolean('visto')->default(false); + $table->unsignedBigInteger('usuario_id')->nullable(); + $table->timestamp('fecha_confirmacion')->nullable(); + $table->timestamps(); + + // Índices para optimizar consultas + $table->index('visto'); + $table->index('arco_id'); + $table->index('fecha_deteccion'); + + // Foreign keys + $table->foreign('arco_id')->references('id')->on('arcos')->onDelete('cascade'); + $table->foreign('usuario_id')->references('id')->on('users')->onDelete('set null'); + }); + } + + public function down(): void + { + Schema::dropIfExists('alertas_robos'); + } +}; diff --git a/docker-compose.yml b/docker-compose.yml index c9d8f4a..c1fd4c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: - DB_PASSWORD=${DB_PASSWORD} - DB_DATABASE=${DB_DATABASE} - DB_PORT=${DB_PORT} + ports: + - "8080:8080" volumes: - ./:/var/www/arcos-backend - ./vendor:/var/www/arcos/vendor diff --git a/routes/api.php b/routes/api.php index 9387442..f846fdd 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,7 @@