diff --git a/app/Http/Controllers/Repuve/CancellationController.php b/app/Http/Controllers/Repuve/CancellationController.php index 183fc47..ec4b665 100644 --- a/app/Http/Controllers/Repuve/CancellationController.php +++ b/app/Http/Controllers/Repuve/CancellationController.php @@ -4,9 +4,12 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Repuve\CancelConstanciaRequest; +use App\Models\CatalogCancellationReason; use App\Models\Record; use App\Models\Tag; +use App\Models\TagCancellationLog; use App\Models\VehicleTagLog; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -59,7 +62,7 @@ public function cancelarConstancia(CancelConstanciaRequest $request) 'vehicle_id' => $vehicle->id, 'tag_id' => $tag->id, 'action_type' => 'cancelacion', - 'cancellation_reason' => $request->cancellation_reason, + 'cancellation_reason_id' => $request->cancellation_reason_id, 'cancellation_observations' => $request->cancellation_observations, 'cancellation_at' => now(), 'cancelled_by' => Auth::id(), @@ -100,7 +103,7 @@ public function cancelarConstancia(CancelConstanciaRequest $request) 'vehicle_id' => $vehicle->id, 'tag_id' => $newTag->id, 'action_type' => 'sustitucion', - 'cancellation_reason' => $request->cancellation_reason, + 'cancellation_reason_id' => $request->cancellation_reason_id, 'cancellation_observations' => 'Tag sustituido. Tag anterior: ' . $oldTagNumber . ' (Folio: ' . $oldFolio . '). Motivo: ' . ($request->cancellation_observations ?? ''), 'performed_by' => Auth::id(), ]); @@ -136,7 +139,7 @@ public function cancelarConstancia(CancelConstanciaRequest $request) 'tag_number' => $newTag->tag_number, 'status' => $newTag->status->name, ] : null, - 'cancellation_reason' => $request->cancellation_reason, + 'cancellation_reason_id' => $cancellationLog->cancellationReason->name, 'cancellation_observations' => $request->cancellation_observations, 'cancelled_at' => $cancellationLog->cancellation_at->toDateTimeString(), 'cancelled_by' => Auth::user()->name, @@ -157,4 +160,74 @@ public function cancelarConstancia(CancelConstanciaRequest $request) ]); } } + + public function cancelarTagNoAsignado(Request $request) +{ + try { + $request->validate([ + 'tag_number' => 'required|string|exists:tags,tag_number', + 'cancellation_reason_id' => 'required|exists:catalog_cancellation_reasons,id', + 'cancellation_observations' => 'nullable|string', + ]); + + DB::beginTransaction(); + + $tag = Tag::where('tag_number', $request->tag_number)->first(); + + // Validar que el tag NO esté asignado + if ($tag->isAssigned()) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Este tag está asignado a un vehículo. Usa el endpoint de cancelación de constancia.', + 'tag_status' => $tag->status->name, + ]); + } + + // Validar que no esté ya cancelado + if ($tag->isCancelled()) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'El tag ya está cancelado.', + ]); + } + + // Crear log de cancelación ANTES de cancelar el tag + $cancellationLog = TagCancellationLog::create([ + 'tag_id' => $tag->id, + 'cancellation_reason_id' => $request->cancellation_reason_id, + 'cancellation_observations' => $request->cancellation_observations, + 'cancellation_at' => now(), + 'cancelled_by' => Auth::id(), + ]); + + // Cancelar el tag + $tag->markAsCancelled(); + + DB::commit(); + + return ApiResponse::OK->response([ + 'message' => 'Tag cancelado exitosamente', + 'tag' => [ + 'id' => $tag->id, + 'tag_number' => $tag->tag_number, + 'folio' => $tag->folio, + 'previous_status' => 'Disponible', + 'new_status' => 'Cancelado', + ], + 'cancellation' => [ + 'id' => $cancellationLog->id, + 'reason' => $cancellationLog->cancellationReason->name, + 'observations' => $cancellationLog->cancellation_observations, + 'cancelled_at' => $cancellationLog->cancellation_at->toDateTimeString(), + 'cancelled_by' => Auth::user()->name, + ], + ]); + + } catch (\Exception $e) { + DB::rollBack(); + + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Error al cancelar el tag', + 'error' => $e->getMessage(), + ]); + } +} } diff --git a/app/Http/Controllers/Repuve/CatalogController.php b/app/Http/Controllers/Repuve/CatalogController.php new file mode 100644 index 0000000..81819f9 --- /dev/null +++ b/app/Http/Controllers/Repuve/CatalogController.php @@ -0,0 +1,115 @@ +query('type'); + + $query = CatalogCancellationReason::query(); + + if ($type === 'cancelacion') { + $query->forCancellation(); + } elseif ($type === 'sustitucion') { + $query->forSubstitution(); + } else { + $query->orderBy('id'); + } + + $reasons = $query->get(['id', 'code', 'name', 'description', 'applies_to']); + + return ApiResponse::OK->response([ + 'message' => 'Razones de cancelación obtenidas exitosamente', + 'data' => $reasons, + ]); + } + + public function show($id) + { + $reason = CatalogCancellationReason::find($id); + + if (!$reason) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'Razón de cancelación no encontrada', + ]); + } + + return ApiResponse::OK->response([ + 'message' => 'Razón de cancelación obtenida exitosamente', + 'data' => $reason, + ]); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'code' => 'required|string|unique:catalog_cancellation_reasons,code', + 'name' => 'required|string', + 'description' => 'nullable|string', + 'applies_to' => 'required|in:cancelacion,sustitucion,ambos', + ]); + + $reason = CatalogCancellationReason::create($validated); + + return ApiResponse::CREATED->response([ + 'message' => 'Razón de cancelación creada exitosamente', + 'data' => $reason, + ]); + } + + public function update(Request $request, $id) + { + $reason = CatalogCancellationReason::find($id); + + if (!$reason) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'Razón de cancelación no encontrada', + ]); + } + + $validated = $request->validate([ + 'name' => 'required|string', + 'description' => 'nullable|string', + 'applies_to' => 'required|in:cancelacion,sustitucion,ambos', + ]); + + $reason->update($validated); + + return ApiResponse::OK->response([ + 'message' => 'Razón de cancelación actualizada exitosamente', + 'data' => $reason, + ]); + } + + public function destroy($id) + { + $reason = CatalogCancellationReason::find($id); + + if (!$reason) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'Razón de cancelación no encontrada', + ]); + } + + $reason->delete(); + + return ApiResponse::OK->response([ + 'message' => 'Razón de cancelación eliminada exitosamente', + ]); + } +} diff --git a/app/Http/Controllers/Repuve/ExcelController.php b/app/Http/Controllers/Repuve/ExcelController.php index da25b10..0201bcc 100644 --- a/app/Http/Controllers/Repuve/ExcelController.php +++ b/app/Http/Controllers/Repuve/ExcelController.php @@ -45,6 +45,7 @@ public function constanciasSustituidas(Request $request) $logs = VehicleTagLog::with([ 'vehicle', 'tag', + 'cancellationReason' ]) ->where('action_type', 'sustitucion') ->whereHas('vehicle.records', function ($query) use ($moduleId) { diff --git a/app/Http/Controllers/Repuve/RecordController.php b/app/Http/Controllers/Repuve/RecordController.php index 094179a..7213e64 100644 --- a/app/Http/Controllers/Repuve/RecordController.php +++ b/app/Http/Controllers/Repuve/RecordController.php @@ -5,6 +5,8 @@ use App\Http\Controllers\Controller; use Barryvdh\DomPDF\Facade\Pdf; use App\Models\Record; +use App\Models\Tag; +use Carbon\Carbon; use Illuminate\Support\Facades\Storage; use Notsoweb\ApiResponse\Enums\ApiResponse; use Codedge\Fpdf\Fpdf\Fpdf; @@ -182,9 +184,6 @@ public function generatePdfImages($id) public function generatePdfForm(Request $request) { $request->validate([ - 'fecha' => 'nullable|string', - 'mes' => 'nullable|string', - 'anio' => 'nullable|string', 'marca' => 'required|string', 'linea' => 'required|string', 'modelo' => 'required|string', @@ -195,21 +194,21 @@ public function generatePdfForm(Request $request) 'telefono' => 'nullable|string', ]); + $now = Carbon::now()->locale('es_MX'); + $data = [ - 'fecha' => $request->input('fecha', ''), - 'mes' => $request->input('mes', ''), - 'anio' => $request->input('anio', ''), - 'marca' => $request->input('marca'), - 'linea' => $request->input('linea'), 'modelo' => $request->input('modelo'), 'niv' => $request->input('niv'), 'numero_motor' => $request->input('numero_motor'), 'placa' => $request->input('placa'), 'folio' => $request->input('folio'), 'telefono' => $request->input('telefono', ''), + 'fecha' => $now->format('d'), + 'mes' => ucfirst($now->translatedFormat('F')), + 'anio' => $now->format('Y'), ]; - $pdf = Pdf::loadView('pdfs.form', $data) + $pdf = Pdf::loadView('pdfs.tag', $data) ->setPaper('a4', 'portrait') ->setOptions([ 'defaultFont' => 'sans-serif', @@ -220,6 +219,128 @@ public function generatePdfForm(Request $request) return $pdf->stream('solicitud-sustitucion-' . time() . '.pdf'); } + public function pdfCancelledTag(Tag $tag) + { + try { + // Validar que el tag esté cancelado + if (!$tag->isCancelled()) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Solo se puede generar PDF para tags cancelados.', + 'current_status' => $tag->status->name, + ]); + } + + // Obtener datos de cancelación + $cancellationData = $this->cancellationData($tag); + + $pdf = Pdf::loadView('pdfs.tag', [ + 'cancellation' => $cancellationData, + ]) + ->setPaper('a4', 'portrait') + ->setOptions([ + 'defaultFont' => 'sans-serif', + 'isHtml5ParserEnabled' => true, + 'isRemoteEnabled' => true, + ]); + + return $pdf->stream('constancia_cancelada_' . $tag->tag_number . '.pdf'); + } catch (\Exception $e) { + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al generar el PDF.', + 'error' => $e->getMessage(), + ]); + } + } + + private function cancellationData(Tag $tag) + { + $data = [ + 'fecha' => now()->format('d/m/Y'), + 'folio' => $tag->folio ?? '', + 'id_chip' => '', + 'placa' => '', + 'niv' => '', + 'motivo' => 'N/A', + 'operador' => 'N/A', + 'modulo' => '', + 'ubicacion' => '', + ]; + + // Intentar obtener datos del vehículo si existe + if ($tag->vehicle_id && $tag->vehicle) { + $data['id_chip'] = $tag->vehicle->id_chip ?? ''; + $data['placa'] = $tag->vehicle->placa ?? ''; + $data['niv'] = $tag->vehicle->niv ?? ''; + } + + // Buscar log de cancelación directa + $tagCancellationLog = $tag->cancellationLogs() + ->with(['cancellationReason', 'cancelledBy']) + ->latest() + ->first(); + + if ($tagCancellationLog) { + $data['motivo'] = $tagCancellationLog->cancellationReason->name ?? 'No especificado'; + $data['operador'] = $tagCancellationLog->cancelledBy->name ?? 'Sistema'; + + // Cargar módulo del cual el usuario es responsable + if ($tagCancellationLog->cancelledBy) { + $user = $tagCancellationLog->cancelledBy; + $this->loadUserModule($user, $data); + } + + return $data; + } + + // Buscar log de vehículo (tag asignado y luego cancelado) + $vehicleTagLog = $tag->vehicleTagLogs() + ->where('action_type', 'cancelacion') + ->with(['cancellationReason', 'cancelledBy', 'vehicle']) + ->latest() + ->first(); + + if ($vehicleTagLog) { + $data['motivo'] = $vehicleTagLog->cancellationReason->name ?? 'No especificado'; + $data['operador'] = $vehicleTagLog->cancelledBy->name ?? 'Sistema'; + + // Cargar módulo del cual el usuario es responsable + if ($vehicleTagLog->cancelledBy) { + $user = $vehicleTagLog->cancelledBy; + $this->loadUserModule($user, $data); + } + + if ($vehicleTagLog->vehicle) { + $data['id_chip'] = $vehicleTagLog->vehicle->id_chip ?? ''; + $data['placa'] = $vehicleTagLog->vehicle->placa ?? ''; + $data['niv'] = $vehicleTagLog->vehicle->niv ?? ''; + } + } + + return $data; + } + + /** + * Cargar módulo del usuario + */ + private function loadUserModule($user, &$data) + { + // Intentar cargar module + $user->load('module'); + + // Si no tiene module, usar responsibleModule + if (!$user->module) { + $user->load('responsibleModule'); + + if ($user->responsibleModule) { + $data['modulo'] = $user->responsibleModule->name; + $data['ubicacion'] = $user->responsibleModule->address; + } + } else { + $data['modulo'] = $user->module->name; + $data['ubicacion'] = $user->module->address; + } + } + public function errors(Request $request) { $request->validate([ diff --git a/app/Http/Controllers/Repuve/TagsController.php b/app/Http/Controllers/Repuve/TagsController.php index 954935c..cf3208d 100644 --- a/app/Http/Controllers/Repuve/TagsController.php +++ b/app/Http/Controllers/Repuve/TagsController.php @@ -9,10 +9,16 @@ class TagsController extends Controller { - public function index() + public function index(Request $request) { try { - $tags = Tag::with('vehicle:id,placa,niv', 'package:id,lot,box_number')->orderBy('id', 'ASC'); + $tags = Tag::with('vehicle:id,placa,niv', 'package:id,lot,box_number', 'status:id,name')->orderBy('id', 'ASC'); + + if ($request->has('status')) { + $tags->whereHas('status', function ($q) use ($request) { + $q->where('name', $request->status); + }); + } return ApiResponse::OK->response([ 'tag' => $tags->paginate(config('app.pagination')), diff --git a/app/Http/Controllers/Repuve/UpdateController.php b/app/Http/Controllers/Repuve/UpdateController.php index ebd1367..ee2a126 100644 --- a/app/Http/Controllers/Repuve/UpdateController.php +++ b/app/Http/Controllers/Repuve/UpdateController.php @@ -161,7 +161,7 @@ public function vehicleUpdate(VehicleUpdateRequest $request) if (!$tag) { return ApiResponse::NOT_FOUND->response([ - 'message' => 'No se encontró el tag con el tag_number proporcionado', + 'message' => 'El TAG no coincide con el registrado en el expediente', 'tag_number' => $tagNumber, ]); } @@ -174,74 +174,126 @@ public function vehicleUpdate(VehicleUpdateRequest $request) DB::beginTransaction(); - // Intentar obtener datos del caché primero - $vehicleData = Cache::get("update_vehicle_{$niv}"); - $ownerData = Cache::get("update_owner_{$niv}"); - - // Si no hay - if (!$vehicleData || !$ownerData) { - $vehicleData = $this->getVehicle($niv); - $ownerData = $this->getOwner($niv); - } - - // Limpiar caché - Cache::forget("update_vehicle_{$niv}"); - Cache::forget("update_owner_{$niv}"); - - // Detectar si hay cambios - $hasVehicleChanges = $this->detectVehicleChanges($vehicle, $vehicleData); - $hasOwnerChanges = $this->detectOwnerChanges($vehicle->owner, $ownerData); - - // Actualizar propietario solo si hay cambios + $isManualUpdate = $request->has('vehicle') || $request->has('owner'); + $hasVehicleChanges = false; + $hasOwnerChanges = false; $owner = $vehicle->owner; - if ($hasOwnerChanges) { - $owner = Owner::updateOrCreate( - ['rfc' => $ownerData['rfc']], - [ - 'name' => $ownerData['name'], - 'paternal' => $ownerData['paternal'], - 'maternal' => $ownerData['maternal'], - 'curp' => $ownerData['curp'], - 'address' => $ownerData['address'], - 'tipopers' => $ownerData['tipopers'], - 'pasaporte' => $ownerData['pasaporte'], - 'licencia' => $ownerData['licencia'], - 'ent_fed' => $ownerData['ent_fed'], - 'munic' => $ownerData['munic'], - 'callep' => $ownerData['callep'], - 'num_ext' => $ownerData['num_ext'], - 'num_int' => $ownerData['num_int'], - 'colonia' => $ownerData['colonia'], - 'cp' => $ownerData['cp'], - ] - ); + + if ($isManualUpdate) { + if ($request->has('vehicle')) { + $vehicleData = $request->input('vehicle', []); + + $allowedVehicleFields = [ + 'placa', + 'marca', + 'linea', + 'sublinea', + 'modelo', + 'color', + 'numero_motor', + 'clase_veh', + 'tipo_servicio', + 'rfv', + 'ofcexpedicion', + 'fechaexpedicion', + 'tipo_veh', + 'numptas', + 'observac', + 'cve_vehi', + 'nrpv', + 'tipo_mov' + ]; + + $vehicleData = array_filter( + array_intersect_key($vehicleData, array_flip($allowedVehicleFields)), + fn($value) => $value !== null && $value !== '' + ); + + if (!empty($vehicleData)) { + $vehicle->update($vehicleData); + $hasVehicleChanges = true; + } + } + + if ($request->has('owner')) { + $ownerInput = $request->input('owner', []); + + $allowedOwnerFields = [ + 'name', + 'paternal', + 'maternal', + 'rfc', + 'curp', + 'address', + 'tipopers', + 'pasaporte', + 'licencia', + 'ent_fed', + 'munic', + 'callep', + 'num_ext', + 'num_int', + 'colonia', + 'cp', + 'telefono' + ]; + + $ownerData = array_filter( + array_intersect_key($ownerInput, array_flip($allowedOwnerFields)), + fn($value) => $value !== null && $value !== '' + ); + + if (!empty($ownerData)) { + // Si se cambió el RFC, buscar/crear otro propietario + if (isset($ownerData['rfc']) && $ownerData['rfc'] !== $owner->rfc) { + $owner = Owner::updateOrCreate( + ['rfc' => $ownerData['rfc']], + $ownerData + ); + $vehicle->update(['owner_id' => $owner->id]); + } else { + // Actualizar propietario existente + $owner->update($ownerData); + } + $hasOwnerChanges = true; + } + } + } else { + + // Intentar obtener datos del caché primero + $vehicleDataEstatal = Cache::get("update_vehicle_{$niv}"); + $ownerDataEstatal = Cache::get("update_owner_{$niv}"); + + // Si no hay + if (!$vehicleDataEstatal || !$ownerDataEstatal) { + $vehicleDataEstatal = $this->getVehicle($niv); + $ownerDataEstatal = $this->getOwner($niv); + } + + // Limpiar caché + Cache::forget("update_vehicle_{$niv}"); + Cache::forget("update_owner_{$niv}"); + + // Detectar si hay cambios + $hasVehicleChanges = $this->detectVehicleChanges($vehicle, $vehicleDataEstatal); + $hasOwnerChanges = $this->detectOwnerChanges($vehicle->owner, $ownerDataEstatal); + + // Actualizar vehículo solo si hay cambios + if ($hasVehicleChanges) { + $vehicle->update($vehicleDataEstatal); + } + + // Actualizar propietario solo si hay cambios + $owner = $vehicle->owner; + if ($hasOwnerChanges) { + $owner = Owner::updateOrCreate( + ['rfc' => $ownerDataEstatal['rfc']], + $ownerDataEstatal + ); + $vehicle->update(['owner_id' => $owner->id]); + } } - // Actualizar vehículo solo si hay cambios - if ($hasVehicleChanges) { - $vehicle->update([ - 'placa' => $vehicleData['placa'], - 'marca' => $vehicleData['marca'], - 'linea' => $vehicleData['linea'], - 'sublinea' => $vehicleData['sublinea'], - 'modelo' => $vehicleData['modelo'], - 'color' => $vehicleData['color'], - 'numero_motor' => $vehicleData['numero_motor'], - 'clase_veh' => $vehicleData['clase_veh'], - 'tipo_servicio' => $vehicleData['tipo_servicio'], - 'rfv' => $vehicleData['rfv'], - 'rfc' => $vehicleData['rfc'], - 'ofcexpedicion' => $vehicleData['ofcexpedicion'], - 'fechaexpedicion' => $vehicleData['fechaexpedicion'], - 'tipo_veh' => $vehicleData['tipo_veh'], - 'numptas' => $vehicleData['numptas'], - 'observac' => $vehicleData['observac'], - 'cve_vehi' => $vehicleData['cve_vehi'], - 'nrpv' => $vehicleData['nrpv'], - 'tipo_mov' => $vehicleData['tipo_mov'], - 'owner_id' => $owner->id, - ]); - } $uploadedFiles = []; $replacedFiles = []; @@ -268,7 +320,7 @@ public function vehicleUpdate(VehicleUpdateRequest $request) if ($nameId === null) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ - 'message' => "Falta el name_id para el archivo", + 'message' => "Falta el nombre para el archivo, busca en el catálogo de nombres", ]); } @@ -317,7 +369,7 @@ public function vehicleUpdate(VehicleUpdateRequest $request) 'vehicle_id' => $vehicle->id, 'tag_id' => $tag->id, 'action_type' => 'actualizacion', - 'performed_by' => Auth::id(), + 'performed_by' => Auth::id() ]); } @@ -510,7 +562,6 @@ private function detectVehicleChanges($vehicle, array $vehicleDataEstatal): bool 'clase_veh', 'tipo_servicio', 'rfv', - 'rfc', 'ofcexpedicion', 'tipo_veh', 'numptas', diff --git a/app/Http/Requests/Repuve/CancelConstanciaRequest.php b/app/Http/Requests/Repuve/CancelConstanciaRequest.php index fcebc3d..43446f8 100644 --- a/app/Http/Requests/Repuve/CancelConstanciaRequest.php +++ b/app/Http/Requests/Repuve/CancelConstanciaRequest.php @@ -20,9 +20,9 @@ public function authorize(): bool public function rules(): array { return [ - 'record_id' => 'required|exists:records,id', + 'record_id' => 'nullable|exists:records,id', 'folio' => 'required|string', - 'cancellation_reason' => 'required|in:fallo_lectura_handheld,cambio_parabrisas,roto_al_pegarlo,extravio,otro', + 'cancellation_reason_id' => 'required|exists:catalog_cancellation_reasons,id', 'cancellation_observations' => 'nullable|string', 'new_tag_number' => 'nullable|exists:tags,tag_number', ]; @@ -38,11 +38,11 @@ public function messages(): array '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.', + 'cancellation_reason_id.required' => 'El motivo de cancelación es obligatorio.', + 'cancellation_reason_id.exists' => 'El motivo de cancelación no es válido.', - 'cancellation_observations.string' => 'Las observaciones deben ser texto.', - 'cancellation_observations.max' => 'Las observaciones no pueden exceder 1000 caracteres.', + 'new_tag_number.exists' => 'El nuevo tag no existe', + 'folio.required_with' => 'El folio es requerido cuando se proporciona un nuevo tag', ]; } diff --git a/app/Http/Requests/Repuve/VehicleUpdateRequest.php b/app/Http/Requests/Repuve/VehicleUpdateRequest.php index 41b1346..9d4ecb0 100644 --- a/app/Http/Requests/Repuve/VehicleUpdateRequest.php +++ b/app/Http/Requests/Repuve/VehicleUpdateRequest.php @@ -1,4 +1,6 @@ - ['nullable', 'array', 'min:1'], - 'files.*' => ['file', 'mimes:jpeg,png,jpg,pdf', 'max:10240'], - 'names' => ['nullable', 'array'], - 'names.*' => ['string', 'max:255'], + // --- DATOS DEL VEHÍCULO --- + 'vehicle.placa' => 'nullable|string|max:20', + 'vehicle.marca' => 'nullable|string|max:100', + 'vehicle.linea' => 'nullable|string|max:100', + 'vehicle.sublinea' => 'nullable|string|max:100', + 'vehicle.modelo' => 'nullable|string', + 'vehicle.color' => 'nullable|string|max:50', + 'vehicle.numero_motor' => 'nullable|string|max:50', + 'vehicle.clase_veh' => 'nullable|string|max:50', + 'vehicle.tipo_servicio' => 'nullable|string|max:50', + 'vehicle.rfv' => 'nullable|string|max:50', + 'vehicle.rfc' => 'nullable|string|max:13', + 'vehicle.ofcexpedicion' => 'nullable|string|max:100', + 'vehicle.fechaexpedicion' => 'nullable|date', + 'vehicle.tipo_veh' => 'nullable|string|max:50', + 'vehicle.numptas' => 'nullable|string', + 'vehicle.observac' => 'nullable|string|max:500', + 'vehicle.cve_vehi' => 'nullable|string|max:50', + 'vehicle.nrpv' => 'nullable|string|max:50', + 'vehicle.tipo_mov' => 'nullable|string|max:50', + + // --- DATOS DEL PROPIETARIO --- + 'owner.name' => 'nullable|string|max:100', + 'owner.paternal' => 'nullable|string|max:100', + 'owner.maternal' => 'nullable|string|max:100', + 'owner.rfc' => 'nullable|string|max:13', + 'owner.curp' => 'nullable|string|max:18', + 'owner.address' => 'nullable|string|max:255', + 'owner.tipopers' => 'nullable|boolean', + 'owner.pasaporte' => 'nullable|string|max:20', + 'owner.licencia' => 'nullable|string|max:20', + 'owner.ent_fed' => 'nullable|string|max:50', + 'owner.munic' => 'nullable|string|max:100', + 'owner.callep' => 'nullable|string|max:100', + 'owner.num_ext' => 'nullable|string|max:10', + 'owner.num_int' => 'nullable|string|max:10', + 'owner.colonia' => 'nullable|string|max:100', + 'owner.cp' => 'nullable|string|max:5', + 'owner.telefono' => 'nullable|string|max:15', + + // --- ARCHIVOS --- + 'files' => 'nullable|array|min:1', + 'files.*' => 'file|mimes:jpeg,png,jpg|max:2048', + 'name_id' => 'nullable|array', + 'name_id.*' => 'integer|exists:catalog_name_img,id', ]; } public function messages(): array { return [ + 'vehicle.modelo.string' => 'El modelo debe ser texto', + 'vehicle.numptas.string' => 'El número de puertas debe ser texto', + 'owner.tipopers.boolean' => 'El tipo de persona debe ser física o Moral', + 'owner.cp.max' => 'El código postal debe tener máximo 5 caracteres', 'files.*.mimes' => 'Solo se permiten archivos JPG, PNG o JPEG', - 'files.*.max' => 'El archivo no debe superar 3MB', + 'files.*.max' => 'El archivo no debe superar 2MB', ]; } } diff --git a/app/Models/CatalogCancellationReason.php b/app/Models/CatalogCancellationReason.php new file mode 100644 index 0000000..4d9caac --- /dev/null +++ b/app/Models/CatalogCancellationReason.php @@ -0,0 +1,42 @@ +whereIn('applies_to', ['cancelacion', 'ambos']); + } + + /** + * Obtener razones para sustitución + */ + public function scopeForSubstitution($query) + { + return $query->whereIn('applies_to', ['sustitucion', 'ambos']); + } + + /** + * Logs que usan esta razón + */ + public function vehicleTagLogs() + { + return $this->hasMany(VehicleTagLog::class, 'cancellation_reason_id'); + } +} diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 7bc97f2..08e502f 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -48,6 +48,11 @@ public function scanHistories() return $this->hasMany(ScanHistory::class); } + public function cancellationLogs() + { + return $this->hasMany(TagCancellationLog::class); + } + /** * Marcar tag como asignado a un vehículo */ diff --git a/app/Models/TagCancellationLog.php b/app/Models/TagCancellationLog.php new file mode 100644 index 0000000..9262b32 --- /dev/null +++ b/app/Models/TagCancellationLog.php @@ -0,0 +1,51 @@ + + * + * @version 1.0.0 + */ +class TagCancellationLog extends Model +{ + use HasFactory; + + protected $table = 'tag_cancellation_logs'; + + protected $fillable = [ + 'tag_id', + 'cancellation_reason_id', + 'cancellation_observations', + 'cancellation_at', + 'cancelled_by', + ]; + + protected function casts(): array + { + return [ + 'cancellation_at' => 'datetime', + ]; + } + + // Relaciones + public function tag() + { + return $this->belongsTo(Tag::class); + } + + public function cancellationReason() + { + return $this->belongsTo(CatalogCancellationReason::class, 'cancellation_reason_id'); + } + + public function cancelledBy() + { + return $this->belongsTo(User::class, 'cancelled_by'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 3653914..d8cbed0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -133,4 +133,12 @@ public function module() { return $this->belongsTo(Module::class); } + + /** + * Módulo del cual el usuario es responsable + */ + public function responsibleModule() + { + return $this->hasOne(Module::class, 'responsible_id'); + } } diff --git a/app/Models/VehicleTagLog.php b/app/Models/VehicleTagLog.php index ca6f073..db321f7 100644 --- a/app/Models/VehicleTagLog.php +++ b/app/Models/VehicleTagLog.php @@ -15,7 +15,7 @@ class VehicleTagLog extends Model 'vehicle_id', 'tag_id', 'action_type', - 'cancellation_reason', + 'cancellation_reason_id', 'cancellation_observations', 'cancellation_at', 'cancelled_by', @@ -41,6 +41,11 @@ public function cancelledBy() { return $this->belongsTo(User::class, 'cancelled_by'); } + public function cancellationReason() + { + return $this->belongsTo(CatalogCancellationReason::class, 'cancellation_reason_id'); + } + public function isInscription() { return $this->action_type === 'inscripcion'; diff --git a/database/migrations/2025_11_27_102628_create_catalog_cancellation_reasons_table.php b/database/migrations/2025_11_27_102628_create_catalog_cancellation_reasons_table.php new file mode 100644 index 0000000..145375c --- /dev/null +++ b/database/migrations/2025_11_27_102628_create_catalog_cancellation_reasons_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('code')->unique(); + $table->string('name'); + $table->text('description')->nullable(); + $table->enum('applies_to', ['cancelacion', 'sustitucion', 'ambos']); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('catalog_cancellation_reasons'); + } +}; diff --git a/database/migrations/2025_11_27_102818_modify_vehicle_tags_logs_add_reason_id.php b/database/migrations/2025_11_27_102818_modify_vehicle_tags_logs_add_reason_id.php new file mode 100644 index 0000000..333e037 --- /dev/null +++ b/database/migrations/2025_11_27_102818_modify_vehicle_tags_logs_add_reason_id.php @@ -0,0 +1,46 @@ +foreignId('cancellation_reason_id') + ->nullable() + ->after('tag_id') + ->constrained('catalog_cancellation_reasons') + ->nullOnDelete(); + + // Eliminar el enum viejo + $table->dropColumn('cancellation_reason'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('vehicle_tags_logs', function (Blueprint $table) { + $table->dropForeign(['cancellation_reason_id']); + $table->dropColumn('cancellation_reason_id'); + + // Restaurar enum (solo si es necesario hacer rollback) + $table->enum('cancellation_reason', [ + 'fallo_lectura_handheld', + 'cambio_parabrisas', + 'roto_al_pegarlo', + 'extravio', + 'otro' + ])->nullable(); + }); + } +}; diff --git a/database/migrations/2025_11_27_140245_create_tag_cancellation_logs_table.php b/database/migrations/2025_11_27_140245_create_tag_cancellation_logs_table.php new file mode 100644 index 0000000..9bfe4e3 --- /dev/null +++ b/database/migrations/2025_11_27_140245_create_tag_cancellation_logs_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete(); + $table->foreignId('cancellation_reason_id')->nullable()->constrained('catalog_cancellation_reasons')->nullOnDelete(); + $table->text('cancellation_observations')->nullable(); + $table->timestamp('cancellation_at'); + $table->foreignId('cancelled_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tag_cancellation_logs'); + } +}; diff --git a/database/seeders/CatalogCancellationReasonSeeder.php b/database/seeders/CatalogCancellationReasonSeeder.php new file mode 100644 index 0000000..7c7e4d1 --- /dev/null +++ b/database/seeders/CatalogCancellationReasonSeeder.php @@ -0,0 +1,70 @@ + + * + * @version 1.0.0 + */ +class CatalogCancellationReasonSeeder extends Seeder +{ + /** + * Ejecutar sembrado de base de datos + */ + public function run(): void + { + $reasons = [ + [ + 'code' => '01', + 'name' => 'Fallo de lectura en handheld', + 'description' => 'El dispositivo handheld no puede leer el TAG correctamente', + 'applies_to' => 'ambos', + ], + [ + 'code' => '02', + 'name' => 'Cambio de parabrisas', + 'description' => 'El vehículo requirió cambio de parabrisas', + 'applies_to' => 'sustitucion', + ], + [ + 'code' => '03', + 'name' => 'TAG roto al pegarlo', + 'description' => 'El TAG se dañó durante la instalación', + 'applies_to' => 'sustitucion', + ], + [ + 'code' => '04', + 'name' => 'Extravío del TAG', + 'description' => 'El TAG fue extraviado o perdido', + 'applies_to' => 'ambos', + ], + [ + 'code' => '05', + 'name' => 'Daño físico del TAG', + 'description' => 'El TAG presenta daño físico que impide su funcionamiento', + 'applies_to' => 'ambos', + ], + [ + 'code' => '06', + 'name' => 'Otro motivo', + 'description' => 'Motivo no especificado en las opciones anteriores', + 'applies_to' => 'ambos', + ], + ]; + + foreach ($reasons as $reason) { + CatalogCancellationReason::updateOrCreate( + ['code' => $reason['code']], + $reason + ); + } + } +} diff --git a/database/seeders/DevSeeder.php b/database/seeders/DevSeeder.php index 8dab9ad..083cf61 100644 --- a/database/seeders/DevSeeder.php +++ b/database/seeders/DevSeeder.php @@ -27,5 +27,6 @@ public function run(): void $this->call(MunicipalitySeeder::class); $this->call(ModuleSeeder::class); $this->call(CatalogTagStatusSeeder::class); + $this->call(CatalogCancellationReasonSeeder::class); } } diff --git a/resources/views/pdfs/form.blade.php b/resources/views/pdfs/form.blade.php index 6176c6f..72005ad 100644 --- a/resources/views/pdfs/form.blade.php +++ b/resources/views/pdfs/form.blade.php @@ -200,8 +200,7 @@
- {{ $fecha ?? '' }} de - {{ $mes ?? '' }} del 20{{ $anio ?? '' }} + {{ $fecha }} de {{ $mes }} del {{ $anio }}
diff --git a/resources/views/pdfs/tag.blade.php b/resources/views/pdfs/tag.blade.php new file mode 100644 index 0000000..f640ce2 --- /dev/null +++ b/resources/views/pdfs/tag.blade.php @@ -0,0 +1,152 @@ + + + + + Constancia Cancelada - REPUVE Tabasco + + + +
+ +
+ REPUVE TABASCO +
+ + +
+ CONSTANCIA DAÑADA +
+ + +
+
+ PEGAR CONSTANCIA +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FECHA:{{ $cancellation['fecha'] }}
FOLIO:{{ $cancellation['folio'] ?? '' }}
ID CHIP:{{ $cancellation['id_chip'] ?? '' }}
PLACAS:{{ $cancellation['placa'] ?? '' }}
VIN:{{ $cancellation['niv'] ?? '' }}
MOTIVO:{{ strtoupper($cancellation['motivo'] ?? '') }}
OPERADOR:{{ strtoupper($cancellation['operador'] ?? '') }}
MÓDULO:{{ $cancellation['modulo']}}
UBICACIÓN:{{ $cancellation['ubicacion'] }}
+
+ + diff --git a/routes/api.php b/routes/api.php index 08ea7c2..174f714 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@