diff --git a/app/Http/Controllers/Repuve/CancellationController.php b/app/Http/Controllers/Repuve/CancellationController.php new file mode 100644 index 0000000..2cd27a9 --- /dev/null +++ b/app/Http/Controllers/Repuve/CancellationController.php @@ -0,0 +1,319 @@ +input('search_type'); + $searchValue = $request->input('search_value'); + + // Simulación: consulta a base de datos REPUVE hardcodeada + $vehicleData = $this->getVehicleDataFromRepuve($searchType, $searchValue); + + if (!$vehicleData) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'No se encontró ningún vehículo con los datos proporcionados en REPUVE.', + 'search_type' => $searchType, + 'search_value' => $searchValue, + ]); + } + + // Buscar vehículo en nuestra base de datos local + $vehicle = Vehicle::where('numero_serie', $vehicleData['NO_SERIE']) + ->with(['owner']) + ->first(); + + if (!$vehicle) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'El vehículo existe en REPUVE pero no está registrado localmente.', + 'repuve_data' => $vehicleData, + ]); + } + + // Buscar tag asignado al vehículo (solo assigned) + $tag = Tag::where('vehicle_id', $vehicle->id) + ->where('status', 'assigned') + ->first(); + + // Verificar si ya tiene cancelaciones previas + $cancelaciones = VehicleTagLog::where('vehicle_id', $vehicle->id) + ->whereNotNull('cancellation_at') + ->count(); + + $canCancel = !is_null($tag) && $tag->status === 'assigned'; + + return ApiResponse::OK->response([ + 'vehicle' => [ + 'id' => $vehicle->id, + 'estatus' => $tag ? $tag->status : 'sin_tag', + 'folio' => $vehicle->folio, + 'tag' => $tag ? $tag->folio : null, + 'niv' => $vehicle->numero_serie, + 'tipo' => $vehicle->tipo, + 'registro' => $vehicle->created_at->format('d/m/Y'), + 'placa' => $vehicle->placa, + 'marca' => $vehicle->marca, + 'modelo' => $vehicle->modelo, + 'color' => $vehicle->color, + ], + 'tag' => $tag ? [ + 'id' => $tag->id, + 'folio' => $tag->folio, + 'status' => $tag->status, + ] : null, + 'can_cancel' => $canCancel, + 'total_cancelaciones_previas' => $cancelaciones, + 'message' => $canCancel + ? 'Vehículo encontrado. Puede proceder con la cancelación.' + : 'Vehículo encontrado pero no tiene tag asignado o ya está cancelado.', + ]); + + } catch (\Exception $e) { + Log::error('Error en buscarVehiculoParaCancelar: ' . $e->getMessage(), [ + 'search_type' => $searchType ?? null, + 'search_value' => $searchValue ?? null, + 'trace' => $e->getTraceAsString() + ]); + + return ApiResponse::INTERNAL_SERVER_ERROR->response([ + 'message' => 'Error al buscar el vehículo', + 'error' => $e->getMessage(), + ]); + } + } + + /* =========================================================== + * Cancelar constancia + * =========================================================== + */ + public function cancelarConstancia(CancelConstanciaRequest $request) + { + // Iniciar transacción + DB::beginTransaction(); + + try { + $vehicleId = $request->input('vehicle_id'); + $tagId = $request->input('tag_id'); + $reason = $request->input('cancellation_reason'); + $observations = $request->input('cancellation_observations'); + + // Validar que el vehículo existe + $vehicle = Vehicle::findOrFail($vehicleId); + + // Validar que el tag existe + $tag = Tag::findOrFail($tagId); + + // Validar que el tag pertenece al vehículo + if ($tag->vehicle_id !== $vehicle->id) { + DB::rollBack(); + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'El tag no está asignado al vehículo especificado', + ]); + } + + // Validar que el tag esté en estado 'assigned' + if ($tag->status !== 'assigned') { + DB::rollBack(); + return ApiResponse::BAD_REQUEST->response([ + 'message' => "El tag no puede ser cancelado. Estado actual: {$tag->status}", + 'current_status' => $tag->status, + ]); + } + + // Verificar que no exista ya un registro de cancelación para este tag + $existingCancellation = VehicleTagLog::where('tag_id', $tagId) + ->whereNotNull('cancellation_at') + ->first(); + + if ($existingCancellation) { + DB::rollBack(); + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Este tag ya tiene un registro de cancelación', + 'cancellation_date' => $existingCancellation->cancellation_at->toDateTimeString(), + ]); + } + + // Crear registro de cancelación en vehicle_tags_logs + $cancellationLog = VehicleTagLog::create([ + 'vehicle_id' => $vehicleId, + 'tag_id' => $tagId, + 'cancellation_at' => now(), + 'cancellation_reason' => $reason, + 'cancellation_observations' => $observations, + 'cancelled_by' => auth()->id(), + ]); + + // Actualizar estado del tag a 'cancelled' + $tag->update(['status' => 'cancelled']); + + // Confirmar transacción + DB::commit(); + + // Recargar vehículo con tag actualizado + $vehicle->load('owner'); + $tag->refresh(); + + return ApiResponse::OK->response([ + 'success' => true, + 'message' => 'Constancia de inscripción cancelada exitosamente', + 'vehicle' => [ + 'id' => $vehicle->id, + 'estatus' => 'cancelada', + 'folio' => $vehicle->folio, + 'tag' => $tag->folio, + 'niv' => $vehicle->numero_serie, + 'tipo' => $vehicle->tipo, + 'registro' => $vehicle->created_at->format('d/m/Y'), + ], + 'cancellation' => [ + 'id' => $cancellationLog->id, + 'motivo' => $cancellationLog->cancellation_reason, + 'observaciones' => $cancellationLog->cancellation_observations, + 'fecha_cancelacion' => $cancellationLog->cancellation_at->format('d/m/Y H:i:s'), + 'cancelado_por' => auth()->user()->name ?? 'Sistema', + ], + ]); + + } catch (\Exception $e) { + // Revertir transacción en caso de error + DB::rollBack(); + + Log::error('Error en cancelarConstancia: ' . $e->getMessage(), [ + 'vehicle_id' => $request->input('vehicle_id'), + 'tag_id' => $request->input('tag_id'), + 'trace' => $e->getTraceAsString() + ]); + + return ApiResponse::INTERNAL_SERVER_ERROR->response([ + 'message' => 'Error al cancelar la constancia de inscripción', + 'error' => $e->getMessage(), + ]); + } + } + + /** + * Obtiene historial de cancelaciones de un vehículo + */ + public function historialCancelaciones($vehicleId) + { + try { + $vehicle = Vehicle::findOrFail($vehicleId); + + $cancelaciones = VehicleTagLog::where('vehicle_id', $vehicleId) + ->whereNotNull('cancellation_at') + ->with(['tag', 'cancelledBy']) + ->orderBy('cancellation_at', 'desc') + ->get(); + + return ApiResponse::OK->response([ + 'vehicle' => [ + 'id' => $vehicle->id, + 'placa' => $vehicle->placa, + 'numero_serie' => $vehicle->numero_serie, + ], + 'total_cancelaciones' => $cancelaciones->count(), + 'cancelaciones' => $cancelaciones->map(function ($log) { + return [ + 'id' => $log->id, + 'tag_folio' => $log->tag->folio ?? 'N/A', + 'cancellation_at' => $log->cancellation_at->toDateTimeString(), + 'cancellation_reason' => $log->cancellation_reason, + 'cancellation_observations' => $log->cancellation_observations, + 'cancelled_by' => $log->cancelledBy ? [ + 'id' => $log->cancelledBy->id, + 'name' => $log->cancelledBy->name, + ] : null, + ]; + }), + ]); + + } catch (\Exception $e) { + Log::error('Error en historialCancelaciones: ' . $e->getMessage(), [ + 'vehicle_id' => $vehicleId ?? null, + 'trace' => $e->getTraceAsString() + ]); + + return ApiResponse::INTERNAL_SERVER_ERROR->response([ + 'message' => 'Error al obtener el historial de cancelaciones', + 'error' => $e->getMessage(), + ]); + } + } + + /** + * Simula consulta a base de datos REPUVE + * + */ + private function getVehicleDataFromRepuve(string $searchType, string $searchValue): ?array + { + // Datos hardcodeados del vehículo de ejemplo + $vehicleData = [ + "ANIO_PLACA" => "2020", + "PLACA" => "WNU700B", + "NO_SERIE" => "LSGHD52H0ND032457", + "RFC" => "GME111116GJA", + "FOLIO" => "3962243", + "VIGENCIA" => "2025", + "FECHA_IMPRESION" => "10-01-2025", + "QR_HASH" => "Vu5TF4kYsbbltzjDdGQyenKfZoIk2wro34a5Gkh9JVh0CFxfPlrd92YEWK21JF.nLjQNyzKmqRvWYuPiS.kU7A--", + "VALIDO" => true, + "FOLIOTEMP" => false, + "NOMBRE" => "GOLSYSTEMS DE MEXICO S DE RL DE CV", + "NOMBRE2" => "GOLS*MS DXICOE RL*CV", + "MUNICIPIO" => "CENTRO", + "LOCALIDAD" => "VILLAHERMOSA", + "CALLE" => "C BUGAMBILIAS 118 ", + "CALLE2" => "C BU*ILIA*18 ", + "TIPO" => "SEDAN", + "TIPO_SERVICIO" => "PARTICULAR", + "MARCA" => "CHEVROLET G.M.C.", + "LINEA" => "AVEO", + "SUBLINEA" => "PAQ. \"A\" LS", + "MODELO" => 2022, + "NUMERO_SERIE" => "LSGHD52H0ND032457", + "NUMERO_MOTOR" => "H. EN WUHANLL,SGM", + "DESCRIPCION_ORIGEN" => "IMPORTADO", + "COLOR" => "BLANCO", + "CODIGO_POSTAL" => "86179", + "SERIE_FOLIO" => "D3962243", + "SFOLIO" => "3962243" + ]; + + // Normalizar el valor de búsqueda (trim y uppercase) + $searchValue = trim(strtoupper($searchValue)); + + // Simular búsqueda por tipo + switch ($searchType) { + case 'folio': + return (strtoupper($vehicleData['FOLIO']) === $searchValue) ? $vehicleData : null; + + case 'vin': + return (strtoupper($vehicleData['NO_SERIE']) === $searchValue) ? $vehicleData : null; + + case 'fecha': + // Para fecha, comparar sin case sensitivity + return (strtoupper($vehicleData['FECHA_IMPRESION']) === $searchValue) ? $vehicleData : null; + + default: + return null; + } + } +} diff --git a/app/Http/Controllers/Repuve/RepuveController.php b/app/Http/Controllers/Repuve/RepuveController.php index 7308f59..d23cf3d 100644 --- a/app/Http/Controllers/Repuve/RepuveController.php +++ b/app/Http/Controllers/Repuve/RepuveController.php @@ -13,6 +13,12 @@ class RepuveController extends Controller { + + /* =========================================================== + * Inscripción de vehículo al REPUVE + * =========================================================== + */ + public function inscripcionVehiculo(VehicleStoreRequest $request) { try { @@ -98,7 +104,7 @@ public function inscripcionVehiculo(VehicleStoreRequest $request) $uploadedFiles = []; if ($request->hasFile('files')) { $files = $request->file('files'); - $fileNames = $request->input('file_names', []); + $fileNames = $request->input('names', []); foreach ($files as $index => $file) { // Generar nombre único diff --git a/app/Http/Requests/Repuve/CancelConstanciaRequest.php b/app/Http/Requests/Repuve/CancelConstanciaRequest.php new file mode 100644 index 0000000..9579d06 --- /dev/null +++ b/app/Http/Requests/Repuve/CancelConstanciaRequest.php @@ -0,0 +1,64 @@ + 'required|integer|exists:vehicle,id', + 'tag_id' => 'required|integer|exists:tags,id', + 'cancellation_reason' => 'required|in:fallo_lectura_handheld,cambio_parabrisas,roto_al_pegarlo,extravio,otro', + 'cancellation_observations' => 'nullable|string|max:1000', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'vehicle_id.required' => 'El id del vehículo es obligatorio.', + 'vehicle_id.integer' => 'El id del vehículo debe ser un número entero.', + 'vehicle_id.exists' => 'El vehículo especificado no existe.', + + 'tag_id.required' => 'El id del tag es obligatorio.', + 'tag_id.integer' => 'El id del tag debe ser un número entero.', + 'tag_id.exists' => 'El tag especificado no existe.', + + 'cancellation_reason.required' => 'El motivo de cancelación es obligatorio.', + 'cancellation_reason.in' => 'El motivo de cancelación no es válido. Opciones: fallo_lectura_handheld, cambio_parabrisas, roto_al_pegarlo, extravio, otro.', + + 'cancellation_observations.string' => 'Las observaciones deben ser texto.', + 'cancellation_observations.max' => 'Las observaciones no pueden exceder 1000 caracteres.', + ]; + } + + /** + * Get custom attributes for validator errors. + */ + public function attributes(): array + { + return [ + 'vehicle_id' => 'id del vehículo', + 'tag_id' => 'id del tag', + 'cancellation_reason' => 'motivo de cancelación', + 'cancellation_observations' => 'observaciones', + ]; + } +} diff --git a/app/Http/Requests/Repuve/ConsultaVehiculoRequest.php b/app/Http/Requests/Repuve/ConsultaVehiculoRequest.php new file mode 100644 index 0000000..f459586 --- /dev/null +++ b/app/Http/Requests/Repuve/ConsultaVehiculoRequest.php @@ -0,0 +1,52 @@ + 'required|in:folio,vin,fecha', + 'search_value' => 'required|string|max:255', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'search_type.required' => 'El tipo de búsqueda es obligatorio.', + 'search_type.in' => 'El tipo de búsqueda debe ser: folio, vin o fecha.', + 'search_value.required' => 'El valor de búsqueda es obligatorio.', + 'search_value.string' => 'El valor de búsqueda debe ser una cadena de texto.', + 'search_value.max' => 'El valor de búsqueda no puede exceder 255 caracteres.', + ]; + } + + /** + * Get custom attributes for validator errors. + */ + public function attributes(): array + { + return [ + 'search_type' => 'tipo de búsqueda', + 'search_value' => 'valor de búsqueda', + ]; + } +} diff --git a/app/Models/VehicleTagLog.php b/app/Models/VehicleTagLog.php index 3bb1562..bd8c775 100644 --- a/app/Models/VehicleTagLog.php +++ b/app/Models/VehicleTagLog.php @@ -14,7 +14,10 @@ class VehicleTagLog extends Model protected $fillable = [ 'vehicle_id', 'tag_id', + 'cancellation_reason', + 'cancellation_observations', 'cancellation_at', + 'cancelled_by', ]; protected function casts(): array @@ -24,4 +27,16 @@ protected function casts(): array ]; } + public function vehicle() { + return $this->belongsTo(Vehicle::class); + } + + public function tag() { + return $this->belongsTo(Tag::class); + } + + public function cancelledBy() { + return $this->belongsTo(User::class, 'cancelled_by'); + } + } diff --git a/database/migrations/2025_10_18_140800_create_vehicle_tags_logs_table.php b/database/migrations/2025_10_18_140800_create_vehicle_tags_logs_table.php index 3625ef0..4544c8f 100644 --- a/database/migrations/2025_10_18_140800_create_vehicle_tags_logs_table.php +++ b/database/migrations/2025_10_18_140800_create_vehicle_tags_logs_table.php @@ -16,6 +16,15 @@ public function up(): void $table->foreignId('vehicle_id')->constrained('vehicle')->cascadeOnDelete(); $table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete(); $table->timestamp('cancellation_at')->nullable(); + $table->enum('cancellation_reason', [ + 'fallo_lectura_handheld', + 'cambio_parabrisas', + 'roto_al_pegarlo', + 'extravio', + 'otro' + ])->nullable(); + $table->text('cancellation_observations')->nullable(); + $table->foreignId('cancelled_by')->nullable()->constrained('users')->nullOnDelete(); $table->timestamps(); }); } diff --git a/resources/images/logo-seguridad.png b/resources/images/logo-seguridad.png new file mode 100755 index 0000000..f980249 Binary files /dev/null and b/resources/images/logo-seguridad.png differ diff --git a/resources/views/pdfs/record.blade.php b/resources/views/pdfs/record.blade.php index 469d2c7..2d237dc 100644 --- a/resources/views/pdfs/record.blade.php +++ b/resources/views/pdfs/record.blade.php @@ -174,11 +174,11 @@
- Logo Seguridad + Logo Seguridad
@@ -258,7 +258,8 @@

