diff --git a/app/Http/Controllers/Repuve/CancellationController.php b/app/Http/Controllers/Repuve/CancellationController.php index 2cd27a9..03e1fe9 100644 --- a/app/Http/Controllers/Repuve/CancellationController.php +++ b/app/Http/Controllers/Repuve/CancellationController.php @@ -3,10 +3,8 @@ namespace App\Http\Controllers\Repuve; use App\Http\Controllers\Controller; -use App\Http\Requests\Repuve\ConsultaVehiculoRequest; use App\Http\Requests\Repuve\CancelConstanciaRequest; -use App\Models\Vehicle; -use App\Models\Tag; +use App\Models\Record; use App\Models\VehicleTagLog; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -14,306 +12,85 @@ class CancellationController extends Controller { - /* =========================================================== - * PASO 1: Buscar vehículo para cancelar - * Muestra la tabla con los datos del vehículo encontrado - * =========================================================== - */ - public function searchToCancel(ConsultaVehiculoRequest $request) - { - try { - $searchType = $request->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 - * =========================================================== + /** + * Cancelar la constancia/tag */ 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'); + DB::beginTransaction(); - // Validar que el vehículo existe - $vehicle = Vehicle::findOrFail($vehicleId); + // Buscar el expediente con sus relaciones + $record = Record::with('vehicle.tag')->findOrFail($request->record_id); + $vehicle = $record->vehicle; + $tag = $vehicle->tag; - // 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(); + // Validar que el vehículo tiene un tag asignado + if (!$tag) { return ApiResponse::BAD_REQUEST->response([ - 'message' => 'El tag no está asignado al vehículo especificado', + 'message' => 'El vehículo no tiene un tag/constancia asignada.' ]); } - // Validar que el tag esté en estado 'assigned' + // Validar que el tag está en estado activo 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, + 'message' => 'El tag ya no está activo. Status actual: ' . $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 + // Crear registro en el log de cancelaciones (HISTORIAL) $cancellationLog = VehicleTagLog::create([ - 'vehicle_id' => $vehicleId, - 'tag_id' => $tagId, + 'vehicle_id' => $vehicle->id, + 'tag_id' => $tag->id, + 'cancellation_reason' => $request->cancellation_reason, + 'cancellation_observations' => $request->cancellation_observations, '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', - ], + $tag->update([ + 'status' => 'cancelled', ]); + DB::commit(); + + return ApiResponse::OK->response([ + 'message' => 'Constancia cancelada exitosamente', + 'cancellation' => [ + 'id' => $cancellationLog->id, + 'vehicle' => [ + 'id' => $vehicle->id, + 'placa' => $vehicle->placa, + 'numero_serie' => $vehicle->numero_serie, + ], + 'tag' => [ + 'id' => $tag->id, + 'folio' => $tag->folio, + 'old_status' => 'assigned', + 'new_status' => 'cancelled', + ], + 'cancellation_reason' => $request->cancellation_reason, + 'cancellation_observations' => $request->cancellation_observations, + 'cancelled_at' => $cancellationLog->cancellation_at->toDateTimeString(), + 'cancelled_by' => auth()->user()->name, + ] + ]); } 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'), + 'record_id' => $request->record_id ?? null, + 'cancellation_reason' => $request->cancellation_reason ?? null, 'trace' => $e->getTraceAsString() ]); - return ApiResponse::INTERNAL_SERVER_ERROR->response([ - 'message' => 'Error al cancelar la constancia de inscripción', - 'error' => $e->getMessage(), + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Error al cancelar la constancia', + '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/RecordController.php b/app/Http/Controllers/Repuve/RecordController.php index d148059..0039928 100644 --- a/app/Http/Controllers/Repuve/RecordController.php +++ b/app/Http/Controllers/Repuve/RecordController.php @@ -4,11 +4,7 @@ use App\Http\Controllers\Controller; use Barryvdh\DomPDF\Facade\Pdf; -use App\Http\Requests\Repuve\FileStoreRequest; -use Notsoweb\ApiResponse\Enums\ApiResponse; -use Illuminate\Support\Facades\Storage; use App\Models\Record; -use App\Models\File; class RecordController extends Controller { @@ -24,94 +20,39 @@ public function generatePdf($id) 'isRemoteEnabled' => true, ]); - return $pdf->stream('record-' . $id . '.pdf'); + return $pdf->stream('constancia-inscripcion-' . $id . '.pdf'); } - public function uploadFile(FileStoreRequest $request){ - try { - // Verificar que existe el record - $record = Record::findOrFail($request->record_id); - - // Obtener el archivo - $uploadedFile = $request->file('file'); - - // Generar nombre único - $fileName = time() . '_' . $uploadedFile->getClientOriginalName(); - - // Guardar en storage - $path = $uploadedFile->storeAs('records', $fileName, 'public'); - - // Calcular MD5 del archivo - $md5 = md5_file($uploadedFile->getRealPath()); - - // Crear registro en la tabla files - $file = File::create([ - 'name' => $request->name, - 'path' => $path, - 'md5' => $md5, - 'record_id' => $record->id, - ]); - - return ApiResponse::OK->response([ - 'message' => 'Archivo subido exitosamente', - 'file' => [ - 'id' => $file->id, - 'name' => $file->name, - 'path' => $file->path, - 'md5' => $file->md5, - 'url' => $file->url, - 'record_id' => $file->record_id, - 'created_at' => $file->created_at->toDateTimeString(), - ], - ]); - - } catch (\Exception $e) { - return ApiResponse::UNPROCESSABLE_CONTENT->response([ - 'message' => 'Error al subir el archivo', - 'error' => $e->getMessage(), - ]); - } - } - - public function getFile($recordId) { - try { - $record = Record::with('files')->findOrFail($recordId); - - return ApiResponse::OK->response([ - 'record_id' => $record->id, - 'files' => $record->files, - 'folio' => $record->folio, - ]); - }catch(\Exception $e) { - return ApiResponse::NOT_FOUND->response([ - 'message' => 'No se encontró el expediente o no tiene archivos asociados.', - 'error' => $e->getMessage(), - ]); - } - } - - public function deleteFile($fileId) + public function generatePdfVerification($id) { - try { - $file = File::findOrFail($fileId); + $record = Record::with('vehicle.owner', 'user')->findOrFail($id); - // Eliminar archivo físico del storage - if (Storage::disk('public')->exists($file->path)) { - Storage::disk('public')->delete($file->path); - } - - // Eliminar registro de la BD - $file->delete(); - - return ApiResponse::OK->response([ - 'message' => 'Archivo eliminado exitosamente', + $pdf = Pdf::loadView('pdfs.verification', compact('record')) + ->setPaper('a4', 'landscape') + ->setOptions([ + 'defaultFont' => 'sans-serif', + 'isHtml5ParserEnabled' => true, + 'isRemoteEnabled' => true, ]); - } catch (\Exception $e) { - return ApiResponse::OK->response([ - 'message' => 'Error al eliminar el archivo', - 'error' => $e->getMessage(), - ]); - } + return $pdf->stream('hoja-verificacion-' . $id . '.pdf'); } + + public function generatePdfConstancia($id) + { + $record = Record::with('vehicle.owner', 'user')->findOrFail($id); + + $pdf = Pdf::loadView('pdfs.constancia', compact('record')) + ->setPaper('a4', 'landscape') + ->setOptions([ + 'defaultFont' => 'sans-serif', + 'isHtml5ParserEnabled' => true, + 'isRemoteEnabled' => true, + ]); + + return $pdf->stream('constancia-inscripcion' . $id . '.pdf'); + } + + + } diff --git a/app/Http/Controllers/Repuve/RepuveController.php b/app/Http/Controllers/Repuve/RepuveController.php index d23cf3d..08904a5 100644 --- a/app/Http/Controllers/Repuve/RepuveController.php +++ b/app/Http/Controllers/Repuve/RepuveController.php @@ -1,9 +1,12 @@ - $folio, 'vehicle_id' => $vehicle->id, - 'user_id' => auth()->id() ?? null, + 'user_id' => auth()->id(), ]); //Procesar y guardar archivos si existen @@ -108,7 +111,7 @@ public function inscripcionVehiculo(VehicleStoreRequest $request) foreach ($files as $index => $file) { // Generar nombre único - $fileName = time() . '_' . $index . '_' . $file->getClientOriginalName(); + $fileName = uniqid() . '_' . time() . '_' . $file->getClientOriginalName(); // Guardar archivos $path = $file->storeAs('records', $fileName, 'public'); @@ -164,7 +167,6 @@ public function inscripcionVehiculo(VehicleStoreRequest $request) 'files' => $uploadedFiles, 'total_files' => count($uploadedFiles), ]); - } catch (\Exception $e) { // Revertir transacción en caso de error DB::rollBack(); @@ -192,15 +194,34 @@ private function checkIfStolen(string $folio): bool public function consultaExpediente(Request $request) { $request->validate([ - 'folio' => 'required|string', + 'folio' => 'nullable|string', + 'placa' => 'nullable|string', + 'niv' => 'nullable|string', + ], [ + 'required_without_all' => 'Debe proporcionar al menos uno de los siguientes: folio, placa o NIV.' ]); - $folio = $request->input('folio'); + if (!$request->filled('folio') && !$request->filled('placa') && !$request->filled('niv')) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Debe proporcionar al menos uno de los siguientes parámetros: folio, placa o niv.' + ]); + } - // Cargar record con todas sus relaciones - $record = Record::where('folio', $folio) - ->with(['vehicle.owner', 'files', 'user']) - ->first(); + $query = Record::with(['vehicle.owner', 'vehicle.tag', 'files', 'user']); + + if ($request->filled('folio')) { + $query->where('folio', $request->input('folio')); + } elseif ($request->filled('placa')) { + $query->whereHas('vehicle', function ($q) use ($request) { + $q->where('placa', $request->input('placa')); + }); + } elseif ($request->filled('niv')) { + $query->whereHas('vehicle', function ($q) use ($request) { + $q->where('numero_serie', $request->input('niv')); + }); + } + + $record = $query->first(); if (!$record) { return ApiResponse::NOT_FOUND->response([ @@ -214,6 +235,7 @@ public function consultaExpediente(Request $request) 'folio' => $record->folio, 'vehicle' => $record->vehicle, 'owner' => $record->vehicle->owner, + 'tag' => $record->vehicle->tag, 'files' => $record->files, 'user' => $record->user, 'created_at' => $record->created_at->toDateTimeString(), @@ -221,6 +243,139 @@ public function consultaExpediente(Request $request) ]); } + public function actualizarVehiculo(VehicleUpdateRequest $request, $recordId) + { + try { + // Buscar el registro existente + $record = Record::with(['vehicle.owner'])->findOrFail($recordId); + + // Iniciar transacción + DB::beginTransaction(); + + $vehicleData = $this->getVehicle2(); + $ownerData = $this->getOwner(); + + // Actualizar propietario + $owner = Owner::updateOrCreate( + ['rfc' => $ownerData['rfc']], + [ + 'name' => $ownerData['name'], + 'paternal' => $ownerData['paternal'], + 'maternal' => $ownerData['maternal'], + 'curp' => $ownerData['curp'], + 'address' => $ownerData['address'], + ] + ); + + // Actualizar vehículo + $record->vehicle->update([ + 'anio_placa' => $vehicleData['ANIO_PLACA'], + 'placa' => $vehicleData['PLACA'], + 'numero_serie' => $vehicleData['NO_SERIE'], + 'rfc' => $vehicleData['RFC'], + 'folio' => $vehicleData['FOLIO'], + 'vigencia' => $vehicleData['VIGENCIA'], + 'fecha_impresion' => $vehicleData['FECHA_IMPRESION'], + 'qr_hash' => $vehicleData['QR_HASH'], + 'valido' => $vehicleData['VALIDO'], + 'foliotemp' => $vehicleData['FOLIOTEMP'], + 'nombre' => $vehicleData['NOMBRE'], + 'nombre2' => $vehicleData['NOMBRE2'], + 'municipio' => $vehicleData['MUNICIPIO'], + 'localidad' => $vehicleData['LOCALIDAD'], + 'calle' => $vehicleData['CALLE'], + 'calle2' => $vehicleData['CALLE2'], + 'tipo' => $vehicleData['TIPO'], + 'tipo_servicio' => $vehicleData['TIPO_SERVICIO'], + 'marca' => $vehicleData['MARCA'], + 'linea' => $vehicleData['LINEA'], + 'sublinea' => $vehicleData['SUBLINEA'], + 'modelo' => $vehicleData['MODELO'], + 'numero_motor' => $vehicleData['NUMERO_MOTOR'], + 'descripcion_origen' => $vehicleData['DESCRIPCION_ORIGEN'], + 'color' => $vehicleData['COLOR'], + 'codigo_postal' => $vehicleData['CODIGO_POSTAL'], + 'serie_folio' => $vehicleData['SERIE_FOLIO'], + 'sfolio' => $vehicleData['SFOLIO'], + 'nrpv' => $vehicleData['NUMERO_SERIE'], + 'owner_id' => $owner->id, + ]); + + // Procesar nuevos archivos si existen + $uploadedFiles = []; + if ($request->hasFile('files')) { + $files = $request->file('files'); + $fileNames = $request->input('names', []); + + foreach ($files as $index => $file) { + // Generar nombre + $fileName = uniqid() . '_' . time() . '_' . $file->getClientOriginalName(); + + // Guardar archivos + $path = $file->storeAs('records', $fileName, 'public'); + + // Calcular MD5 + $md5 = md5_file($file->getRealPath()); + + // Crear registro en BD + $fileRecord = File::create([ + 'name' => $fileNames[$index] ?? "Archivo " . ($index + 1), + 'path' => $path, + 'md5' => $md5, + 'record_id' => $record->id, + ]); + + $uploadedFiles[] = [ + 'id' => $fileRecord->id, + 'name' => $fileRecord->name, + 'path' => $fileRecord->path, + 'url' => $fileRecord->url, + ]; + } + } + + // Confirmar transacción + DB::commit(); + + // Responder con éxito + return ApiResponse::OK->response([ + 'message' => 'Vehículo actualizado exitosamente', + 'record' => [ + 'id' => $record->id, + 'folio' => $record->folio, + 'vehicle_id' => $record->vehicle->id, + 'updated_at' => $record->updated_at->toDateTimeString(), + ], + 'vehicle' => [ + 'id' => $record->vehicle->id, + 'placa' => $record->vehicle->placa, + 'numero_serie' => $record->vehicle->numero_serie, + 'marca' => $record->vehicle->marca, + 'modelo' => $record->vehicle->modelo, + ], + 'owner' => [ + 'id' => $owner->id, + 'full_name' => $owner->full_name, + 'rfc' => $owner->rfc, + ], + 'new_files' => $uploadedFiles, + 'total_new_files' => count($uploadedFiles), + ]); + } catch (\Exception $e) { + DB::rollBack(); + + Log::error('Error en actualizarVehiculo: ' . $e->getMessage(), [ + 'record_id' => $recordId ?? null, + 'trace' => $e->getTraceAsString() + ]); + + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Error al actualizar el vehículo', + 'error' => $e->getMessage(), + ]); + } + } + private function getVehicle(): array { return [ @@ -228,7 +383,7 @@ private function getVehicle(): array "PLACA" => "WNU700B", "NO_SERIE" => "LSGHD52H0ND032457", "RFC" => "GME111116GJA", - "FOLIO" => "3962243", + "FOLIO" => "EXP-2025-201030", "VIGENCIA" => "2025", "FECHA_IMPRESION" => "10-01-2025", "QR_HASH" => "Vu5TF4kYsbbltzjDdGQyenKfZoIk2wro34a5Gkh9JVh0CFxfPlrd92YEWK21JF.nLjQNyzKmqRvWYuPiS.kU7A--", @@ -256,6 +411,41 @@ private function getVehicle(): array ]; } + private function getVehicle2(): array + { + return [ + "ANIO_PLACA" => "2027", + "PLACA" => "WNU700Z", + "NO_SERIE" => "EXP-2025-201030", + "RFC" => "GME111116GJA", + "FOLIO" => "EXP-2025-201030", + "VIGENCIA" => "2026", + "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 119 ", + "CALLE2" => "C BU*ILIA*18 ", + "TIPO" => "SEDAN", + "TIPO_SERVICIO" => "PARTICULAR", + "MARCA" => "CHEVROLET G.M.C.", + "LINEA" => "AVEO", + "SUBLINEA" => "PAQ. \"A\" LS", + "MODELO" => 2023, + "NUMERO_SERIE" => "EXP-2025-201030", + "NUMERO_MOTOR" => "H. EN WUHANLL,SGM", + "DESCRIPCION_ORIGEN" => "IMPORTADO", + "COLOR" => "AZUL", + "CODIGO_POSTAL" => "86181", + "SERIE_FOLIO" => "D3962242", + "SFOLIO" => "EXP-2025-201030" + ]; + } + private function getOwner(): array { return [ @@ -267,6 +457,4 @@ private function getOwner(): array 'address' => 'Fracc Pomoca, Calle Armadillo MZ9 LT28', ]; } - - } diff --git a/app/Http/Requests/Repuve/CancelConstanciaRequest.php b/app/Http/Requests/Repuve/CancelConstanciaRequest.php index 9579d06..f6c30db 100644 --- a/app/Http/Requests/Repuve/CancelConstanciaRequest.php +++ b/app/Http/Requests/Repuve/CancelConstanciaRequest.php @@ -20,8 +20,7 @@ public function authorize(): bool public function rules(): array { return [ - 'vehicle_id' => 'required|integer|exists:vehicle,id', - 'tag_id' => 'required|integer|exists:tags,id', + 'record_id' => 'required|integer|exists:records,id', 'cancellation_reason' => 'required|in:fallo_lectura_handheld,cambio_parabrisas,roto_al_pegarlo,extravio,otro', 'cancellation_observations' => 'nullable|string|max:1000', ]; @@ -33,13 +32,9 @@ public function rules(): array 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.', + 'record_id.required' => 'El id del expediente es obligatorio.', + 'record_id.integer' => 'El id del expediente debe ser un número entero.', + 'record_id.exists' => 'El expediente 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.', @@ -55,8 +50,7 @@ public function messages(): array public function attributes(): array { return [ - 'vehicle_id' => 'id del vehículo', - 'tag_id' => 'id del tag', + 'record_id' => 'id del expediente', '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 deleted file mode 100644 index f459586..0000000 --- a/app/Http/Requests/Repuve/ConsultaVehiculoRequest.php +++ /dev/null @@ -1,52 +0,0 @@ - '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/Http/Requests/Repuve/RecordSearchRequest.php b/app/Http/Requests/Repuve/RecordSearchRequest.php new file mode 100644 index 0000000..1115a1d --- /dev/null +++ b/app/Http/Requests/Repuve/RecordSearchRequest.php @@ -0,0 +1,50 @@ + ['nullable', 'string', 'max:50'], + 'niv' => ['nullable', 'string', 'max:50'], + 'numero_serie' => ['nullable', 'string', 'max:50'], + 'fecha_desde' => ['nullable', 'date', 'date_format:Y-m-d'], + 'fecha_hasta' => ['nullable', 'date', 'date_format:Y-m-d', 'after_or_equal:fecha_desde'], + ]; + } + + public function messages(): array + { + return [ + 'folio.string' => 'El folio debe ser una cadena de texto', + 'niv.string' => 'El NIV debe ser una cadena de texto', + 'numero_serie.string' => 'El número de serie debe ser una cadena de texto', + 'fecha_desde.date' => 'La fecha desde debe ser una fecha válida', + 'fecha_desde.date_format' => 'La fecha desde debe tener el formato Y-m-d', + 'fecha_hasta.date' => 'La fecha hasta debe ser una fecha válida', + 'fecha_hasta.after_or_equal' => 'La fecha hasta debe ser posterior o igual a la fecha desde', + ]; + } + + public function withValidator($validator) + { + $validator->after(function ($validator) { + if (!$this->filled('folio') && + !$this->filled('niv') && + !$this->filled('numero_serie') && + !$this->filled('fecha_desde')) { + $validator->errors()->add( + 'search', + 'Debe proporcionar al menos un criterio de búsqueda (folio, niv o fecha_desde)' + ); + } + }); + } +} diff --git a/app/Http/Requests/Repuve/VehicleStoreRequest.php b/app/Http/Requests/Repuve/VehicleStoreRequest.php index 16dc00f..c023ae4 100644 --- a/app/Http/Requests/Repuve/VehicleStoreRequest.php +++ b/app/Http/Requests/Repuve/VehicleStoreRequest.php @@ -16,8 +16,8 @@ public function rules(): array 'folio' => ['required', 'string', 'max:50'], 'files' => ['nullable', 'array', 'min:1'], 'files.*' => ['file', 'mimes:jpeg,png,jpg,pdf', 'max:10240'], - 'file_names' => ['nullable', 'array'], - 'file_names.*' => ['string', 'max:255'], + 'names' => ['nullable', 'array'], + 'names.*' => ['string', 'max:255'], ]; } diff --git a/app/Http/Requests/Repuve/VehicleUpdateRequest.php b/app/Http/Requests/Repuve/VehicleUpdateRequest.php new file mode 100644 index 0000000..35805b8 --- /dev/null +++ b/app/Http/Requests/Repuve/VehicleUpdateRequest.php @@ -0,0 +1,24 @@ + ['sometimes', 'string', 'max:50'], + 'files' => ['nullable', 'array', 'min:1'], + 'files.*' => ['file', 'mimes:jpeg,png,jpg,pdf', 'max:10240'], + 'names' => ['nullable', 'array'], + 'names.*' => ['string', 'max:255'], + 'replace_files' => ['nullable', 'boolean'], + ]; + } +} diff --git a/app/Models/Device.php b/app/Models/Device.php index 5934ecf..b4180ec 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -23,4 +23,23 @@ protected function casts(): array ]; } + public function modules() + { + return $this->belongsToMany(Module::class, 'device_module') + ->withPivot('status') + ->withTimestamps(); + } + + public function deviceModules() + { + return $this->hasMany(DeviceModule::class); + } + + public function activeModules() + { + return $this->belongsToMany(Module::class, 'device_module') + ->wherePivot('status', true) + ->withPivot('status') + ->withTimestamps(); + } } diff --git a/app/Models/DeviceModule.php b/app/Models/DeviceModule.php index 5986f37..487d961 100644 --- a/app/Models/DeviceModule.php +++ b/app/Models/DeviceModule.php @@ -23,4 +23,14 @@ protected function casts(): array 'status' => 'boolean', ]; } + + public function device() + { + return $this->belongsTo(Device::class); + } + + public function module() + { + return $this->belongsTo(Module::class); + } } diff --git a/app/Models/Error.php b/app/Models/Error.php index 832a84c..fb15fb8 100644 --- a/app/Models/Error.php +++ b/app/Models/Error.php @@ -14,4 +14,8 @@ class Error extends Model 'description', ]; + public function records() + { + return $this->hasMany(Record::class); + } } diff --git a/app/Models/Module.php b/app/Models/Module.php index 63f1290..c95dee0 100644 --- a/app/Models/Module.php +++ b/app/Models/Module.php @@ -28,4 +28,28 @@ protected function casts(): array ]; } + public function devices() + { + return $this->belongsToMany(Device::class, 'device_module') + ->withPivot('status') + ->withTimestamps(); + } + + public function deviceModules() + { + return $this->hasMany(DeviceModule::class); + } + + public function activeDevices() + { + return $this->belongsToMany(Device::class, 'device_module') + ->wherePivot('status', true) + ->withPivot('status') + ->withTimestamps(); + } + + public function packages() + { + return $this->hasMany(Package::class); + } } diff --git a/app/Models/Owner.php b/app/Models/Owner.php index cf498a3..6044200 100644 --- a/app/Models/Owner.php +++ b/app/Models/Owner.php @@ -29,4 +29,9 @@ protected function fullName(): Attribute get: fn() => trim("{$this->name} {$this->paternal} {$this->maternal}") ); } + + public function vehicles() + { + return $this->hasMany(Vehicle::class); + } } diff --git a/app/Models/Package.php b/app/Models/Package.php index a4a9f08..55a6ba2 100644 --- a/app/Models/Package.php +++ b/app/Models/Package.php @@ -25,4 +25,13 @@ protected function casts(): array ]; } + public function module() + { + return $this->belongsTo(Module::class); + } + + public function tags() + { + return $this->hasMany(Tag::class); + } } diff --git a/app/Models/Record.php b/app/Models/Record.php index acbe04a..ec20b33 100644 --- a/app/Models/Record.php +++ b/app/Models/Record.php @@ -30,4 +30,9 @@ public function files() { return $this->hasMany(File::class); } + + public function error() + { + return $this->belongsTo(Error::class); + } } diff --git a/app/Models/ScanHistory.php b/app/Models/ScanHistory.php index ca4127d..c0801ce 100644 --- a/app/Models/ScanHistory.php +++ b/app/Models/ScanHistory.php @@ -15,4 +15,22 @@ class ScanHistory extends Model 'user_id', 'tag_id', ]; + + /** + * Relación con User + * Un escaneo pertenece a un usuario + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Relación con Tag + * Un escaneo pertenece a una etiqueta + */ + public function tag() + { + return $this->belongsTo(Tag::class); + } } diff --git a/app/Models/Tag.php b/app/Models/Tag.php index d736b6d..f9c5465 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -15,4 +15,24 @@ class Tag extends Model 'package_id', 'status', ]; + + public function vehicle() + { + return $this->belongsTo(Vehicle::class); + } + + public function package() + { + return $this->belongsTo(Package::class); + } + + public function vehicleTagLogs() + { + return $this->hasMany(VehicleTagLog::class); + } + + public function scanHistories() + { + return $this->hasMany(ScanHistory::class); + } } diff --git a/app/Models/Vehicle.php b/app/Models/Vehicle.php index 47db55b..a038508 100644 --- a/app/Models/Vehicle.php +++ b/app/Models/Vehicle.php @@ -16,11 +16,11 @@ class Vehicle extends Model 'placa', 'numero_serie', 'rfc', + 'folio', 'vigencia', 'fecha_impresion', 'qr_hash', 'valido', - 'foliotemp', 'nombre', 'nombre2', 'municipio', @@ -48,4 +48,18 @@ public function owner() return $this->belongsTo(Owner::class); } + public function records() + { + return $this->hasMany(Record::class); + } + + public function tag() + { + return $this->hasOne(Tag::class); + } + + public function vehicleTagLogs() + { + return $this->hasMany(VehicleTagLog::class); + } } diff --git a/composer.json b/composer.json index f8734ef..521893a 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "laravel/pulse": "^1.4", "laravel/reverb": "^1.4", "laravel/tinker": "^2.10", + "milon/barcode": "^12.0", "notsoweb/laravel-core": "dev-main", "spatie/laravel-permission": "^6.16", "tightenco/ziggy": "^2.5" diff --git a/composer.lock b/composer.lock index 1bd8140..4fc2bc5 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": "28203b03d474b52340b6e7941df2e111", + "content-hash": "2e95bbdb182abae36557342b05654934", "packages": [ { "name": "barryvdh/laravel-dompdf", @@ -3270,6 +3270,81 @@ }, "time": "2025-07-25T09:04:22+00:00" }, + { + "name": "milon/barcode", + "version": "v12.0.0", + "source": { + "type": "git", + "url": "https://github.com/milon/barcode.git", + "reference": "252dc9a530c72454bc6cefb8d274c2acaba24f15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/milon/barcode/zipball/252dc9a530c72454bc6cefb8d274c2acaba24f15", + "reference": "252dc9a530c72454bc6cefb8d274c2acaba24f15", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "illuminate/support": "^7.0|^8.0|^9.0|^10.0 | ^11.0 | ^12.0", + "php": "^7.3 | ^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "DNS1D": "Milon\\Barcode\\Facades\\DNS1DFacade", + "DNS2D": "Milon\\Barcode\\Facades\\DNS2DFacade" + }, + "providers": [ + "Milon\\Barcode\\BarcodeServiceProvider" + ] + } + }, + "autoload": { + "psr-0": { + "Milon\\Barcode": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Nuruzzaman Milon", + "email": "contact@milon.im" + } + ], + "description": "Barcode generator like Qr Code, PDF417, C39, C39+, C39E, C39E+, C93, S25, S25+, I25, I25+, C128, C128A, C128B, C128C, 2-Digits UPC-Based Extention, 5-Digits UPC-Based Extention, EAN 8, EAN 13, UPC-A, UPC-E, MSI (Variation of Plessey code)", + "keywords": [ + "CODABAR", + "CODE 128", + "CODE 39", + "barcode", + "datamatrix", + "ean", + "laravel", + "pdf417", + "qr code", + "qrcode" + ], + "support": { + "issues": "https://github.com/milon/barcode/issues", + "source": "https://github.com/milon/barcode/tree/v12.0.0" + }, + "funding": [ + { + "url": "https://paypal.me/nuruzzamanmilon", + "type": "custom" + }, + { + "url": "https://github.com/milon", + "type": "github" + } + ], + "time": "2025-02-24T18:09:25+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", diff --git a/database/factories/ErrorFactory.php b/database/factories/ErrorFactory.php new file mode 100644 index 0000000..e033343 --- /dev/null +++ b/database/factories/ErrorFactory.php @@ -0,0 +1,64 @@ + + */ +class ErrorFactory extends Factory +{ + protected $model = Error::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $errorTypes = [ + ['code' => 'E001', 'description' => 'Vehículo reportado como robado'], + ['code' => 'E002', 'description' => 'Número de serie no válido o no coincide'], + ['code' => 'E003', 'description' => 'Documentos incompletos o ilegibles'], + ['code' => 'E004', 'description' => 'Propietario no coincide con documentos'], + ['code' => 'E005', 'description' => 'Placas no corresponden al vehículo'], + ['code' => 'E006', 'description' => 'Vehículo presenta adulteración'], + ['code' => 'E007', 'description' => 'RFC o CURP inválido'], + ['code' => 'E008', 'description' => 'Factura apócrifa o alterada'], + ['code' => 'E009', 'description' => 'Vehículo importado sin documentación legal'], + ['code' => 'E010', 'description' => 'Error en sistema REPUVE externo'], + ]; + + $error = fake()->randomElement($errorTypes); + + return [ + 'code' => $error['code'] . '-' . fake()->unique()->numberBetween(1000, 9999), + 'description' => $error['description'], + ]; + } + + /** + * State for stolen vehicle error + */ + public function stolen(): static + { + return $this->state(fn (array $attributes) => [ + 'code' => 'E001-' . fake()->unique()->numberBetween(1000, 9999), + 'description' => 'Vehículo reportado como robado', + ]); + } + + /** + * State for invalid VIN error + */ + public function invalidVin(): static + { + return $this->state(fn (array $attributes) => [ + 'code' => 'E002-' . fake()->unique()->numberBetween(1000, 9999), + 'description' => 'Número de serie no válido o no coincide', + ]); + } +} diff --git a/database/factories/FileFactory.php b/database/factories/FileFactory.php new file mode 100644 index 0000000..165e103 --- /dev/null +++ b/database/factories/FileFactory.php @@ -0,0 +1,128 @@ + + */ +class FileFactory extends Factory +{ + protected $model = File::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $fileTypes = [ + ['name' => 'Foto frontal del vehículo', 'ext' => 'jpg'], + ['name' => 'Foto lateral derecha del vehículo', 'ext' => 'jpg'], + ['name' => 'Foto lateral izquierda del vehículo', 'ext' => 'jpg'], + ['name' => 'Foto trasera del vehículo', 'ext' => 'jpg'], + ['name' => 'Foto del NIV/VIN', 'ext' => 'jpg'], + ['name' => 'Tarjeta de circulación', 'ext' => 'pdf'], + ['name' => 'Factura original', 'ext' => 'pdf'], + ['name' => 'Comprobante de domicilio', 'ext' => 'pdf'], + ['name' => 'Identificación oficial del propietario', 'ext' => 'pdf'], + ['name' => 'Constancia de inscripción', 'ext' => 'pdf'], + ]; + + $fileType = fake()->randomElement($fileTypes); + $timestamp = now()->timestamp . '_' . fake()->numberBetween(1000, 9999); + $fileName = $timestamp . '.' . $fileType['ext']; + + return [ + 'name' => $fileType['name'], + 'path' => 'records/' . $fileName, + 'md5' => md5(fake()->uuid()), + 'record_id' => Record::factory(), + ]; + } + + /** + * Indicate that the file is an image + */ + public function image(): static + { + $imageTypes = [ + 'Foto frontal del vehículo', + 'Foto lateral derecha del vehículo', + 'Foto lateral izquierda del vehículo', + 'Foto trasera del vehículo', + 'Foto del NIV/VIN', + ]; + + return $this->state(function (array $attributes) use ($imageTypes) { + $name = fake()->randomElement($imageTypes); + $timestamp = now()->timestamp . '_' . fake()->numberBetween(1000, 9999); + + return [ + 'name' => $name, + 'path' => 'records/' . $timestamp . '.jpg', + ]; + }); + } + + /** + * Indicate that the file is a PDF document + */ + public function pdf(): static + { + $pdfTypes = [ + 'Tarjeta de circulación', + 'Factura original', + 'Comprobante de domicilio', + 'Identificación oficial del propietario', + 'Constancia de inscripción', + ]; + + return $this->state(function (array $attributes) use ($pdfTypes) { + $name = fake()->randomElement($pdfTypes); + $timestamp = now()->timestamp . '_' . fake()->numberBetween(1000, 9999); + + return [ + 'name' => $name, + 'path' => 'records/' . $timestamp . '.pdf', + ]; + }); + } + + /** + * Indicate that the file belongs to a specific record + */ + public function forRecord(int $recordId): static + { + return $this->state(fn (array $attributes) => [ + 'record_id' => $recordId, + ]); + } + + /** + * Create a vehicle photo file + */ + public function vehiclePhoto(): static + { + $photoTypes = [ + 'Foto frontal del vehículo', + 'Foto lateral derecha del vehículo', + 'Foto lateral izquierda del vehículo', + 'Foto trasera del vehículo', + ]; + + return $this->state(function (array $attributes) use ($photoTypes) { + $name = fake()->randomElement($photoTypes); + $timestamp = now()->timestamp . '_' . fake()->numberBetween(1000, 9999); + + return [ + 'name' => $name, + 'path' => 'records/' . $timestamp . '.jpg', + ]; + }); + } +} diff --git a/database/factories/ModuleFactory.php b/database/factories/ModuleFactory.php new file mode 100644 index 0000000..6ad75e6 --- /dev/null +++ b/database/factories/ModuleFactory.php @@ -0,0 +1,76 @@ + + */ +class ModuleFactory extends Factory +{ + protected $model = Module::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $municipalities = [ + 'Centro', + 'Cárdenas', + 'Comalcalco', + 'Cunduacán', + 'Huimanguillo', + 'Macuspana', + 'Paraíso', + 'Tacotalpa', + 'Teapa', + 'Tenosique', + ]; + + $colonies = [ + 'Centro', + 'Tierra Colorada', + 'Atasta de Serra', + 'José Colomo', + 'La Manga', + 'Tamulté de las Barrancas', + 'Gaviotas Norte', + 'Carrizal', + ]; + + return [ + 'name' => fake()->company() . ' - ' . fake()->randomElement($municipalities), + 'municipality' => fake()->randomElement($municipalities), + 'address' => fake()->streetAddress(), + 'colony' => fake()->randomElement($colonies), + 'longitude' => fake()->longitude(-93.5, -92.5), // Tabasco longitude range + 'latitude' => fake()->latitude(17.5, 18.5), // Tabasco latitude range + 'status' => fake()->boolean(90), // 90% activos + ]; + } + + /** + * Indicate that the module is inactive. + */ + public function inactive(): static + { + return $this->state(fn (array $attributes) => [ + 'status' => false, + ]); + } + + /** + * Indicate that the module is in Centro municipality. + */ + public function centro(): static + { + return $this->state(fn (array $attributes) => [ + 'municipality' => 'Centro', + ]); + } +} diff --git a/database/factories/OwnerFactory.php b/database/factories/OwnerFactory.php new file mode 100644 index 0000000..0df8125 --- /dev/null +++ b/database/factories/OwnerFactory.php @@ -0,0 +1,116 @@ + + */ +class OwnerFactory extends Factory +{ + protected $model = Owner::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $name = fake()->firstName(); + $paternal = fake()->lastName(); + $maternal = fake()->lastName(); + + return [ + 'name' => strtoupper($name), + 'paternal' => strtoupper($paternal), + 'maternal' => strtoupper($maternal), + 'rfc' => $this->generateRFC($paternal, $maternal, $name), + 'curp' => $this->generateCURP($paternal, $maternal, $name), + 'address' => strtoupper(fake()->address()), + ]; + } + + /** + * Generate a realistic RFC (13 characters) + */ + private function generateRFC(string $paternal, string $maternal, string $name): string + { + $firstPaternal = substr($paternal, 0, 1); + $firstVowelPaternal = $this->getFirstVowel(substr($paternal, 1)); + $firstMaternal = substr($maternal, 0, 1); + $firstName = substr($name, 0, 1); + + $year = fake()->numberBetween(50, 99); + $month = str_pad(fake()->numberBetween(1, 12), 2, '0', STR_PAD_LEFT); + $day = str_pad(fake()->numberBetween(1, 28), 2, '0', STR_PAD_LEFT); + + $homoclave = strtoupper(fake()->bothify('???')); + + return strtoupper($firstPaternal . $firstVowelPaternal . $firstMaternal . $firstName . $year . $month . $day . $homoclave); + } + + /** + * Generate a realistic CURP (18 characters) + */ + private function generateCURP(string $paternal, string $maternal, string $name): string + { + $firstPaternal = substr($paternal, 0, 1); + $firstVowelPaternal = $this->getFirstVowel(substr($paternal, 1)); + $firstMaternal = substr($maternal, 0, 1); + $firstName = substr($name, 0, 1); + + $year = str_pad(fake()->numberBetween(50, 99), 2, '0', STR_PAD_LEFT); + $month = str_pad(fake()->numberBetween(1, 12), 2, '0', STR_PAD_LEFT); + $day = str_pad(fake()->numberBetween(1, 28), 2, '0', STR_PAD_LEFT); + + $gender = fake()->randomElement(['H', 'M']); + $state = 'TC'; // Tabasco + + $consonants = strtoupper( + $this->getFirstConsonant(substr($paternal, 1)) . + $this->getFirstConsonant(substr($maternal, 1)) . + $this->getFirstConsonant(substr($name, 1)) + ); + + $homoclave = strtoupper(fake()->bothify('??')); + + return strtoupper($firstPaternal . $firstVowelPaternal . $firstMaternal . $firstName . $year . $month . $day . $gender . $state . $consonants . $homoclave); + } + + /** + * Get first vowel from string + */ + private function getFirstVowel(string $str): string + { + $vowels = ['A', 'E', 'I', 'O', 'U']; + $str = strtoupper($str); + + for ($i = 0; $i < strlen($str); $i++) { + if (in_array($str[$i], $vowels)) { + return $str[$i]; + } + } + + return 'X'; + } + + /** + * Get first consonant from string + */ + private function getFirstConsonant(string $str): string + { + $vowels = ['A', 'E', 'I', 'O', 'U']; + $str = strtoupper($str); + + for ($i = 0; $i < strlen($str); $i++) { + if (!in_array($str[$i], $vowels) && ctype_alpha($str[$i])) { + return $str[$i]; + } + } + + return 'X'; + } +} diff --git a/database/factories/PackageFactory.php b/database/factories/PackageFactory.php new file mode 100644 index 0000000..1495d75 --- /dev/null +++ b/database/factories/PackageFactory.php @@ -0,0 +1,76 @@ + + */ +class PackageFactory extends Factory +{ + protected $model = Package::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $year = fake()->numberBetween(2020, 2025); + $lot = 'LOTE-' . $year . '-' . fake()->numberBetween(1, 99); + $boxNumber = 'CAJA-' . strtoupper(fake()->bothify('??##')); + + $startingPage = fake()->numberBetween(1, 100) * 100; + $endingPage = $startingPage + fake()->numberBetween(50, 200); + + return [ + 'lot' => $lot, + 'box_number' => $boxNumber, + 'starting_page' => $startingPage, + 'ending_page' => $endingPage, + 'module_id' => Module::factory(), + ]; + } + + /** + * Indicate that the package belongs to a specific module. + */ + public function forModule(int $moduleId): static + { + return $this->state(fn (array $attributes) => [ + 'module_id' => $moduleId, + ]); + } + + /** + * Create a small package (50-100 pages) + */ + public function small(): static + { + return $this->state(function (array $attributes) { + $startingPage = fake()->numberBetween(1, 50) * 100; + return [ + 'starting_page' => $startingPage, + 'ending_page' => $startingPage + fake()->numberBetween(50, 100), + ]; + }); + } + + /** + * Create a large package (200-500 pages) + */ + public function large(): static + { + return $this->state(function (array $attributes) { + $startingPage = fake()->numberBetween(1, 100) * 100; + return [ + 'starting_page' => $startingPage, + 'ending_page' => $startingPage + fake()->numberBetween(200, 500), + ]; + }); + } +} diff --git a/database/factories/RecordFactory.php b/database/factories/RecordFactory.php new file mode 100644 index 0000000..20817c2 --- /dev/null +++ b/database/factories/RecordFactory.php @@ -0,0 +1,93 @@ + + */ +class RecordFactory extends Factory +{ + protected $model = Record::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'folio' => $this->generateFolio(), + 'vehicle_id' => Vehicle::factory(), + 'user_id' => User::inRandomOrder()->first()?->id ?? User::factory(), + 'error_id' => fake()->boolean(10) ? Error::factory() : null, // 10% con error + ]; + } + + /** + * Generate a unique record folio + */ + private function generateFolio(): string + { + $year = now()->year; + $number = fake()->unique()->numerify('######'); + + return 'EXP-' . $year . '-' . $number; + } + + /** + * Indicate that the record has an error + */ + public function withError(): static + { + return $this->state(fn (array $attributes) => [ + 'error_id' => Error::factory(), + ]); + } + + /** + * Indicate that the record has no error + */ + public function withoutError(): static + { + return $this->state(fn (array $attributes) => [ + 'error_id' => null, + ]); + } + + /** + * Indicate that the record belongs to a specific vehicle + */ + public function forVehicle(int $vehicleId): static + { + return $this->state(fn (array $attributes) => [ + 'vehicle_id' => $vehicleId, + ]); + } + + /** + * Indicate that the record belongs to a specific user + */ + public function forUser(int $userId): static + { + return $this->state(fn (array $attributes) => [ + 'user_id' => $userId, + ]); + } + + /** + * Indicate that the record has a specific error + */ + public function withSpecificError(int $errorId): static + { + return $this->state(fn (array $attributes) => [ + 'error_id' => $errorId, + ]); + } +} diff --git a/database/factories/TagFactory.php b/database/factories/TagFactory.php new file mode 100644 index 0000000..ae0f07b --- /dev/null +++ b/database/factories/TagFactory.php @@ -0,0 +1,110 @@ + + */ +class TagFactory extends Factory +{ + protected $model = Tag::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $statuses = ['available', 'assigned', 'cancelled', 'lost']; + $weights = [40, 50, 7, 3]; // Probabilidades: 40% available, 50% assigned, 7% cancelled, 3% lost + + return [ + 'folio' => $this->generateFolio(), + 'vehicle_id' => fake()->boolean(60) ? Vehicle::factory() : null, // 60% asignados + 'package_id' => Package::factory(), + 'status' => fake()->randomElement(array_combine($statuses, $weights)) ?? 'available', + ]; + } + + /** + * Generate a unique tag folio + */ + private function generateFolio(): string + { + $year = fake()->numberBetween(2020, 2025); + $series = strtoupper(fake()->bothify('??')); + $number = fake()->unique()->numerify('########'); + + return 'TAG-' . $year . '-' . $series . '-' . $number; + } + + /** + * Indicate that the tag is available (not assigned) + */ + public function available(): static + { + return $this->state(fn (array $attributes) => [ + 'vehicle_id' => null, + 'status' => 'available', + ]); + } + + /** + * Indicate that the tag is assigned to a vehicle + */ + public function assigned(): static + { + return $this->state(fn (array $attributes) => [ + 'vehicle_id' => Vehicle::factory(), + 'status' => 'assigned', + ]); + } + + /** + * Indicate that the tag is cancelled + */ + public function cancelled(): static + { + return $this->state(fn (array $attributes) => [ + 'status' => 'cancelled', + ]); + } + + /** + * Indicate that the tag is lost + */ + public function lost(): static + { + return $this->state(fn (array $attributes) => [ + 'vehicle_id' => null, + 'status' => 'lost', + ]); + } + + /** + * Indicate that the tag belongs to a specific package + */ + public function forPackage(int $packageId): static + { + return $this->state(fn (array $attributes) => [ + 'package_id' => $packageId, + ]); + } + + /** + * Indicate that the tag is assigned to a specific vehicle + */ + public function forVehicle(int $vehicleId): static + { + return $this->state(fn (array $attributes) => [ + 'vehicle_id' => $vehicleId, + 'status' => 'assigned', + ]); + } +} diff --git a/database/factories/VehicleFactory.php b/database/factories/VehicleFactory.php new file mode 100644 index 0000000..aad826a --- /dev/null +++ b/database/factories/VehicleFactory.php @@ -0,0 +1,128 @@ + + */ +class VehicleFactory extends Factory +{ + protected $model = Vehicle::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $brands = [ + 'CHEVROLET G.M.C.', + 'NISSAN MEXICANA', + 'VOLKSWAGEN', + 'FORD MOTOR COMPANY', + 'TOYOTA', + 'HONDA', + 'MAZDA', + 'KIA MOTORS', + 'HYUNDAI', + ]; + + $types = ['SEDAN', 'SUV', 'PICKUP', 'HATCHBACK', 'VAN']; + $colors = ['BLANCO', 'NEGRO', 'GRIS', 'ROJO', 'AZUL', 'PLATA']; + $municipalities = ['CENTRO', 'CÁRDENAS', 'COMALCALCO', 'CUNDUACÁN', 'HUIMANGUILLO']; + + $year = fake()->numberBetween(2015, 2025); + $vin = $this->generateVIN(); + $placa = $this->generatePlaca(); + + return [ + 'anio_placa' => (string) $year, + 'placa' => $placa, + 'numero_serie' => $vin, + 'rfc' => 'GME' . fake()->numerify('######') . 'GJA', + 'folio' => fake()->unique()->numerify('#######'), + 'vigencia' => (string) ($year + 1), + 'fecha_impresion' => fake()->date('d-m-Y'), + 'qr_hash' => fake()->sha256(), + 'valido' => fake()->boolean(95), // 95% válidos + 'nombre' => strtoupper(fake()->company()), + 'nombre2' => strtoupper(fake()->lexify('????*????')), + 'municipio' => fake()->randomElement($municipalities), + 'localidad' => 'VILLAHERMOSA', + 'calle' => strtoupper(fake()->streetAddress()), + 'calle2' => strtoupper(fake()->lexify('? ??*????')), + 'tipo' => fake()->randomElement($types), + 'tipo_servicio' => fake()->randomElement(['PARTICULAR', 'PUBLICO', 'OFICIAL']), + 'marca' => fake()->randomElement($brands), + 'linea' => strtoupper(fake()->word()), + 'sublinea' => 'PAQ. "' . strtoupper(fake()->randomLetter()) . '" ' . strtoupper(fake()->word()), + 'modelo' => $year, + 'numero_motor' => strtoupper(fake()->bothify('??####??##')), + 'descripcion_origen' => fake()->randomElement(['NACIONAL', 'IMPORTADO']), + 'color' => fake()->randomElement($colors), + 'codigo_postal' => fake()->numerify('86###'), + 'serie_folio' => 'D' . fake()->unique()->numerify('#######'), + 'sfolio' => fake()->unique()->numerify('#######'), + 'nrpv' => $vin, + 'owner_id' => Owner::factory(), + ]; + } + + /** + * Generate a realistic VIN (17 characters) + */ + private function generateVIN(): string + { + $wmi = strtoupper(fake()->bothify('???')); // World Manufacturer Identifier + $vds = strtoupper(fake()->bothify('??????')); // Vehicle Descriptor Section + $check = fake()->randomDigit(); + $year = strtoupper(fake()->randomLetter()); + $plant = fake()->randomDigit(); + $serial = fake()->numerify('######'); + + return $wmi . $vds . $check . $year . $plant . $serial; + } + + /** + * Generate a Tabasco plate + */ + private function generatePlaca(): string + { + return strtoupper(fake()->bothify('???###?')); + } + + /** + * Indicate that the vehicle belongs to a specific owner. + */ + public function forOwner(int $ownerId): static + { + return $this->state(fn (array $attributes) => [ + 'owner_id' => $ownerId, + ]); + } + + /** + * Create an imported vehicle + */ + public function imported(): static + { + return $this->state(fn (array $attributes) => [ + 'descripcion_origen' => 'IMPORTADO', + ]); + } + + /** + * Create a national vehicle + */ + public function national(): static + { + return $this->state(fn (array $attributes) => [ + 'descripcion_origen' => 'NACIONAL', + ]); + } +} diff --git a/database/migrations/2025_10_18_140600_create_vehicle_table.php b/database/migrations/2025_10_18_140600_create_vehicle_table.php index f721f1d..82eeabc 100644 --- a/database/migrations/2025_10_18_140600_create_vehicle_table.php +++ b/database/migrations/2025_10_18_140600_create_vehicle_table.php @@ -17,6 +17,7 @@ public function up(): void $table->string('placa')->unique()->nullable(); $table->string('numero_serie')->unique()->nullable(); $table->string('rfc')->nullable(); + $table->string('folio')->nullable(); $table->string('vigencia')->nullable(); $table->string('fecha_impresion')->nullable(); $table->string('qr_hash')->nullable(); diff --git a/database/migrations/2025_10_18_140700_create_tags_table.php b/database/migrations/2025_10_18_140700_create_tags_table.php index b48360a..9120bf7 100644 --- a/database/migrations/2025_10_18_140700_create_tags_table.php +++ b/database/migrations/2025_10_18_140700_create_tags_table.php @@ -14,7 +14,7 @@ public function up(): void Schema::create('tags', function (Blueprint $table) { $table->id(); $table->string('folio')->unique(); - $table->foreignId('vehicle_id')->nullable()->constrained('vehicle')->nullOnDelete(); + $table->foreignId('vehicle_id')->nullable()->unique()->constrained('vehicle')->nullOnDelete(); $table->foreignId('package_id')->nullable()->constrained('packages')->nullOnDelete(); $table->enum('status', ['available', 'assigned', 'cancelled', 'lost'])->default('available'); $table->timestamps(); diff --git a/database/migrations/2025_10_18_140900_create_records_table.php b/database/migrations/2025_10_18_140900_create_records_table.php index 4883806..2211fb1 100644 --- a/database/migrations/2025_10_18_140900_create_records_table.php +++ b/database/migrations/2025_10_18_140900_create_records_table.php @@ -13,7 +13,7 @@ public function up(): void { Schema::create('records', function (Blueprint $table) { $table->id(); - $table->string('folio'); + $table->string('folio')->unique(); $table->foreignId('vehicle_id')->constrained('vehicle')->cascadeOnDelete(); $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); $table->foreignId('error_id')->nullable()->constrained('errors')->nullOnDelete(); diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 31ba273..c70eaad 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -7,9 +7,9 @@ /** * Seeder Producción - * + * * @author Moisés Cortés C. - * + * * @version 1.0.0 */ class DatabaseSeeder extends Seeder @@ -19,6 +19,7 @@ class DatabaseSeeder extends Seeder */ public function run(): void { + // Seeders de producción (siempre se ejecutan) $this->call(RoleSeeder::class); $this->call(UserSeeder::class); $this->call(SettingSeeder::class); diff --git a/database/seeders/DevSeeder.php b/database/seeders/DevSeeder.php index ffea706..4377c22 100644 --- a/database/seeders/DevSeeder.php +++ b/database/seeders/DevSeeder.php @@ -7,9 +7,9 @@ /** * Seeder de desarrollo - * + * * @author Moisés Cortés C. - * + * * @version 1.0.0 */ class DevSeeder extends Seeder @@ -22,5 +22,21 @@ public function run(): void $this->call(RoleSeeder::class); $this->call(UserSeeder::class); $this->call(SettingSeeder::class); + + // Nivel 1 - Sin dependencias + $this->call(ModuleSeeder::class); + $this->call(OwnerSeeder::class); + $this->call(ErrorSeeder::class); + + // Nivel 2 - Dependen de Nivel 1 + $this->call(PackageSeeder::class); + $this->call(VehicleSeeder::class); + + // Nivel 3 - Dependen de Nivel 2 + $this->call(TagSeeder::class); + $this->call(RecordSeeder::class); + + // Nivel 4 - Dependen de Nivel 3 + $this->call(FileSeeder::class); } } diff --git a/database/seeders/ErrorSeeder.php b/database/seeders/ErrorSeeder.php new file mode 100644 index 0000000..894f9c4 --- /dev/null +++ b/database/seeders/ErrorSeeder.php @@ -0,0 +1,38 @@ + 'E001', 'description' => 'Vehículo reportado como robado'], + ['code' => 'E002', 'description' => 'Número de serie (NIV/VIN) no válido o no coincide'], + ['code' => 'E003', 'description' => 'Documentos incompletos o ilegibles'], + ['code' => 'E004', 'description' => 'Datos del propietario no coinciden con documentos oficiales'], + ['code' => 'E005', 'description' => 'Placas de circulación no corresponden al vehículo'], + ['code' => 'E006', 'description' => 'Vehículo presenta evidencia de adulteración en NIV'], + ['code' => 'E007', 'description' => 'RFC o CURP inválido o no coincide'], + ['code' => 'E008', 'description' => 'Factura apócrifa o presenta alteraciones'], + ['code' => 'E009', 'description' => 'Vehículo importado sin documentación legal'], + ['code' => 'E010', 'description' => 'Error en consulta al sistema REPUVE externo'], + ['code' => 'E011', 'description' => 'Vehículo tiene adeudos de tenencia o infracciones'], + ['code' => 'E012', 'description' => 'Tarjeta de circulación no válida o vencida'], + ['code' => 'E013', 'description' => 'Comprobante de domicilio no válido'], + ['code' => 'E014', 'description' => 'Verificación física del vehículo no aprobada'], + ['code' => 'E015', 'description' => 'Modelo y año del vehículo no coinciden'], + ]; + + foreach ($errors as $error) { + Error::create($error); + } + } +} diff --git a/database/seeders/FileSeeder.php b/database/seeders/FileSeeder.php new file mode 100644 index 0000000..0c721a7 --- /dev/null +++ b/database/seeders/FileSeeder.php @@ -0,0 +1,50 @@ +forRecord($record->id) + ->vehiclePhoto() + ->create(); + $imageCount++; + } + + // El resto son PDFs (documentos) + $pdfFiles = $filesPerRecord - $vehiclePhotos; + for ($i = 0; $i < $pdfFiles; $i++) { + File::factory() + ->forRecord($record->id) + ->pdf() + ->create(); + $pdfCount++; + } + + $totalFiles += $filesPerRecord; + } + } +} diff --git a/database/seeders/ModuleSeeder.php b/database/seeders/ModuleSeeder.php new file mode 100644 index 0000000..afe106c --- /dev/null +++ b/database/seeders/ModuleSeeder.php @@ -0,0 +1,72 @@ + 'Módulo Centro Villahermosa', + 'municipality' => 'Centro', + 'address' => 'Av. Paseo Tabasco 1203', + 'colony' => 'Tabasco 2000', + 'longitude' => -92.9376, + 'latitude' => 17.9892, + 'status' => true, + ], + [ + 'name' => 'Módulo Cárdenas', + 'municipality' => 'Cárdenas', + 'address' => 'Calle Benito Juárez No. 305', + 'colony' => 'Centro', + 'longitude' => -93.3808, + 'latitude' => 18.0011, + 'status' => true, + ], + [ + 'name' => 'Módulo Comalcalco', + 'municipality' => 'Comalcalco', + 'address' => 'Av. Gregorio Méndez Magaña', + 'colony' => 'Centro', + 'longitude' => -93.2042, + 'latitude' => 18.2667, + 'status' => true, + ], + [ + 'name' => 'Módulo Cunduacán', + 'municipality' => 'Cunduacán', + 'address' => 'Calle Carlos Pellicer Cámara', + 'colony' => 'Centro', + 'longitude' => -93.1608, + 'latitude' => 18.0667, + 'status' => true, + ], + [ + 'name' => 'Módulo Huimanguillo', + 'municipality' => 'Huimanguillo', + 'address' => 'Av. Constitución s/n', + 'colony' => 'Centro', + 'longitude' => -93.3908, + 'latitude' => 17.8422, + 'status' => true, + ], + ]; + + foreach ($modules as $module) { + Module::create($module); + } + + // Crear módulos adicionales con factory para pruebas + Module::factory(5)->create(); + + } +} diff --git a/database/seeders/OwnerSeeder.php b/database/seeders/OwnerSeeder.php new file mode 100644 index 0000000..526410f --- /dev/null +++ b/database/seeders/OwnerSeeder.php @@ -0,0 +1,18 @@ +create(); + } +} diff --git a/database/seeders/PackageSeeder.php b/database/seeders/PackageSeeder.php new file mode 100644 index 0000000..0b580fc --- /dev/null +++ b/database/seeders/PackageSeeder.php @@ -0,0 +1,31 @@ +forModule($module->id) + ->create(); + + $totalPackages += $packagesCount; + } + } +} diff --git a/database/seeders/RecordSeeder.php b/database/seeders/RecordSeeder.php new file mode 100644 index 0000000..97f9701 --- /dev/null +++ b/database/seeders/RecordSeeder.php @@ -0,0 +1,55 @@ +random(); + + // 10% de probabilidad de tener error + $hasError = rand(1, 100) <= 10; + + if ($hasError && $errors->isNotEmpty()) { + $randomError = $errors->random(); + Record::factory() + ->forVehicle($vehicle->id) + ->forUser($randomUser->id) + ->withSpecificError($randomError->id) + ->create(); + $recordsWithError++; + } else { + Record::factory() + ->forVehicle($vehicle->id) + ->forUser($randomUser->id) + ->withoutError() + ->create(); + $recordsWithoutError++; + } + } + } + + } +} diff --git a/database/seeders/TagSeeder.php b/database/seeders/TagSeeder.php new file mode 100644 index 0000000..a3af2a8 --- /dev/null +++ b/database/seeders/TagSeeder.php @@ -0,0 +1,93 @@ + 0, + 'assigned' => 0, + 'cancelled' => 0, + 'lost' => 0, + ]; + + // Vehículos que ya tienen un tag asignado + $vehiclesWithTag = []; + + // Crear tags para cada paquete + foreach ($packages as $package) { + // Cada paquete tiene entre 20-50 tags + $tagsPerPackage = rand(20, 50); + + // 40% disponibles + $availableCount = (int)($tagsPerPackage * 0.4); + Tag::factory($availableCount) + ->forPackage($package->id) + ->available() + ->create(); + $statusCounts['available'] += $availableCount; + + // 50% asignados (si hay vehículos disponibles sin tag) + $assignedCount = (int)($tagsPerPackage * 0.5); + if ($vehicles->isNotEmpty()) { + // Filtrar vehículos que NO tienen tag asignado + $availableVehicles = $vehicles->filter(function($vehicle) use ($vehiclesWithTag) { + return !in_array($vehicle->id, $vehiclesWithTag); + }); + + // Limitar la cantidad de tags asignados a los vehículos disponibles + $actualAssignedCount = min($assignedCount, $availableVehicles->count()); + + for ($i = 0; $i < $actualAssignedCount; $i++) { + $randomVehicle = $availableVehicles->random(); + Tag::factory() + ->forPackage($package->id) + ->forVehicle($randomVehicle->id) + ->create(); + + // Marcar el vehículo como que ya tiene tag + $vehiclesWithTag[] = $randomVehicle->id; + + // Remover del pool de vehículos disponibles + $availableVehicles = $availableVehicles->reject(function($vehicle) use ($randomVehicle) { + return $vehicle->id === $randomVehicle->id; + }); + } + $statusCounts['assigned'] += $actualAssignedCount; + } + + // 7% cancelados + $cancelledCount = (int)($tagsPerPackage * 0.07); + Tag::factory($cancelledCount) + ->forPackage($package->id) + ->cancelled() + ->create(); + $statusCounts['cancelled'] += $cancelledCount; + + // 3% perdidos + $lostCount = max(1, (int)($tagsPerPackage * 0.03)); + Tag::factory($lostCount) + ->forPackage($package->id) + ->lost() + ->create(); + $statusCounts['lost'] += $lostCount; + + $totalTags += $tagsPerPackage; + } + } +} diff --git a/database/seeders/VehicleSeeder.php b/database/seeders/VehicleSeeder.php new file mode 100644 index 0000000..e3cacfa --- /dev/null +++ b/database/seeders/VehicleSeeder.php @@ -0,0 +1,39 @@ += $vehiclesCount) { + break; + } + + $toCreate = min($vehiclesForOwner, $vehiclesCount - $vehiclesCreated); + + Vehicle::factory($toCreate) + ->forOwner($owner->id) + ->create(); + + $vehiclesCreated += $toCreate; + } + } +} diff --git a/resources/views/pdfs/constancia.blade.php b/resources/views/pdfs/constancia.blade.php new file mode 100644 index 0000000..1f3b3b1 --- /dev/null +++ b/resources/views/pdfs/constancia.blade.php @@ -0,0 +1,181 @@ + + + + + + Etiquetas Vehiculares + + + + +
+ +
+
+ +
+ {!! DNS1D::getBarcodeHTML($record->vehicle->numero_serie, 'C128', 2, 60) !!} +
+ {{ $record->vehicle->numero_serie }} +
+
+ + +
+
+
+
{{ $record->vehicle->placa }}
+
+
+
{{ strtoupper($record->vehicle->marca) }}
+
+
+
+
{{ strtoupper($record->vehicle->linea) }}
+
{{ $record->vehicle->modelo }}
+
+
+
{{ strtoupper($record->vehicle->tipo) }}
+
+
+
+ + +
+ {{ strtoupper($record->vehicle->owner->full_name) }} +
+ + +
+ {{ strtoupper($record->vehicle->owner->address) }} +
+
+
+ + +
+
+ +
+ {!! DNS1D::getBarcodeHTML($record->vehicle->numero_serie, 'C128', 2, 60) !!} +
+ {{ $record->vehicle->numero_serie }} +
+
+ + +
+
+
+
{{ $record->vehicle->placa }}
+
+
+
{{ strtoupper($record->vehicle->marca) }}
+
+
+
+
{{ strtoupper($record->vehicle->linea) }}
+
{{ $record->vehicle->modelo }}
+
+
+
{{ strtoupper($record->vehicle->tipo) }}
+
+
+
+ + +
+ {{ strtoupper($record->vehicle->owner->full_name) }} +
+ + +
+ {{ strtoupper($record->vehicle->owner->address) }} +
+
+
+
+ + + \ No newline at end of file diff --git a/resources/views/pdfs/verification.blade.php b/resources/views/pdfs/verification.blade.php new file mode 100644 index 0000000..9fb08bf --- /dev/null +++ b/resources/views/pdfs/verification.blade.php @@ -0,0 +1,360 @@ + + + + + Hoja de Verificación Vehicular + + + + +
+ +
+

HOJA DE VERIFICACION VEHICULAR

+
+ + +
+
+
+ Placa: {{ $record->vehicle->placa }} +
+
+ NIV: {{ $record->vehicle->numero_serie }} +
+
+ +
+
+ Marca: {{ strtoupper($record->vehicle->marca) }} +
+
+ Sub Marca: {{ strtoupper($record->vehicle->linea) }} +
+
+ +
+
+ Modelo: {{ $record->vehicle->modelo }} +
+
+ Tipo: {{ strtoupper($record->vehicle->tipo_servicio) }} +
+
+ +
+
+
+ Tipo Vehi: {{ strtoupper($record->vehicle->tipo) }} +
+
+
+ + +
+
+

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

+

OPERADOR

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ORIGINALCORRECTONUEVO
COLOR{{ strtoupper($record->vehicle->color) }}
PLACA{{ $record->vehicle->placa }}
VIN{{ $record->vehicle->numero_serie }}
MARCA{{ strtoupper($record->vehicle->marca) }}
SUBMARCA{{ strtoupper($record->vehicle->linea) }}
MODELO{{ $record->vehicle->modelo }}
TIPO{{ strtoupper($record->vehicle->tipo) }}
MOTOR{{ $record->vehicle->numero_motor ?? 'S/N' }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDDESCRIPCIÓNIDDESCRIPCIÓNOBSERVACIONES
1PLACA VIN TABLERO IZQ
2PARED DE FUEGO
3BASE DE AMORTIGUADOR DERECHO
4PISO ABAJO DEL ASIENTO DEL COPILOTO
5PISO DE CAJUELA
6ESTRIBO
7CHASIS DERECHO
8CHASIS IZQUIERDO
9BAZTIDOR IZQUIERDO
10BAZTIDOR DERECHO
11MARCO RADIADOR
12PISO CARROCERIA
13TABLERO DE PARTE MEDIA
14MOTOR
15TRANSMISION
+
+ + diff --git a/routes/api.php b/routes/api.php index 92e54a9..20374ec 100644 --- a/routes/api.php +++ b/routes/api.php @@ -26,14 +26,14 @@ // 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']); + Route::get('expediente/{id}/pdfVerificacion', [RecordController::class, 'generatePdfVerification']); + Route::get('expediente/{id}/pdfConstancia', [RecordController::class, 'generatePdfConstancia']); + + //Rutas de Actualización + Route::put('expediente/{recordId}', [RepuveController::class, 'actualizarVehiculo']); // 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 */