Fix: Correciones de cancelacion y logs, creación del excel tag sustituidos

This commit is contained in:
Juan Felipe Zapata Moreno 2025-11-26 16:49:24 -06:00
parent 975c6863ff
commit f25901ed9d
13 changed files with 959 additions and 76 deletions

View File

@ -5,6 +5,7 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\Repuve\CancelConstanciaRequest;
use App\Models\Record;
use App\Models\Tag;
use App\Models\VehicleTagLog;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
@ -49,23 +50,73 @@ public function cancelarConstancia(CancelConstanciaRequest $request)
]);
}
// Crear registro en el log de cancelaciones
// Guardar información del tag anterior ANTES de cancelarlo
$oldTagNumber = $tag->tag_number;
$oldFolio = $tag->folio;
// Crear registro en el log de vehiculos
$cancellationLog = VehicleTagLog::create([
'vehicle_id' => $vehicle->id,
'tag_id' => $tag->id,
'action_type' => 'cancelacion',
'cancellation_reason' => $request->cancellation_reason,
'cancellation_observations' => $request->cancellation_observations,
'cancellation_at' => now(),
'cancelled_by' => Auth::id(),
'performed_by' => Auth::id(),
]);
// Actualizar estado del tag a 'cancelled' y desasignar vehículo
$tag->markAsCancelled();
$newTag = null;
$substitutionLog = null;
$isSubstitution = $request->filled('new_tag_number');
if ($isSubstitution) {
$newTag = Tag::where('tag_number', $request->new_tag_number)->first();
if(!$newTag){
DB::rollBack();
return ApiResponse::NOT_FOUND->response([
'message' => 'El nuevo tag proporcionado no existe.',
'new_tag_number' => $request->new_tag_number,
]);
}
if(!$newTag->isAvailable()) {
DB::rollBack();
return ApiResponse::BAD_REQUEST->response([
'message' => 'El nuevo tag no está disponible para asignación',
'new_tag_number' => $request->new_tag_number,
'current_status' => $newTag->status->name,
]);
}
// Usar el folio del NUEVO TAG
$newTag->markAsAssigned($vehicle->id, $newTag->folio);
$substitutionLog = VehicleTagLog::create([
'vehicle_id' => $vehicle->id,
'tag_id' => $newTag->id,
'action_type' => 'sustitucion',
'cancellation_reason' => $request->cancellation_reason,
'cancellation_observations' => 'Tag sustituido. Tag anterior: ' . $oldTagNumber . 'Motivo: ' . ($request->cancellation_observations ?? ''),
'performed_by' => Auth::id(),
]);
$record->update(['folio' => $newTag->folio]);
}
DB::commit();
$message = $isSubstitution
? 'Tag cancelado y sustituido exitosamente'
: 'Constancia cancelada exitosamente';
return ApiResponse::OK->response([
'message' => 'Constancia cancelada exitosamente',
'message' => $message,
'is_substitution' => $isSubstitution,
'cancellation' => [
'id' => $cancellationLog->id,
'vehicle' => [
@ -73,12 +124,18 @@ public function cancelarConstancia(CancelConstanciaRequest $request)
'placa' => $vehicle->placa,
'niv' => $vehicle->niv,
],
'tag' => [
'old_tag' => [
'id' => $tag->id,
'folio' => $tag->folio,
'old_status' => $tag->status->name,
'tag_number' => $tag->tag_number,
'new_status' => 'Cancelado',
],
'new_tag' => $newTag ? [
'id' => $newTag->id,
'folio' => $newTag->folio,
'tag_number' => $newTag->tag_number,
'status' => $newTag->status->name,
] : null,
'cancellation_reason' => $request->cancellation_reason,
'cancellation_observations' => $request->cancellation_observations,
'cancelled_at' => $cancellationLog->cancellation_at->toDateTimeString(),

View File

@ -0,0 +1,262 @@
<?php
namespace App\Http\Controllers\Repuve;
/**
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\VehicleTagLog;
use App\Models\Tag;
use App\Models\Module;
use Carbon\Carbon;
use Notsoweb\ApiResponse\Enums\ApiResponse;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border as PhpSpreadsheetBorder;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
/**
* Descripción
*/
class ExcelController extends Controller
{
public function constanciasSustituidas(Request $request)
{
$request->validate([
'fecha_inicio' => 'required|date',
'fecha_fin' => 'required|date|after_or_equal:fecha_inicio',
'module_id' => 'required|exists:modules,id',
]);
$fechaInicio = Carbon::parse($request->fecha_inicio)->startOfDay();
$fechaFin = Carbon::parse($request->fecha_fin)->endOfDay();
$moduleId = $request->module_id;
// Obtener información del módulo
$module = Module::findOrFail($moduleId);
// Obtener logs de sustitución en el rango de fechas
$logs = VehicleTagLog::with([
'vehicle',
'tag',
])
->where('action_type', 'sustitucion')
->whereHas('vehicle.records', function ($query) use ($moduleId) {
$query->where('module_id', $moduleId);
})
->whereBetween('created_at', [$fechaInicio, $fechaFin])
->orderBy('created_at', 'asc')
->get();
if ($logs->isEmpty()) {
return ApiResponse::NOT_FOUND->response([
'message' => 'No se encontraron constancias sustituidas en el periodo especificado',
'fecha_inicio' => $fechaInicio->format('Y-m-d'),
'fecha_fin' => $fechaFin->format('Y-m-d'),
'module_id' => $moduleId,
]);
}
// Preparar datos para el Excel
$data = $this->prepareExcelData($logs);
// Generar archivo Excel
$fileName = 'Constancias_Sustituidas_' . $fechaInicio->format('Ymd') . '_' . $fechaFin->format('Ymd') . '.xlsx';
$filePath = storage_path('app/temp/' . $fileName);
// Crear directorio temporal si no existe
if (!file_exists(storage_path('app/temp'))) {
mkdir(storage_path('app/temp'), 0755, true);
}
// Crear Excel
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// Agregar logo
$logoPath = storage_path('app/images/logo-seguridad.png');
if (file_exists($logoPath)) {
$drawing = new Drawing();
$drawing->setName('Logo Seguridad');
$drawing->setDescription('Logo Seguridad Pública');
$drawing->setPath($logoPath);
$drawing->setHeight(100); // Altura del logo en pixeles
$drawing->setCoordinates('B1'); // Posición del logo
$drawing->setWorksheet($sheet);
// Ajustar altura de las filas del logo
$sheet->getRowDimension(1)->setRowHeight(45);
}
// Definir estilo de bordes
$borderStyle = [
'borders' => [
'allBorders' => [
'borderStyle' => PhpSpreadsheetBorder::BORDER_THIN,
'color' => ['rgb' => '000000'],
],
],
];
// Empezar después del logo
$row = 5;
// Fila 1: ENTIDAD (empieza en B)
$sheet->setCellValue('B' . $row, 'ENTIDAD:');
$sheet->mergeCells('C' . $row . ':K' . $row);
$sheet->setCellValue('C' . $row, 'TABASCO');
$sheet->getStyle('B' . $row)->getFont()->setBold(true)->setSize(11);
$sheet->getStyle('B' . $row . ':K' . $row)->applyFromArray($borderStyle);
$sheet->getStyle('B' . $row . ':K' . $row)->getAlignment()->setWrapText(true);
$row++;
// Fila 2: MÓDULO (empieza en B)
$sheet->setCellValue('B' . $row, 'MÓDULO:');
$sheet->mergeCells('C' . $row . ':K' . $row);
$sheet->setCellValue('C' . $row, $module->name ?? 'MÓDULO 1. BASE 4');
$sheet->getStyle('B' . $row)->getFont()->setBold(true)->setSize(11);
$sheet->getStyle('B' . $row . ':K' . $row)->applyFromArray($borderStyle);
$sheet->getStyle('B' . $row . ':K' . $row)->getAlignment()->setWrapText(true);
$row++;
// Fila 3: PERIODO A INFORMAR (empieza en B, fecha completa con año)
$sheet->setCellValue('B' . $row, 'PERIODO A INFORMAR:');
$sheet->mergeCells('C' . $row . ':K' . $row);
$sheet->setCellValue('C' . $row, 'del ' . $fechaInicio->format('d') . ' al ' . $fechaFin->format('d') . ' de ' . $fechaInicio->translatedFormat('F') . ' de ' . $fechaInicio->year);
$sheet->getStyle('B' . $row)->getFont()->setBold(true)->setSize(11);
$sheet->getStyle('B' . $row . ':K' . $row)->applyFromArray($borderStyle);
$sheet->getStyle('B' . $row . ':K' . $row)->getAlignment()->setWrapText(true);
$row++;
// Fila vacía
$row++;
// Título: CONSTANCIAS SUSTITUIDAS (combinada B:K)
$sheet->mergeCells('B' . $row . ':K' . $row);
$sheet->setCellValue('B' . $row, 'CONSTANCIAS SUSTITUIDAS');
$sheet->getStyle('B' . $row)->getFont()->setBold(true)->setSize(12);
$sheet->getStyle('B' . $row)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER)->setWrapText(true);
$sheet->getStyle('B' . $row . ':K' . $row)->applyFromArray($borderStyle);
$row++;
// Fila vacía
$row++;
// Encabezados de la tabla
$headers = [
'No.',
'NIV DEL VEHÍCULO',
'NRPV/NCI',
'MARCA DEL VEHÍCULO',
'PLACA',
'AÑO MODELO',
'FOLIO ANTERIOR',
'FOLIO ACTUAL',
'ID DE LA CONSTANCIA (CHIP)',
'FECHA DE REEMPLAZO',
'OBSERVACIONES (MOTIVO DEL REEMPLAZO)',
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . $row, $header);
$col++;
}
$sheet->getStyle('A' . $row . ':K' . $row)->applyFromArray([
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF'], 'size' => 10],
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '8B0000']],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'wrapText' => true],
'borders' => [
'allBorders' => [
'borderStyle' => PhpSpreadsheetBorder::BORDER_THIN,
'color' => ['rgb' => '000000'],
],
],
]);
$row++;
// Agregar datos
foreach ($data as $rowData) {
$col = 'A';
foreach ($rowData as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$sheet->getStyle('A' . $row . ':K' . $row)->applyFromArray([
'font' => ['size' => 10],
'alignment' => ['wrapText' => true],
'borders' => [
'allBorders' => [
'borderStyle' => PhpSpreadsheetBorder::BORDER_THIN,
'color' => ['rgb' => '000000'],
],
],
]);
$row++;
}
// Ajustar anchos de columnas
$sheet->getColumnDimension('A')->setWidth(6);
$sheet->getColumnDimension('B')->setWidth(25);
$sheet->getColumnDimension('C')->setWidth(15);
$sheet->getColumnDimension('D')->setWidth(20);
$sheet->getColumnDimension('E')->setWidth(12);
$sheet->getColumnDimension('F')->setWidth(15);
$sheet->getColumnDimension('G')->setWidth(18);
$sheet->getColumnDimension('H')->setWidth(18);
$sheet->getColumnDimension('I')->setWidth(30);
$sheet->getColumnDimension('J')->setWidth(20);
$sheet->getColumnDimension('K')->setWidth(50);
// Guardar archivo
$writer = new Xlsx($spreadsheet);
$writer->save($filePath);
// Descargar archivo y eliminarlo después
return response()->download($filePath, $fileName)->deleteFileAfterSend(true);
}
private function prepareExcelData($logs)
{
$data = [];
$no = 1;
foreach ($logs as $log) {
$vehicle = $log->vehicle;
$newTag = $log->tag;
// Extraer el folio anterior de las observaciones
$folioAnterior = 'N/A';
if ($log->cancellation_observations) {
if (preg_match('/Folio:\s*([^)]+)/', $log->cancellation_observations, $matches)) {
$folioAnterior = trim($matches[1]);
}
}
$data[] = [
$no,
$vehicle->niv ?? 'N/A',
$vehicle->nrpv ?? 'N/A',
strtoupper($vehicle->marca ?? 'N/A'),
strtoupper($vehicle->placa ?? 'N/A'),
$vehicle->modelo ?? 'N/A',
$folioAnterior,
$newTag->folio ?? 'N/A',
$newTag->tag_number ?? 'N/A',
$log->created_at->format('d/m/Y'),
$log->cancellation_observations ?? 'CONSTANCIA DAÑADA',
];
$no++;
}
return $data;
}
}