Propietario

-
{{ $record->user->full_name }}
+
+

{{ $record->user->full_name }}

Operador

diff --git a/routes/api.php b/routes/api.php index 3a4ea49..92e54a9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,6 +2,7 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\Repuve\RepuveController; use App\Http\Controllers\Repuve\RecordController; +use App\Http\Controllers\Repuve\CancellationController; /** * Rutas del núcleo de la aplicación. @@ -19,14 +20,20 @@ /** Rutas protegidas (requieren autenticación) */ Route::middleware('auth:api')->group(function() { - // Tus rutas protegidas + // Rutas de inscripción de vehículos Route::post('inscripcion', [RepuveController::class, 'inscripcionVehiculo']); Route::post('consulta', [RepuveController::class, 'consultaExpediente']); + // Rutas de expedientes y documentos Route::get('expediente/{id}/pdf', [RecordController::class, 'generatePdf']); Route::get('expediente/{recordId}/documentos', [RecordController::class, 'getFile']); Route::post('expediente/documentos', [RecordController::class, 'uploadFile']); Route::delete('expediente/documentos/{fileId}', [RecordController::class, 'deleteFile']); + + // Rutas de cancelación de constancias + Route::post('cancelacion/buscar', [CancellationController::class, 'searchToCancel']); + Route::post('cancelacion/cancelar', [CancellationController::class, 'cancelarConstancia']); + Route::get('cancelacion/historial/{vehicleId}', [CancellationController::class, 'historialCancelaciones']); }); /** Rutas públicas */