diff --git a/app/Http/Controllers/Repuve/CancellationController.php b/app/Http/Controllers/Repuve/CancellationController.php index 53dda44..fc80b19 100644 --- a/app/Http/Controllers/Repuve/CancellationController.php +++ b/app/Http/Controllers/Repuve/CancellationController.php @@ -14,6 +14,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Notsoweb\ApiResponse\Enums\ApiResponse; +use Barryvdh\DomPDF\Facade\Pdf; class CancellationController extends Controller { @@ -239,27 +240,69 @@ public function cancelarTagNoAsignado(Request $request) } // Cancelar el tag - $tag->markAsCancelled(); + $tag->markAsDamaged(); 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, - ], - ]); + try { + // Recargar el tag con sus relaciones actualizadas + $tag->load(['status', 'cancellationLogs.cancellationReason', 'cancellationLogs.cancelledBy', 'module']); + + // Obtener datos de cancelación del último log + $lastCancellation = $tag->cancellationLogs() + ->with(['cancellationReason', 'cancelledBy']) + ->latest() + ->first(); + + // Preparar datos para el PDF + $cancellationData = [ + 'fecha' => $lastCancellation ? $lastCancellation->cancellation_at->format('d/m/Y') : now()->format('d/m/Y'), + 'folio' => $tag->folio ?? '', + 'id_chip' => $request->id_chip ?? '', + 'motivo' => $lastCancellation && $lastCancellation->cancellationReason + ? $lastCancellation->cancellationReason->name + : 'No especificado', + 'operador' => $lastCancellation && $lastCancellation->cancelledBy + ? $lastCancellation->cancelledBy->name + : 'Sistema', + 'modulo' => $tag->module ? $tag->module->name : 'No especificado', + 'ubicacion' => $tag->module ? $tag->module->address : 'No especificado', + ]; + + $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 ?? $tag->folio) . '.pdf'); + } catch (\Exception $e) { + // Si falla la generación del PDF, devolver respuesta JSON + Log::error('Error al generar PDF de tag cancelado: ' . $e->getMessage()); + + return ApiResponse::OK->response([ + 'message' => 'Tag cancelado exitosamente (Error al generar PDF)', + 'tag' => [ + 'id' => $tag->id, + 'tag_number' => $tag->tag_number, + 'folio' => $tag->folio, + 'previous_status' => 'Disponible', + 'new_status' => 'Dañado', + ], + 'cancellation' => [ + 'id' => $cancellationLog->id, + 'reason' => $cancellationLog->cancellationReason->name, + 'observations' => $cancellationLog->cancellation_observations, + 'cancelled_at' => $cancellationLog->cancellation_at->toDateTimeString(), + 'cancelled_by' => Auth::user()->name, + ], + 'pdf_error' => $e->getMessage(), + ]); + } } catch (\Exception $e) { DB::rollBack(); diff --git a/app/Http/Controllers/Repuve/UpdateController.php b/app/Http/Controllers/Repuve/UpdateController.php index 48536e5..baa043b 100644 --- a/app/Http/Controllers/Repuve/UpdateController.php +++ b/app/Http/Controllers/Repuve/UpdateController.php @@ -129,14 +129,14 @@ public function tagSubstitution(Request $request) } // Verificar robo del vehículo - /* $isStolen = $this->checkIfStolen($vehicle->niv); + $isStolen = $this->checkIfStolen($vehicle->niv); if ($isStolen) { return ApiResponse::FORBIDDEN->response([ 'message' => 'El vehículo reporta robo. No se puede continuar con la sustitución.', 'niv' => $vehicle->niv, ]); - } */ + } DB::beginTransaction(); @@ -179,9 +179,9 @@ public function tagSubstitution(Request $request) 'performed_by' => Auth::id(), ]); - /* // 6. Enviar a REPUVE Nacional + // 6. Enviar a REPUVE Nacional $datosCompletos = $this->prepararDatosParaInscripcion($vehicle->niv); - ProcessRepuveResponse::dispatch($record->id, $datosCompletos); */ + ProcessRepuveResponse::dispatch($record->id, $datosCompletos); DB::commit(); @@ -725,19 +725,6 @@ private function detectOwnerChanges($owner, array $ownerDataEstatal) return $changedFields; } - private function getVehicle(string $niv): array - { - $datos = $this->padronEstatalService->getVehiculoByNiv($niv); - return $this->padronEstatalService->extraerDatosVehiculo($datos); - } - - private function getOwner(string $niv): array - { - $datos = $this->padronEstatalService->getVehiculoByNiv($niv); - return $this->padronEstatalService->extraerDatosPropietario($datos); - } - - /* --------------------------------------------------------- */ public function updateData(VehicleUpdateRequest $request, $id) @@ -754,6 +741,8 @@ public function updateData(VehicleUpdateRequest $request, $id) $hasVehicleChanges = false; $hasOwnerChanges = false; + $hasFolioChange = false; + $oldFolio = $record->folio; if ($request->has('vehicle')) { $vehicleData = $request->input('vehicle', []); @@ -791,6 +780,31 @@ public function updateData(VehicleUpdateRequest $request, $id) } } + // Actualizar folio si se envía + if ($request->has('folio')) { + $newFolio = $request->input('folio'); + + if ($newFolio && $newFolio !== $record->folio) { + // Verificar que el nuevo folio no exista + $existingRecord = Record::where('folio', $newFolio)->first(); + + if ($existingRecord && $existingRecord->id !== $record->id) { + DB::rollBack(); + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'El folio ya existe en el sistema y pertenece a un expediente', + 'folio' => $newFolio, + 'record_id' => $existingRecord->id, + ]); + } + // Mover carpeta de archivos + $this->moveRecordFiles($record->id, $oldFolio, $newFolio); + + // Actualizar folio del record + $record->update(['folio' => $newFolio]); + $hasFolioChange = true; + } + } + // Actualizar propietario si se envían datos if ($request->has('owner')) { $ownerInput = $request->input('owner', []); @@ -847,7 +861,7 @@ public function updateData(VehicleUpdateRequest $request, $id) $replacedFiles = []; $deletedFiles = []; - // Manejar eliminación de archivos (solo de BD, conserva archivo físico) + // Manejar eliminación de archivos if ($request->has('delete_files')) { $filesToDelete = $request->input('delete_files', []); @@ -857,14 +871,23 @@ public function updateData(VehicleUpdateRequest $request, $id) ->first(); if ($fileToDelete) { + $catalogName = $fileToDelete->catalogName; + $deletedFiles[] = [ 'id' => $fileToDelete->id, 'name_id' => $fileToDelete->name_id, - 'name' => $fileToDelete->catalogName->name ?? 'Desconocido', + 'name' => $catalogName->name ?? 'Desconocido', 'path' => $fileToDelete->path, ]; + // Eliminar archivo físico y registro de BD + Storage::disk('public')->delete($fileToDelete->path); $fileToDelete->delete(); + + // Si es Evidencia Adicional, renumerar las restantes + if ($catalogName && $catalogName->name === 'EVIDENCIA ADICIONAL') { + $this->renumberEvidenciasAdicionales($record->id, $record->folio); + } } } } @@ -873,6 +896,7 @@ public function updateData(VehicleUpdateRequest $request, $id) if ($request->hasFile('files')) { $files = $request->file('files'); $nameIds = $request->input('name_id', []); + $observations = $request->input('observations', []); if (!empty($nameIds)) { $validIds = CatalogNameImg::whereIn('id', $nameIds)->pluck('id')->toArray(); @@ -896,26 +920,47 @@ public function updateData(VehicleUpdateRequest $request, $id) ]); } - $existingFile = File::where('record_id', $record->id) - ->where('name_id', $nameId) - ->first(); + // Obtener el nombre del catálogo + $catalogName = CatalogNameImg::find($nameId); - if ($existingFile) { - Storage::disk('public')->delete($existingFile->path); - - $replacedFiles[] = [ - 'id' => $existingFile->id, - 'name_id' => $nameId, - 'old_path' => $existingFile->path, - ]; - - $existingFile->delete(); + if (!$catalogName) { + DB::rollBack(); + return ApiResponse::BAD_REQUEST->response([ + 'message' => "No se encontró el catálogo de nombre con id {$nameId}", + ]); } - // Obtener el nombre del catálogo para el nombre del archivo - $catalogName = CatalogNameImg::find($nameId); $extension = $file->getClientOriginalExtension(); - $fileName = $catalogName->name . '_' . date('dmY_His') . '.' . $extension; + $isEvidenciaAdicional = $catalogName->name === 'EVIDENCIA ADICIONAL'; + + // Si NO es Evidencia Adicional, verificar si existe y reemplazar + if (!$isEvidenciaAdicional) { + $existingFile = File::where('record_id', $record->id) + ->where('name_id', $nameId) + ->first(); + + if ($existingFile) { + Storage::disk('public')->delete($existingFile->path); + + $replacedFiles[] = [ + 'id' => $existingFile->id, + 'name_id' => $nameId, + 'old_path' => $existingFile->path, + ]; + + $existingFile->delete(); + } + + $fileName = $catalogName->name . '_' . date('dmY_His') . '.' . $extension; + } else { + // Si es Evidencia Adicional, contar cuántas ya existen y agregar número + $count = File::where('record_id', $record->id) + ->where('name_id', $nameId) + ->count(); + + $fileName = 'Evidencia_Adicional_' . ($count + 1) . '_' . date('dmY_His') . '.' . $extension; + } + $path = $file->storeAs("records/{$record->folio}", $fileName, 'public'); $md5 = md5_file($file->getRealPath()); @@ -924,20 +969,33 @@ public function updateData(VehicleUpdateRequest $request, $id) 'path' => $path, 'md5' => $md5, 'record_id' => $record->id, + 'observations' => $observations[$indx] ?? null, ]); + // Calcular el número de Evidencia Adicional si aplica + $displayNumber = null; + if ($isEvidenciaAdicional) { + $displayNumber = File::where('record_id', $record->id) + ->where('name_id', $nameId) + ->where('id', '<=', $fileRecord->id) + ->count(); + } + $uploadedFiles[] = [ 'id' => $fileRecord->id, 'name' => $catalogName->name, + 'display_name' => $isEvidenciaAdicional ? "Evidencia Adicional {$displayNumber}" : $catalogName->name, 'path' => $fileRecord->path, 'url' => $fileRecord->url, - 'replaced' => $existingFile !== null, + 'observations' => $fileRecord->observations, + 'number' => $displayNumber, + 'replaced' => isset($existingFile) && $existingFile !== null, ]; } } // Registrar el log de cambios si hubo actualizaciones - if ($hasVehicleChanges || $hasOwnerChanges || count($uploadedFiles) > 0 || count($deletedFiles) > 0) { + if ($hasVehicleChanges || $hasOwnerChanges || $hasFolioChange || count($uploadedFiles) > 0 || count($deletedFiles) > 0) { VehicleTagLog::create([ 'vehicle_id' => $vehicle->id, 'tag_id' => $tag->id, @@ -947,7 +1005,7 @@ public function updateData(VehicleUpdateRequest $request, $id) } // datos para REPUVE Nacional usando datos actuales de la BD - if ($hasVehicleChanges || $hasOwnerChanges || count($uploadedFiles) > 0 || count($deletedFiles) > 0) { + if ($hasVehicleChanges || $hasOwnerChanges || $hasFolioChange || count($uploadedFiles) > 0 || count($deletedFiles) > 0) { // Recargar el vehículo y propietario con los datos actualizados $vehicle->refresh(); $owner->refresh(); @@ -1000,8 +1058,16 @@ public function updateData(VehicleUpdateRequest $request, $id) $record->load(['vehicle.owner', 'vehicle.tag', 'files', 'error']); $message = 'Expediente actualizado exitosamente'; - if (!$hasVehicleChanges && !$hasOwnerChanges && empty($uploadedFiles) && empty($deletedFiles)) { + if (!$hasVehicleChanges && !$hasOwnerChanges && !$hasFolioChange && empty($uploadedFiles) && empty($deletedFiles)) { $message = 'No se detectaron cambios. Los datos ya estaban actualizados.'; + } elseif ($hasFolioChange) { + $message = 'Folio actualizado exitosamente de "' . $oldFolio . '" a "' . $record->folio . '".'; + if ($hasVehicleChanges || $hasOwnerChanges) { + $message .= ' Datos del vehículo/propietario actualizados.'; + } + if (!empty($uploadedFiles) || !empty($deletedFiles)) { + $message .= ' Archivos modificados.'; + } } elseif (($hasVehicleChanges || $hasOwnerChanges) && (!empty($uploadedFiles) || !empty($deletedFiles))) { $message = 'Datos del vehículo/propietario y archivos actualizados exitosamente.'; } elseif (($hasVehicleChanges || $hasOwnerChanges) && empty($uploadedFiles) && empty($deletedFiles)) { @@ -1010,6 +1076,7 @@ public function updateData(VehicleUpdateRequest $request, $id) $message = 'Archivos modificados exitosamente. No hubo cambios en los datos del vehículo/propietario.'; } + return ApiResponse::OK->response([ 'message' => $message, 'has_error' => false, @@ -1099,4 +1166,83 @@ public function resendToRepuve($id) ]); } } + + /** + * Renumerar las Evidencias Adicionales después de eliminar una + */ + private function renumberEvidenciasAdicionales(int $recordId, string $folio): void + { + $catalogName = CatalogNameImg::where('name', 'EVIDENCIA ADICIONAL')->first(); + if (!$catalogName) { + return; + } + + // Obtener todas las evidencias adicionales ordenadas por ID + $files = File::where('record_id', $recordId) + ->where('name_id', $catalogName->id) + ->orderBy('id') + ->get(); + + foreach ($files as $index => $file) { + $newNumber = $index + 1; + $extension = pathinfo($file->path, PATHINFO_EXTENSION); + $oldPath = $file->path; + + // Nuevo nombre de archivo con numeración actualizada + $newFileName = 'Evidencia_Adicional_' . $newNumber . '_' . date('dmY_His') . '.' . $extension; + $newPath = "records/{$folio}/" . $newFileName; + + // Solo renombrar si el path es diferente + if ($oldPath !== $newPath) { + // Verificar que el archivo antiguo existe antes de intentar moverlo + if (Storage::disk('public')->exists($oldPath)) { + // Renombrar archivo físico + Storage::disk('public')->move($oldPath, $newPath); + + // Actualizar registro en BD + $file->update(['path' => $newPath]); + } + } + } + } + + private function moveRecordFiles(int $recordId, string $oldFolio, string $newFolio): void + { + $files = File::where('record_id', $recordId)->get(); + + $oldDirectory = "records/{$oldFolio}"; + $newDirectory = "records/{$newFolio}"; + + // Verificar si existe la carpeta antigua + if (!Storage::disk('public')->exists($oldDirectory)) { + return; + } + + // Crear la nueva carpeta si no existe + if (!Storage::disk('public')->exists($newDirectory)) { + Storage::disk('public')->makeDirectory($newDirectory); + } + + // Mover cada archivo + foreach ($files as $file) { + $oldPath = $file->path; + $fileName = basename($oldPath); + $newPath = "{$newDirectory}/{$fileName}"; + + // Verificar que el archivo existe antes de moverlo + if (Storage::disk('public')->exists($oldPath)) { + // Mover archivo físico + Storage::disk('public')->move($oldPath, $newPath); + + // Actualizar registro en BD + $file->update(['path' => $newPath]); + } + } + + // Eliminar carpeta antigua si está vacía + $remainingFiles = Storage::disk('public')->files($oldDirectory); + if (empty($remainingFiles)) { + Storage::disk('public')->deleteDirectory($oldDirectory); + } + } } diff --git a/app/Http/Requests/Repuve/VehicleUpdateRequest.php b/app/Http/Requests/Repuve/VehicleUpdateRequest.php index 9d4ecb0..c0fc3c5 100644 --- a/app/Http/Requests/Repuve/VehicleUpdateRequest.php +++ b/app/Http/Requests/Repuve/VehicleUpdateRequest.php @@ -15,6 +15,7 @@ public function authorize(): bool public function rules(): array { return [ + 'folio' => 'nullable|string|max:50|unique:records,folio,' . $this->route('id'), // --- DATOS DEL VEHÍCULO --- 'vehicle.placa' => 'nullable|string|max:20', 'vehicle.marca' => 'nullable|string|max:100', @@ -60,6 +61,10 @@ public function rules(): array 'files.*' => 'file|mimes:jpeg,png,jpg|max:2048', 'name_id' => 'nullable|array', 'name_id.*' => 'integer|exists:catalog_name_img,id', + 'observations' => 'nullable|array', + 'observations.*' => 'nullable|string|max:500', + 'delete_files' => 'nullable|array', + 'delete_files.*' => 'integer|exists:files,id', ]; } @@ -72,6 +77,10 @@ public function messages(): array '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 2MB', + 'observations.*.max' => 'La observación no debe superar 120 caracteres', + 'delete_files.*.exists' => 'El archivo a eliminar no existe', + 'folio.unique' => 'El folio ya existe en el sistema', + 'folio.max' => 'El folio no puede exceder 50 caracteres', ]; } } diff --git a/app/Models/File.php b/app/Models/File.php index 0b5b1be..1c0fd3c 100644 --- a/app/Models/File.php +++ b/app/Models/File.php @@ -15,6 +15,7 @@ class File extends Model 'name_id', 'path', 'md5', + 'observations', 'record_id', ]; diff --git a/app/Models/Tag.php b/app/Models/Tag.php index bc19e25..d4ad8ca 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -13,7 +13,7 @@ class Tag extends Model const STATUS_AVAILABLE = 'available'; const STATUS_ASSIGNED = 'assigned'; const STATUS_CANCELLED = 'cancelled'; - const STATUS_LOST = 'lost'; + const STATUS_DAMAGED = 'damaged'; protected $fillable = [ 'folio', @@ -85,6 +85,19 @@ public function markAsCancelled(): void ]); } + /** + * Marcar tag como dañado + */ + public function markAsDamaged(): void + { + $statusDamaged = CatalogTagStatus::where('code', self::STATUS_DAMAGED)->first(); + + $this->update([ + 'status_id' => $statusDamaged->id, + 'vehicle_id' => null, + ]); + } + /** * Verificar si el tag está disponible */ @@ -109,4 +122,12 @@ public function isCancelled(): bool return $this->status->code === self::STATUS_CANCELLED; } + /** + * Verificar si el tag está dañado + */ + public function isDamaged(): bool + { + return $this->status->code === self::STATUS_DAMAGED; + } + } diff --git a/database/migrations/2025_12_18_101350_add_observations_to_files_table.php b/database/migrations/2025_12_18_101350_add_observations_to_files_table.php new file mode 100644 index 0000000..d998574 --- /dev/null +++ b/database/migrations/2025_12_18_101350_add_observations_to_files_table.php @@ -0,0 +1,28 @@ +text('observations')->nullable()->after('md5'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('files', function (Blueprint $table) { + $table->dropColumn('observations'); + }); + } +}; diff --git a/database/seeders/CatalogNameImgSeeder.php b/database/seeders/CatalogNameImgSeeder.php index ddc8b0b..6da0c2d 100644 --- a/database/seeders/CatalogNameImgSeeder.php +++ b/database/seeders/CatalogNameImgSeeder.php @@ -9,9 +9,9 @@ /** * Descripción - * + * * @author Moisés Cortés C. - * + * * @version 1.0.0 */ class CatalogNameImgSeeder extends Seeder @@ -40,6 +40,7 @@ public function run(): void 'IDENTIFICACIÓN OFICIAL TRASERA', 'TARJETA DE CIRCULACIÓN', 'FOTO VIN', + 'EVIDENCIA ADICIONAL', ]; foreach ($names as $name) { diff --git a/database/seeders/CatalogTagStatusSeeder.php b/database/seeders/CatalogTagStatusSeeder.php index 4f91d32..5077248 100644 --- a/database/seeders/CatalogTagStatusSeeder.php +++ b/database/seeders/CatalogTagStatusSeeder.php @@ -37,7 +37,13 @@ public function run(): void [ 'code' => 'cancelled', 'name' => 'Cancelado', - 'description' => 'Tag cancelado (incluye perdidos, dañados o reemplazados)', + 'description' => 'Tag cancelado', + 'active' => true, + ], + [ + 'code' => 'damaged', + 'name' => 'Dañado', + 'description' => 'Tag dañado y no utilizable', 'active' => true, ], ]; diff --git a/resources/views/pdfs/tag.blade.php b/resources/views/pdfs/tag.blade.php index 0747ba6..c704ff1 100644 --- a/resources/views/pdfs/tag.blade.php +++ b/resources/views/pdfs/tag.blade.php @@ -126,11 +126,11 @@ MOTIVO: - {{ strtoupper($cancellation['motivo'] ?? '') }} + {{ mb_strtoupper($cancellation['motivo'] ?? '') }} OPERADOR: - {{ strtoupper($cancellation['operador'] ?? '') }} + {{ mb_strtoupper($cancellation['operador'] ?? '') }} MÓDULO: diff --git a/resources/views/pdfs/tag_sustitution.blade.php b/resources/views/pdfs/tag_sustitution.blade.php index 263ee34..77a0195 100644 --- a/resources/views/pdfs/tag_sustitution.blade.php +++ b/resources/views/pdfs/tag_sustitution.blade.php @@ -1,8 +1,9 @@ + - Constancia Sustituida - REPUVE Tabasco + Constancia Sustituida - REPUVE Tabasco +
@@ -152,4 +156,5 @@
+ diff --git a/routes/api.php b/routes/api.php index 0d983ef..b2a0f12 100644 --- a/routes/api.php +++ b/routes/api.php @@ -42,7 +42,6 @@ Route::get('expediente/{id}/pdfVerificacion', [RecordController::class, 'generatePdfVerification']); Route::get('expediente/{id}/pdfConstancia', [RecordController::class, 'generatePdfConstancia']); Route::get('expediente/{id}/pdfImagenes', [RecordController::class, 'generatePdfImages']); - Route::get('tags/{tag}/pdfTag-cancelado', [RecordController::class, 'pdfCancelledTag']); Route::get('tags/{id}/pdfTag-sustituido', [RecordController::class, 'pdfSubstitutedTag']); Route::get('expediente/{id}/pdfFormulario', [RecordController::class, 'generatePdfForm']); Route::get('RecordErrors', [RecordController::class, 'errors']);