View File

@ -13,6 +13,7 @@
use App\Models\File;
use App\Models\Tag;
use App\Models\Error;
use App\Models\VehicleTagLog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use App\Services\RepuveService;
@ -129,6 +130,13 @@ public function vehicleInscription(VehicleStoreRequest $request)
// Asignar Tag al vehículo
$tag->markAsAssigned($vehicle->id, $folio);
VehicleTagLog::create([
'vehicle_id' => $vehicle->id,
'tag_id' => $tag->id,
'action_type' => 'inscripcion',
'performed_by' => Auth::id(),
]);
// Crear registro
$record = Record::create([
'folio' => $folio,

View File

@ -14,7 +14,6 @@
use App\Models\Owner;
use App\Models\Error;
use App\Models\CatalogNameImg;
use App\Models\Tag;
use App\Models\VehicleTagLog;
use App\Services\RepuveService;
use App\Services\PadronEstatalService;
@ -64,16 +63,16 @@ public function vehicleData(Request $request)
'user:id,name,email',
'error:id,code,description'
])
->select([
'id',
'folio',
'vehicle_id',
'user_id',
'error_id',
'created_at',
'updated_at'
])->where('folio', $folio)
->first();
->select([
'id',
'folio',
'vehicle_id',
'user_id',
'error_id',
'created_at',
'updated_at'
])->where('folio', $folio)
->first();
if (!$record) {
return ApiResponse::NOT_FOUND->response([
@ -158,28 +157,15 @@ public function vehicleUpdate(VehicleUpdateRequest $request)
}
$vehicle = $record->vehicle;
$currentTag = $vehicle->tag;
$newTag = Tag::where('tag_number', $tagNumber)->first();
$tag = $vehicle->tag;
if (!$newTag) {
if (!$tag) {
return ApiResponse::NOT_FOUND->response([
'message' => 'No se encontró el tag con el tag_number proporcionado',
'tag_number' => $tagNumber,
]);
}
$isTagReplacement = false;
if($currentTag && $currentTag->id !== $newTag->id) {
if(!$newTag->isAvailable()) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'El tag proporcionado no está disponible para asignación',
'tag_number' => $tagNumber,
'current_status' => $newTag->status->name,
]);
}
$isTagReplacement = true;
}
if ($vehicle->niv !== $niv) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'El NIV no coincide con el registrado en el expediente',
@ -257,26 +243,6 @@ public function vehicleUpdate(VehicleUpdateRequest $request)
]);
}
$tagReplacementLog = null;
if($isTagReplacement) {
$tagReplacementLog = VehicleTagLog::create([
'vehicle_id' => $vehicle->id,
'tag_id' => $currentTag->id,
'cancellation_reason' => 'Otro',
'cancellation_observations' => 'Reemplazo automático al actualizar datos del vehículo: '. $newTag->tag_number,
'cancellation_at' => now(),
'cancelled_by' => Auth::id(),
]);
$currentTag->markAsCancelled();
$newTag->markAsAssigned($vehicle->id, $folio);
$tag = $newTag;
}else{
$tag = $currentTag;
}
$uploadedFiles = [];
$replacedFiles = [];
@ -346,8 +312,17 @@ public function vehicleUpdate(VehicleUpdateRequest $request)
}
}
if ($hasVehicleChanges || $hasOwnerChanges || count($uploadedFiles) > 0) {
VehicleTagLog::create([
'vehicle_id' => $vehicle->id,
'tag_id' => $tag->id,
'action_type' => 'actualizacion',
'performed_by' => Auth::id(),
]);
}
// Solo enviar a REPUVE Nacional si hay cambios
if ($hasVehicleChanges || $hasOwnerChanges || count($uploadedFiles) > 0|| $isTagReplacement) {
if ($hasVehicleChanges || $hasOwnerChanges || count($uploadedFiles) > 0) {
//Envio de datos
$apiResponse = $this->sendToRepuveNacional($niv);
$apiResponse["repuve_response"]["folio_ci"] = $record->folio;
@ -411,15 +386,17 @@ public function vehicleUpdate(VehicleUpdateRequest $request)
DB::commit();
$sentToRepuve = $hasVehicleChanges || $hasOwnerChanges || count($uploadedFiles) > 0 || $isTagReplacement;
$sentToRepuve = $hasVehicleChanges || $hasOwnerChanges || count($uploadedFiles) > 0;
$message = 'Vehículo actualizado exitosamente';
if (!$hasVehicleChanges && !$hasOwnerChanges && empty($uploadedFiles)) {
$message = 'No se detectaron cambios. Los datos ya estaban actualizados.';
} elseif (!$hasVehicleChanges && !$hasOwnerChanges && !$isTagReplacement) {
} elseif (!$hasVehicleChanges && !$hasOwnerChanges && empty($uploadedFiles)) {
$message = 'Solo se actualizaron archivos. Los datos del vehículo/propietario no cambiaron.';
} elseif ($isTagReplacement && !$hasVehicleChanges && !$hasOwnerChanges && empty($uploadedFiles)) {
$message = 'Tag reemplazado exitosamente. Los datos del vehículo/propietario no cambiaron.';
} elseif (($hasVehicleChanges || $hasOwnerChanges) && empty($uploadedFiles)) {
$message = 'Datos del vehículo/propietario actualizados exitosamente. No se subieron archivos.';
} elseif ((!$hasVehicleChanges && !$hasOwnerChanges) && !empty($uploadedFiles)) {
$message = 'Archivos subidos exitosamente. No hubo cambios en los datos del vehículo/propietario.';
}
return ApiResponse::OK->response([
@ -430,20 +407,7 @@ public function vehicleUpdate(VehicleUpdateRequest $request)
'vehicle_updated' => $hasVehicleChanges,
'owner_updated' => $hasOwnerChanges,
'files_uploaded' => count($uploadedFiles) > 0,
'tag_replaced' => $isTagReplacement,
],
'tag_replacement' => $isTagReplacement ? [
'old_tag' => [
'id' => $currentTag->id,
'tag_number' => $currentTag->tag_number,
'status' => $currentTag->status->name,
],
'new_tag' => [
'id' => $newTag->id,
'tag_number' => $newTag->tag_number,
'status' => $newTag->status->name,
],
] : null,
'record' => [
'id' => $record->id,
'folio' => $record->folio,

View File

@ -20,9 +20,10 @@ public function authorize(): bool
public function rules(): array
{
return [
'record_id' => 'required|integer|exists:records,id',
'record_id' => 'required|exists:records,id',
'cancellation_reason' => 'required|in:fallo_lectura_handheld,cambio_parabrisas,roto_al_pegarlo,extravio,otro',
'cancellation_observations' => 'nullable|string|max:1000',
'cancellation_observations' => 'nullable|string',
'new_tag_number' => 'nullable|exists:tags,tag_number',
];
}

View File

@ -13,7 +13,6 @@ public function authorize(): bool
public function rules(): array
{
return [
'record_id' => ['required', 'integer', 'exists:records,id'],
'files' => ['nullable', 'array', 'min:1'],
'files.*' => ['file', 'mimes:jpeg,png,jpg,pdf', 'max:10240'],
'names' => ['nullable', 'array'],
@ -24,8 +23,6 @@ public function rules(): array
public function messages(): array
{
return [
'record_id.required' => 'El id del expediente es requerido',
'record_id.exists' => 'El expediente no existe en el sistema',
'files.*.mimes' => 'Solo se permiten archivos JPG, PNG o JPEG',
'files.*.max' => 'El archivo no debe superar 3MB',
];

View File

@ -72,6 +72,7 @@ public function markAsCancelled(): void
$this->update([
'status_id' => $statusCancelled->id,
'vehicle_id' => null,
'folio' => null,
]);
}

View File

@ -14,10 +14,12 @@ class VehicleTagLog extends Model
protected $fillable = [
'vehicle_id',
'tag_id',
'action_type',
'cancellation_reason',
'cancellation_observations',
'cancellation_at',
'cancelled_by',
'performed_by',
];
protected function casts(): array
@ -39,4 +41,24 @@ public function cancelledBy() {
return $this->belongsTo(User::class, 'cancelled_by');
}
public function isInscription()
{
return $this->action_type === 'inscripcion';
}
public function isUpdate()
{
return $this->action_type === 'actualizacion';
}
public function isSubstitution()
{
return $this->action_type === 'sustitucion';
}
public function isCancellation()
{
return $this->action_type === 'cancelacion';
}
}

View File

@ -66,7 +66,7 @@ private function consultarPadron(string $tipo, string $valor): array
curl_close($ch);
if ($error) {
throw new Exception("Error en la petición SOAP al padrón estatal: {$error}");
throw new Exception("Error en la petición al padrón estatal: {$error}");
}
if ($httpCode !== 200) {
@ -86,7 +86,7 @@ private function parsearRespuesta(string $soapResponse): array
preg_match('/<result>(.*?)<\/result>/s', $soapResponse, $matches);
if (!isset($matches[1])) {
throw new Exception("No se pudo extraer el resultado del SOAP del padrón estatal");
throw new Exception("No se pudo extraer el resultado del padrón estatal");
}
$jsonContent = trim($matches[1]);

View File

@ -16,8 +16,10 @@
"laravel/tinker": "^2.10",
"milon/barcode": "^12.0",
"notsoweb/laravel-core": "dev-main",
"phpoffice/phpspreadsheet": "*",
"setasign/fpdf": "^1.8",
"spatie/laravel-permission": "^6.16",
"spatie/simple-excel": "^3.8",
"tightenco/ziggy": "^2.5"
},
"require-dev": {

526
composer.lock generated
View File

@ -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": "ddec2d00c4e2c1ad362c8e28c77eb079",
"content-hash": "7a79a1a1e064bf49d5a8cfcf7ee4c69c",
"packages": [
{
"name": "barryvdh/laravel-dompdf",
@ -397,6 +397,85 @@
},
"time": "2025-03-14T15:54:58+00:00"
},
{
"name": "composer/pcre",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-11-12T16:29:46+00:00"
},
{
"name": "defuse/php-encryption",
"version": "v2.4.0",
@ -3258,6 +3337,191 @@
],
"time": "2025-07-17T05:12:15+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
"reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.3"
},
"require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^12.0",
"vimeo/psalm": "^6.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
}
],
"time": "2025-07-17T11:15:13+00:00"
},
{
"name": "markbaker/complex",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Complex\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@lange.demon.co.uk"
}
],
"description": "PHP Class for working with complex numbers",
"homepage": "https://github.com/MarkBaker/PHPComplex",
"keywords": [
"complex",
"mathematics"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
},
"time": "2022-12-06T16:21:08+00:00"
},
{
"name": "markbaker/matrix",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPMatrix.git",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpdocumentor/phpdocumentor": "2.*",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "2.*",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"sebastian/phpcpd": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matrix\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@demon-angel.eu"
}
],
"description": "PHP Class for working with matrices",
"homepage": "https://github.com/MarkBaker/PHPMatrix",
"keywords": [
"mathematics",
"matrix",
"vector"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
},
"time": "2022-12-02T22:17:43+00:00"
},
{
"name": "masterminds/html5",
"version": "2.10.0",
@ -4067,6 +4331,99 @@
],
"time": "2024-09-09T07:06:30+00:00"
},
{
"name": "openspout/openspout",
"version": "v4.32.0",
"source": {
"type": "git",
"url": "https://github.com/openspout/openspout.git",
"reference": "41f045c1f632e1474e15d4c7bc3abcb4a153563d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/openspout/openspout/zipball/41f045c1f632e1474e15d4c7bc3abcb4a153563d",
"reference": "41f045c1f632e1474e15d4c7bc3abcb4a153563d",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-filter": "*",
"ext-libxml": "*",
"ext-xmlreader": "*",
"ext-zip": "*",
"php": "~8.3.0 || ~8.4.0 || ~8.5.0"
},
"require-dev": {
"ext-zlib": "*",
"friendsofphp/php-cs-fixer": "^3.86.0",
"infection/infection": "^0.31.2",
"phpbench/phpbench": "^1.4.1",
"phpstan/phpstan": "^2.1.22",
"phpstan/phpstan-phpunit": "^2.0.7",
"phpstan/phpstan-strict-rules": "^2.0.6",
"phpunit/phpunit": "^12.3.7"
},
"suggest": {
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)",
"ext-mbstring": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.3.x-dev"
}
},
"autoload": {
"psr-4": {
"OpenSpout\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Adrien Loison",
"email": "adrien@box.com"
}
],
"description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way",
"homepage": "https://github.com/openspout/openspout",
"keywords": [
"OOXML",
"csv",
"excel",
"memory",
"odf",
"ods",
"office",
"open",
"php",
"read",
"scale",
"spreadsheet",
"stream",
"write",
"xlsx"
],
"support": {
"issues": "https://github.com/openspout/openspout/issues",
"source": "https://github.com/openspout/openspout/tree/v4.32.0"
},
"funding": [
{
"url": "https://paypal.me/filippotessarotto",
"type": "custom"
},
{
"url": "https://github.com/Slamdunk",
"type": "github"
}
],
"time": "2025-09-03T16:03:54+00:00"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v3.1.3",
@ -4282,6 +4639,112 @@
},
"time": "2025-10-06T08:47:40+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "5.3.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "4d597c1aacdde1805a33c525b9758113ea0d90df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/4d597c1aacdde1805a33c525b9758113ea0d90df",
"reference": "4d597c1aacdde1805a33c525b9758113ea0d90df",
"shasum": ""
},
"require": {
"composer/pcre": "^1||^2||^3",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": "^8.1",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.5",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1 || ^2.0",
"phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"keywords": [
"OpenXML",
"excel",
"gnumeric",
"ods",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.3.0"
},
"time": "2025-11-24T15:47:10+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.4",
@ -6000,6 +6463,67 @@
],
"time": "2025-07-23T16:08:05+00:00"
},
{
"name": "spatie/simple-excel",
"version": "3.8.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/simple-excel.git",
"reference": "80c2fd16090d28e1d0036bfac1afc6bfd8452ea3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/simple-excel/zipball/80c2fd16090d28e1d0036bfac1afc6bfd8452ea3",
"reference": "80c2fd16090d28e1d0036bfac1afc6bfd8452ea3",
"shasum": ""
},
"require": {
"illuminate/support": "^9.0|^10.0|^11.0|^12.0",
"openspout/openspout": "^4.30",
"php": "^8.3"
},
"require-dev": {
"pestphp/pest-plugin-laravel": "^1.3|^2.3|^3.0",
"phpunit/phpunit": "^9.4|^10.5|^11.0|^12.0",
"spatie/pest-plugin-snapshots": "^1.1|^2.1",
"spatie/phpunit-snapshot-assertions": "^4.0|^5.1",
"spatie/temporary-directory": "^1.2|^2.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\SimpleExcel\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Read and write simple Excel and CSV files",
"homepage": "https://github.com/spatie/simple-excel",
"keywords": [
"simple-excel",
"spatie"
],
"support": {
"source": "https://github.com/spatie/simple-excel/tree/3.8.1"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-09-24T06:40:28+00:00"
},
{
"name": "symfony/clock",
"version": "v7.3.0",

View File

@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('vehicle_tags_logs', function (Blueprint $table) {
$table->enum('action_type', [
'inscripcion',
'actualizacion',
'sustitucion',
'cancelacion'
])->after('tag_id')->default('cancelacion');
$table->foreignId('performed_by')->nullable()->after('action_type')->constrained('users')->nullOnDelete();
$table->index('action_type');
$table->index('vehicle_id', 'action_type');
$table->index('performed_by');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('vehicle_tags_logs', function (Blueprint $table) {
$table->dropIndex(['action_type']);
$table->dropIndex(['vehicle_id', 'action_type']);
$table->dropIndex(['performed_by']);
$table->dropForeign(['performed_by']);
$table->dropColumn(['action_type', 'performed_by']);
});
}
};

View File

@ -9,6 +9,7 @@
use App\Http\Controllers\Repuve\UpdateController;
use App\Http\Controllers\Repuve\CatalogNameImgController;
use App\Http\Controllers\Repuve\DeviceController;
use App\Http\Controllers\Repuve\ExcelController;
use App\Http\Controllers\Repuve\PackageController;
use App\Http\Controllers\Repuve\TagsController;
@ -72,5 +73,6 @@
/** Rutas públicas */
// Tus rutas públicas
Route::get('excel/constancias-sustituidas', [ExcelController::class, 'constanciasSustituidas']);