findOrFail($id); $pdf = Pdf::loadView('pdfs.record', compact('record')) ->setPaper('a4', 'portrait') ->setOptions([ 'defaultFont' => 'sans-serif', 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => true, ]); return $pdf->stream('constancia-inscripcion-' . $id . '.pdf'); } public function generatePdfVerification($id) { $record = Record::with('vehicle.owner', 'user')->findOrFail($id); $pdf = Pdf::loadView('pdfs.verification', compact('record')) ->setPaper('a4', 'landscape') ->setOptions([ 'defaultFont' => 'sans-serif', 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => true, ]); return $pdf->stream('hoja-verificacion-' . $id . '.pdf'); } public function generatePdfConstancia($id) { $record = Record::with('vehicle.owner.municipality', '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'); } /** * Generar PDF con las imágenes */ public function generatePdfImages($id) { try { // Obtener el record con sus archivos $record = Record::with(['vehicle.owner', 'files'])->findOrFail($id); // Validar que tenga archivos if ($record->files->isEmpty()) { return ApiResponse::NOT_FOUND->response([ 'message' => 'El expediente no tiene imágenes adjuntas.', 'record_id' => $id, ]); } // Crear instancia de FPDF $pdf = new Fpdf('P', 'mm', 'A4'); $pdf->SetAutoPageBreak(false); $pdf->SetMargins(10, 10, 10); $currentImage = 0; foreach ($record->files as $file) { $currentImage++; // Buscar archivo en disk 'records' $diskRecords = Storage::disk('records'); $fileContent = null; if ($diskRecords->exists($file->path)) { $fileContent = $diskRecords->get($file->path); } // Si no se encontró el archivo, continuar if ($fileContent === null) { continue; } // Agregar nueva página $pdf->AddPage(); // Header con folio $pdf->SetFillColor(44, 62, 80); $pdf->Rect(0, 0, 210, 20, 'F'); $pdf->SetTextColor(255, 255, 255); $pdf->SetFont('Arial', 'B', 14); $pdf->SetXY(10, 7); $pdf->Cell(0, 6, 'FOLIO: ' . $record->folio, 0, 1, 'L'); // Obtener ruta temporal del archivo $tempPath = tempnam(sys_get_temp_dir(), 'pdf_img_'); file_put_contents($tempPath, $fileContent); // Obtener dimensiones de la imagen $imageInfo = getimagesize($tempPath); if ($imageInfo !== false) { list($originalWidth, $originalHeight) = $imageInfo; $imageType = $imageInfo[2]; $availableWidth = 190; // 210mm - 20mm márgenes $availableHeight = 247; // 297mm - 20mm header - 20mm footer - 10mm márgenes // Calcular dimensiones manteniendo proporción $ratio = min($availableWidth / $originalWidth, $availableHeight / $originalHeight); $newWidth = $originalWidth * $ratio; $newHeight = $originalHeight * $ratio; // Centrar imagen $x = (210 - $newWidth) / 2; $y = 25 + (($availableHeight - $newHeight) / 2); // Determinar tipo de imagen $imageExtension = ''; switch ($imageType) { case IMAGETYPE_JPEG: $imageExtension = 'JPEG'; break; case IMAGETYPE_JPEG: $imageExtension = 'JPG'; break; case IMAGETYPE_PNG: $imageExtension = 'PNG'; break; default: // Si no es un formato soportado, continuar unlink($tempPath); continue 2; } // Insertar imagen $pdf->Image($tempPath, $x, $y, $newWidth, $newHeight, $imageExtension); } // Limpiar archivo temporal unlink($tempPath); } // Verificar que se agregaron páginas if ($pdf->PageNo() == 0) { return ApiResponse::NOT_FOUND->response([ 'message' => 'No se pudieron procesar las imágenes del expediente.', 'record_id' => $id, ]); } // Generar PDF $pdfContent = $pdf->Output('S'); return response($pdfContent, 200) ->header('Content-Type', 'application/pdf') ->header('Content-Disposition', 'inline; filename="expediente-imagenes-' . $record->folio . '.pdf"'); } catch (\Exception $e) { return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al generar el PDF de imágenes', 'error' => $e->getMessage(), ]); } } public function generatePdfForm($id) { try { $record = Record::with([ 'vehicle', 'vehicle.owner', 'vehicle.tag', ])->findOrFail($id); if (!$record->vehicle) { return ApiResponse::NOT_FOUND->response([ 'message' => 'El registro no tiene un vehículo asociado.', 'record_id' => $id, ]); } $vehicle = $record->vehicle; $owner = $vehicle->owner; $tag = $vehicle->tag; $now = Carbon::now()->locale('es_MX'); $data = [ // Datos del vehículo 'marca' => strtoupper($vehicle->marca ?? ''), 'linea' => strtoupper($vehicle->linea ?? ''), 'modelo' => $vehicle->modelo ?? '', 'niv' => strtoupper($vehicle->niv ?? ''), 'numero_motor' => strtoupper($vehicle->numero_motor ?? ''), 'placa' => strtoupper($vehicle->placa ?? ''), 'folio' => $tag?->folio ?? $record->folio ?? '', // Datos del propietario 'telefono' => $owner?->telefono ?? '', // Fecha actual 'fecha' => $now->format('d'), 'mes' => ucfirst($now->translatedFormat('F')), 'anio' => $now->format('Y'), 'record_id' => $record->id, 'owner_name' => $owner?->full_name ?? '', ]; $pdf = Pdf::loadView('pdfs.form', $data) ->setPaper('a4', 'portrait') ->setOptions([ 'defaultFont' => 'sans-serif', 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => true, ]); return $pdf->stream('solicitud-sustitucion-' . time() . '.pdf'); } catch (\Exception $e) { return ApiResponse::NOT_FOUND->response([ 'message' => 'No se encontró el registro del expediente proporcionado.', 'record_id' => $id, ]); } catch (\Exception $e) { return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al generar el PDF del formulario', 'error' => $e->getMessage(), ]); } } public function pdfDamagedTag(Tag $tag) { try{ $tag->load('status'); if(!$tag->status){ return ApiResponse::NOT_FOUND->response([ 'message' => 'El tag no tiene un estado asociado.', 'tag_id' => $tag->id, ]); } // Validar que el tag esté cancelado if (!$tag->isDamaged()) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'Solo se puede generar PDF para tags dañados.', '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_dañada_' . $tag->tag_number . '.pdf'); } catch (\Exception $e) { return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al generar el PDF.', 'error' => $e->getMessage(), ]); } } public function pdfCancelledTag(Tag $tag) { try { $tag->load('status'); if(!$tag->status){ return ApiResponse::NOT_FOUND->response([ 'message' => 'El tag no tiene un estado asociado.', 'tag_id' => $tag->id, ]); } // 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_cancelada', [ '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(), ]); } } public function pdfSubstitutedTag($recordId) { try { // Validar que el tag tenga una sustitución registrada $record = Record::with([ 'vehicle.vehicleTagLogs' => function ($query){ $query->where('action_type', 'sustitucion') ->whereNotNull('cancellation_at') ->latest(); } ])->findOrFail($recordId); $oldTagLog = $record->vehicle->vehicleTagLogs->first(); if (!$oldTagLog) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'No se encontró una sustitución registrada para este expediente.', 'record' => $recordId, ]); } // Obtener datos de sustitución $oldTag = Tag::with([ 'vehicleTagLogs' => function($query){ $query->where('action_type', 'sustitucion') ->whereNotNull('cancellation_at') ->with(['cancellationReason', 'cancelledBy', 'vehicle']) ->latest(); } ])->findOrFail($oldTagLog->tag_id); $hasSubstitution = $oldTag->vehicleTagLogs() ->where('action_type', 'sustitucion') ->whereNotNull('cancellation_at') ->exists(); if(!$hasSubstitution){ return ApiResponse::BAD_REQUEST->response([ 'message' => 'El tag no tiene sustitución registrada.', 'tag' => $oldTag->folio, ]); } $substitutionData = $this->substitutionData($oldTag); $pdf = Pdf::loadView('pdfs.tag_sustitution', [ 'substitution' => $substitutionData, ]) ->setPaper('a4', 'portrait') ->setOptions([ 'defaultFont' => 'sans-serif', 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => true, ]); return $pdf->stream('constancia_sustituida_' . $oldTag->folio . '.pdf'); } catch (\Exception $e) { return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al generar el PDF del tag sustituido.', 'error' => $e->getMessage(), ]); } } private function cancellationData(Tag $tag) { $data = [ 'fecha' => now()->format('d/m/Y'), 'folio' => $tag->folio ?? '', 'tag_number' => $tag->tag_number ?? '', 'placa' => '', 'niv' => '', 'motivo' => 'N/A', 'operador' => 'N/A', 'modulo' => 'No especificado', 'ubicacion' => 'No especificado', ]; // Cargar módulo del tag si existe if ($tag->module_id) { $tag->load('module'); if ($tag->module) { $data['modulo'] = $tag->module->name ?? ''; $data['ubicacion'] = $tag->module->address ?? ''; } } // 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['fecha'] = $tagCancellationLog->cancellation_at->format('d/m/Y'); $data['motivo'] = $tagCancellationLog->cancellationReason->name ?? 'No especificado'; $data['operador'] = $tagCancellationLog->cancelledBy->name ?? 'Sistema'; // Extraer datos adicionales de las observaciones $this->extractAdditionalDataFromObservations($tagCancellationLog->cancellation_observations, $data); // Cargar módulo del tag si existe, sino cargar módulo del usuario if ($tag->module_id && $tag->module) { $data['modulo'] = $tag->module->name; $data['ubicacion'] = $tag->module->address; } elseif ($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; } /** * Extraer datos adicionales de las observaciones de cancelación */ private function extractAdditionalDataFromObservations($observations, &$data) { if (empty($observations)) { return; } // Extraer ID CHIP if (preg_match('/ID CHIP:\s*([^|]+)/', $observations, $matches)) { $data['id_chip'] = trim($matches[1]); } // Extraer PLACA if (preg_match('/PLACA:\s*([^|]+)/', $observations, $matches)) { $data['placa'] = trim($matches[1]); } // Extraer VIN if (preg_match('/VIN:\s*([^|]+)/', $observations, $matches)) { $data['niv'] = trim($matches[1]); } } private function substitutionData(Tag $tag) { $data = [ 'fecha' => now()->format('d/m/Y'), 'folio' => $tag->folio ?? '', 'folio_sustituto' => '', 'id_chip' => '', 'placa' => '', 'niv' => '', 'motivo' => 'N/A', 'operador' => 'N/A', 'modulo' => '', 'ubicacion' => '', ]; // log de CANCELACIÓN del tag original $oldTagLog = $tag->vehicleTagLogs() ->where('action_type', 'sustitucion') ->whereNotNull('cancellation_at') ->with(['cancellationReason', 'cancelledBy', 'vehicle']) ->latest() ->first(); if (!$oldTagLog) { return $data; // No se encontró sustitución } // datos del motivo y operador $data['fecha'] = $oldTagLog->cancellation_at->format('d/m/Y'); $data['motivo'] = $oldTagLog->cancellationReason->name ?? 'No especificado'; $data['operador'] = $oldTagLog->cancelledBy->name ?? 'Sistema'; // módulo del usuario if ($oldTagLog->cancelledBy) { $this->loadUserModule($oldTagLog->cancelledBy, $data); } // datos del vehículo if ($oldTagLog->vehicle) { $data['id_chip'] = $oldTagLog->vehicle->id_chip ?? ''; $data['placa'] = $oldTagLog->vehicle->placa ?? ''; $data['niv'] = $oldTagLog->vehicle->niv ?? ''; // tag NUEVO $newTag = $oldTagLog->vehicle->tag; $data['folio_sustituto'] = $newTag?->folio ?? ''; } 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([ 'folio' => 'nullable|string', 'placa' => 'nullable|string', 'vin' => 'nullable|string', ]); $records = Record::with(['vehicle.owner', 'vehicle.tag', 'files', 'user', 'error']) ->whereNotNull('api_response') ->whereRaw("JSON_EXTRACT(api_response, '$.has_error') = true") ->orderBy('id', 'ASC'); if ($request->filled('folio')) { $records->where('folio', 'LIKE', '%' . $request->input('folio') . '%'); } if ($request->filled('placa')) { $records->whereHas('vehicle', function ($q) use ($request) { $q->where('placa', 'LIKE', '%' . $request->input('placa') . '%'); }); } if ($request->filled('vin')) { $records->whereHas('vehicle', function ($q) use ($request) { $q->where('niv', 'LIKE', '%' . $request->input('vin') . '%'); }); } return ApiResponse::OK->response([ 'message' => 'Expedientes con errores encontrados exitosamente', 'records' => $records->paginate(config('app.pagination')), ]); } }