From 8541e6871124ad5fdb2b218be078082704188be4 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Tue, 21 Oct 2025 16:31:54 -0300 Subject: [PATCH] feat: qrcode com contador de leituras --- Controllers/QRController.cs | 4 +- Controllers/TrackingController.cs | 138 +++++++++ Docs/QR-Analytics-Feature.md | 345 +++++++++++++++++++++++ Models/QRCodeHistory.cs | 3 + Models/ViewModels/QRGenerationRequest.cs | 7 + Resources/SharedResource.en.resx | 19 ++ Resources/SharedResource.es-PY.resx | 9 + Resources/SharedResource.pt-BR.resx | 9 + Services/IUserService.cs | 4 + Services/QRRapidoService.cs | 37 ++- Services/UserService.cs | 51 +++- Views/Home/Index.cshtml | 20 +- Views/Pagamento/SelecaoPlano.cshtml | 1 + wwwroot/js/qr-speed-generator.js | 52 +++- 14 files changed, 686 insertions(+), 13 deletions(-) create mode 100644 Controllers/TrackingController.cs create mode 100644 Docs/QR-Analytics-Feature.md diff --git a/Controllers/QRController.cs b/Controllers/QRController.cs index 3dd9c77..7df5dbc 100644 --- a/Controllers/QRController.cs +++ b/Controllers/QRController.cs @@ -307,8 +307,8 @@ namespace QRRapidoApp.Controllers var isAuthenticated = User?.Identity?.IsAuthenticated ?? false; // DEBUG: Log detalhado dos parâmetros recebidos - _logger.LogInformation("🔍 [DEBUG] GenerateRapidWithLogo called - RequestId: {RequestId}, ApplyLogoColorization: {ApplyLogoColorization}, LogoSizePercent: {LogoSizePercent}, HasLogo: {HasLogo}", - requestId, request.ApplyLogoColorization, request.LogoSizePercent, request.HasLogo); + _logger.LogInformation("🔍 [DEBUG] GenerateRapidWithLogo called - RequestId: {RequestId}, EnableTracking: {EnableTracking}, Type: {Type}, ApplyLogoColorization: {ApplyLogoColorization}, LogoSizePercent: {LogoSizePercent}, HasLogo: {HasLogo}", + requestId, request.EnableTracking, request.Type, request.ApplyLogoColorization, request.LogoSizePercent, request.HasLogo); using (_logger.BeginScope(new Dictionary { diff --git a/Controllers/TrackingController.cs b/Controllers/TrackingController.cs new file mode 100644 index 0000000..d6ae381 --- /dev/null +++ b/Controllers/TrackingController.cs @@ -0,0 +1,138 @@ +using Microsoft.AspNetCore.Mvc; +using QRRapidoApp.Services; +using System.Diagnostics; + +namespace QRRapidoApp.Controllers +{ + /// + /// Controller for tracking QR code scans (Premium feature) + /// Handles redirect URLs and analytics + /// + [Route("t")] + public class TrackingController : ControllerBase + { + private readonly IUserService _userService; + private readonly ILogger _logger; + + public TrackingController(IUserService userService, ILogger logger) + { + _userService = userService; + _logger = logger; + } + + /// + /// Track QR code scan and redirect to original URL + /// URL format: https://qrrapido.site/t/{trackingId} + /// + [HttpGet("{trackingId}")] + public async Task TrackAndRedirect(string trackingId) + { + var stopwatch = Stopwatch.StartNew(); + + using (_logger.BeginScope(new Dictionary + { + ["TrackingId"] = trackingId, + ["QRTracking"] = true + })) + { + _logger.LogInformation("QR tracking request - TrackingId: {TrackingId}", trackingId); + + try + { + // Get QR code from database + var qr = await _userService.GetQRByTrackingIdAsync(trackingId); + + if (qr == null) + { + _logger.LogWarning("QR code not found for tracking - TrackingId: {TrackingId}", trackingId); + return NotFound("QR code not found"); + } + + // Increment scan count and update last access (fire and forget for performance) + _ = Task.Run(async () => + { + try + { + await _userService.IncrementQRScanCountAsync(trackingId); + _logger.LogDebug("QR scan count incremented - TrackingId: {TrackingId}, NewCount: {NewCount}", + trackingId, qr.ScanCount + 1); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error incrementing scan count - TrackingId: {TrackingId}", trackingId); + } + }); + + stopwatch.Stop(); + _logger.LogInformation("QR redirect - TrackingId: {TrackingId}, Destination: {Destination}, ProcessingTime: {ProcessingTimeMs}ms", + trackingId, qr.Content, stopwatch.ElapsedMilliseconds); + + // Redirect to original URL (HTTP 302 temporary redirect) + return Redirect(qr.Content); + } + catch (Exception ex) + { + stopwatch.Stop(); + _logger.LogError(ex, "QR tracking failed - TrackingId: {TrackingId}, ProcessingTime: {ProcessingTimeMs}ms", + trackingId, stopwatch.ElapsedMilliseconds); + + // Return error page instead of exposing exception details + return StatusCode(500, "Error processing QR code"); + } + } + } + + /// + /// Get analytics for a specific QR code (requires authentication) + /// + [HttpGet("analytics/{trackingId}")] + public async Task GetAnalytics(string trackingId) + { + var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + _logger.LogWarning("Analytics request without authentication - TrackingId: {TrackingId}", trackingId); + return Unauthorized(); + } + + try + { + var qr = await _userService.GetQRByTrackingIdAsync(trackingId); + + if (qr == null) + { + _logger.LogWarning("Analytics request for non-existent QR - TrackingId: {TrackingId}", trackingId); + return NotFound(); + } + + // Verify ownership + if (qr.UserId != userId) + { + _logger.LogWarning("Analytics request for QR owned by different user - TrackingId: {TrackingId}, RequestingUser: {UserId}, Owner: {OwnerId}", + trackingId, userId, qr.UserId); + return Forbid(); + } + + _logger.LogInformation("Analytics retrieved - TrackingId: {TrackingId}, UserId: {UserId}, ScanCount: {ScanCount}", + trackingId, userId, qr.ScanCount); + + return Ok(new + { + trackingId = qr.TrackingId, + scanCount = qr.ScanCount, + createdAt = qr.CreatedAt, + lastAccessedAt = qr.LastAccessedAt, + content = qr.Content, + type = qr.Type + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving analytics - TrackingId: {TrackingId}, UserId: {UserId}", + trackingId, userId); + return StatusCode(500); + } + } + } +} diff --git a/Docs/QR-Analytics-Feature.md b/Docs/QR-Analytics-Feature.md new file mode 100644 index 0000000..43836f1 --- /dev/null +++ b/Docs/QR-Analytics-Feature.md @@ -0,0 +1,345 @@ +# 📊 QR Code Analytics - Premium Feature + +Sistema de rastreamento de leituras de QR codes para usuários premium. + +## 🎯 Visão Geral + +Permite que usuários premium acompanhem quantas vezes seus QR codes foram escaneados, com timestamps e histórico completo. + +### Como Funciona + +1. **Usuário premium** gera QR code com analytics habilitado +2. QR code aponta para URL de redirect: `https://qrrapido.site/t/{trackingId}` +3. Ao escanear, o sistema: + - Incrementa contador de leituras (`scanCount`) + - Atualiza timestamp da última leitura (`lastAccessedAt`) + - Redireciona para URL original (HTTP 302) + +--- + +## 🔧 Implementação Técnica + +### Novos Componentes + +#### 1. **TrackingController** (`Controllers/TrackingController.cs`) + +Endpoints: +- `GET /t/{trackingId}` - Redireciona e contabiliza leitura +- `GET /t/analytics/{trackingId}` - Retorna analytics (requer autenticação) + +#### 2. **Campos no MongoDB** + +**QRCodeHistory** (campos já existentes, agora em uso): +```csharp +public int ScanCount { get; set; } = 0; // Contador de leituras +public DateTime LastAccessedAt { get; set; } // Última leitura +public bool IsDynamic { get; set; } = false; // Se é trackable +public string? TrackingId { get; set; } // ID público (novo) +``` + +#### 3. **QRGenerationRequest** (novo campo) +```csharp +public bool EnableTracking { get; set; } = false; // Premium feature +``` + +#### 4. **UserService** (novos métodos) +```csharp +Task GetQRByTrackingIdAsync(string trackingId); +Task IncrementQRScanCountAsync(string trackingId); +``` + +--- + +## 🚀 Como Usar + +### Backend (já implementado) + +#### Gerar QR com Analytics + +```csharp +var request = new QRGenerationRequest +{ + Type = "url", + Content = "https://google.com", + IsPremium = true, + EnableTracking = true // ✅ Habilita analytics +}; + +var result = await _qrService.GenerateRapidAsync(request); + +// result.TrackingId conterá o ID para analytics +// QR code aponta para: https://qrrapido.site/t/{trackingId} +``` + +#### Consultar Analytics + +```csharp +// Via UserService +var qr = await _userService.GetQRByTrackingIdAsync("abc123def456"); +Console.WriteLine($"Leituras: {qr.ScanCount}"); +Console.WriteLine($"Última leitura: {qr.LastAccessedAt}"); + +// Via API (requer autenticação) +GET /t/analytics/abc123def456 +``` + +**Response:** +```json +{ + "trackingId": "abc123def456", + "scanCount": 42, + "createdAt": "2025-10-19T10:00:00Z", + "lastAccessedAt": "2025-10-20T15:30:00Z", + "content": "https://google.com", + "type": "url" +} +``` + +--- + +### Frontend (ainda não implementado) + +#### Opção 1: Checkbox no Gerador + +```html + +
+ + +
+``` + +```javascript +// Em wwwroot/js/qr-speed-generator.js +const enableTracking = document.getElementById('enableTracking'); + +formData.append('EnableTracking', enableTracking?.checked && this.isPremium); +``` + +#### Opção 2: Dashboard de Analytics + +```html + +
+

Meus QR Codes com Analytics

+ +
+ @foreach (var qr in Model.TrackableQRs) + { +
+

@qr.Content

+

Leituras: @qr.ScanCount

+

Última leitura: @qr.LastAccessedAt.ToString("dd/MM/yyyy HH:mm")

+ Ver detalhes +
+ } +
+
+``` + +--- + +## 📝 Limitações Conhecidas + +### 1. **Apenas QR codes de URL** +- Tracking funciona apenas para `Type = "url"` +- WiFi, vCard, SMS, etc. **não suportam** redirect + +**Razão**: Esses tipos não aceitam URLs customizadas + +**Solução futura**: Implementar pixel de tracking invisível ou API de scan manual + +### 2. **Redirect adiciona ~50-200ms** +- Usuário experimenta pequeno delay ao escanear +- Impacto: Mínimo para maioria dos casos + +**Otimização**: Redirect é assíncrono, contador atualiza em background + +### 3. **Bloqueadores de ads podem afetar** +- Alguns bloqueadores podem marcar `/t/` como tracking +- Probabilidade: Baixa (não usa cookies ou JS) + +--- + +## 🔐 Segurança + +### Proteções Implementadas + +1. **Tracking ID não-sequencial**: GUID truncado (12 chars) +2. **Validação de ownership**: Endpoint `/t/analytics/` verifica se QR pertence ao usuário +3. **Fire-and-forget counting**: Não bloqueia redirect se MongoDB estiver lento +4. **Logging completo**: Todas as leituras são logadas + +### Considerações de Privacidade + +- **Não coleta**: IP, device, localização (por enquanto) +- **Apenas conta**: Quantidade de leituras + timestamp +- **LGPD compliant**: Usuário premium pode deletar histórico a qualquer momento + +--- + +## 📊 Exemplos de Uso + +### Caso 1: Rastreamento de Campanha + +```csharp +// Gerar QR para campanha de marketing +var campaign = new QRGenerationRequest +{ + Type = "url", + Content = "https://minhaloja.com/promo-verao", + IsPremium = true, + EnableTracking = true +}; + +var qr = await _qrService.GenerateRapidAsync(campaign); + +// Distribuir QR em materiais impressos +// Depois consultar: await _userService.GetQRByTrackingIdAsync(qr.TrackingId); +``` + +### Caso 2: Validação de Engajamento + +```csharp +// Verificar se QR code em outdoor teve leituras +var qr = await _userService.GetQRByTrackingIdAsync("abc123def456"); + +if (qr.ScanCount > 100) +{ + Console.WriteLine("Outdoor teve boa visibilidade!"); +} +else if (qr.ScanCount == 0) +{ + Console.WriteLine("QR pode estar ilegível ou mal posicionado"); +} +``` + +--- + +## 🛠️ Configuração + +### appsettings.json + +```json +{ + "App": { + "BaseUrl": "https://qrrapido.site" // ✅ Usado para gerar URLs de tracking + } +} +``` + +### appsettings.Development.json + +```json +{ + "App": { + "BaseUrl": "https://localhost:52428" // ✅ Para testes locais + } +} +``` + +--- + +## 🧪 Testando Localmente + +### 1. Gerar QR com Tracking + +```bash +# POST /api/QR/GenerateRapid +curl -X POST https://localhost:52428/api/QR/GenerateRapid \ + -H "Content-Type: application/json" \ + -d '{ + "type": "url", + "content": "https://google.com", + "isPremium": true, + "enableTracking": true + }' +``` + +**Response:** +```json +{ + "qrCodeBase64": "iVBORw0KG...", + "trackingId": "a1b2c3d4e5f6", + "success": true +} +``` + +### 2. Simular Leitura do QR + +```bash +# Abrir no navegador (ou curl) +curl -L https://localhost:52428/t/a1b2c3d4e5f6 +# Deve redirecionar para https://google.com +``` + +### 3. Verificar Contador + +```bash +# Acessar MongoDB ou via API +curl https://localhost:52428/t/analytics/a1b2c3d4e5f6 \ + -H "Authorization: Bearer {token}" +``` + +**Response:** +```json +{ + "trackingId": "a1b2c3d4e5f6", + "scanCount": 1, + "lastAccessedAt": "2025-10-20T10:00:00Z" +} +``` + +--- + +## 📈 Próximos Passos (Features Futuras) + +### Analytics Avançado +- [ ] Gráfico de leituras por dia/semana/mês +- [ ] Geo-localização (IP → País/Cidade) +- [ ] Tipo de device (mobile/desktop) +- [ ] Browser/OS usado para escanear + +### UI/UX +- [ ] Dashboard de analytics no painel premium +- [ ] Exportar dados para CSV/Excel +- [ ] Notificações quando QR atingir X leituras + +### Performance +- [ ] Cache Redis para evitar queries MongoDB em cada scan +- [ ] Batch updates (atualizar contador a cada N leituras) + +--- + +## 📚 Referências + +- **Tracking URLs**: Mesma estratégia usada por bit.ly, tinyurl, etc. +- **HTTP 302 Redirect**: Padrão para preservar SEO e funcionalidade +- **Fire-and-forget**: Pattern do ASP.NET Core para operações assíncronas não-bloqueantes + +--- + +## 🐛 Troubleshooting + +### Problema: "QR code not found" +- Verificar se `trackingId` existe no MongoDB +- Verificar se QR foi salvo com `IsDynamic = true` + +### Problema: Contador não incrementa +- Verificar logs do `TrackingController` +- Verificar conectividade MongoDB +- Verificar se `IncrementQRScanCountAsync` está sendo chamado + +### Problema: Redirect não funciona +- Verificar se URL original está salva em `Content` +- Verificar se `BaseUrl` está configurado corretamente +- Verificar logs de erro no controller + +--- + +**Implementado em**: 2025-10-20 +**Versão**: 1.0.0 +**Status**: ✅ Backend completo, Frontend pendente diff --git a/Models/QRCodeHistory.cs b/Models/QRCodeHistory.cs index 12f1c9f..19f753f 100644 --- a/Models/QRCodeHistory.cs +++ b/Models/QRCodeHistory.cs @@ -36,6 +36,9 @@ namespace QRRapidoApp.Models [BsonElement("isDynamic")] public bool IsDynamic { get; set; } = false; + [BsonElement("trackingId")] + public string? TrackingId { get; set; } // Public ID for tracking URL (premium feature) + [BsonElement("size")] public int Size { get; set; } = 300; diff --git a/Models/ViewModels/QRGenerationRequest.cs b/Models/ViewModels/QRGenerationRequest.cs index 0b71c75..564cffc 100644 --- a/Models/ViewModels/QRGenerationRequest.cs +++ b/Models/ViewModels/QRGenerationRequest.cs @@ -28,6 +28,12 @@ namespace QRRapidoApp.Models.ViewModels /// Se deve aplicar a cor do QR code no logo (Premium feature) /// public bool ApplyLogoColorization { get; set; } = false; + + /// + /// Se deve habilitar analytics de leitura (Premium feature) + /// Quando habilitado, o QR code usa URL de redirect para contabilizar leituras + /// + public bool EnableTracking { get; set; } = false; } public class QRGenerationResult @@ -42,5 +48,6 @@ namespace QRRapidoApp.Models.ViewModels public bool Success { get; set; } = true; public string? ErrorMessage { get; set; } public LogoReadabilityInfo? ReadabilityInfo { get; set; } // Nova propriedade para análise de legibilidade + public string? TrackingId { get; set; } // Tracking ID for analytics (Premium feature) } } \ No newline at end of file diff --git a/Resources/SharedResource.en.resx b/Resources/SharedResource.en.resx index 211f76b..29854b7 100644 --- a/Resources/SharedResource.en.resx +++ b/Resources/SharedResource.en.resx @@ -783,4 +783,23 @@ Anonymous users: 3 QR codes per day + + + Advanced customization (colors, corners) + + + Logo and image support + + + Unlimited history and downloads + + + QR code scan counter + + + Enable scan counter + + + QR code will use redirect URL to count scans + \ No newline at end of file diff --git a/Resources/SharedResource.es-PY.resx b/Resources/SharedResource.es-PY.resx index 9c4f4d2..9e113dd 100644 --- a/Resources/SharedResource.es-PY.resx +++ b/Resources/SharedResource.es-PY.resx @@ -2041,6 +2041,15 @@ Historial y descargas ilimitadas + + Contador de lecturas de códigos QR + + + Habilitar contador de lecturas + + + El código QR usará URL de redireccionamiento para contabilizar lecturas + Todos los códigos QR generados por QR Rápido son estáticos y permanentes. Esto significa que el contenido está codificado directamente en el código QR y no puede ser modificado después de la generación. Este enfoque garantiza máxima privacidad, ya que no rastreamos escaneos, y funciona sin conexión sin depender de servidores externos. diff --git a/Resources/SharedResource.pt-BR.resx b/Resources/SharedResource.pt-BR.resx index d7f7f9f..e81c1b5 100644 --- a/Resources/SharedResource.pt-BR.resx +++ b/Resources/SharedResource.pt-BR.resx @@ -2131,6 +2131,15 @@ Histórico e downloads ilimitados + + Contador de leituras de QR codes + + + Habilitar contador de leituras + + + QR code usará URL de redirect para contabilizar leituras + Todos os QR codes gerados pelo QR Rápido são estáticos e permanentes. Isso significa que o conteúdo é codificado diretamente no QR code e não pode ser alterado após a geração. Essa abordagem garante máxima privacidade, pois não rastreamos scans, e funciona offline sem depender de servidores externos. diff --git a/Services/IUserService.cs b/Services/IUserService.cs index cba8749..aa220aa 100644 --- a/Services/IUserService.cs +++ b/Services/IUserService.cs @@ -28,5 +28,9 @@ namespace QRRapidoApp.Services Task MarkPremiumCancelledAsync(string userId, DateTime cancelledAt); Task> GetUsersForHistoryCleanupAsync(DateTime cutoffDate); Task DeleteUserHistoryAsync(string userId); + + // QR Code Tracking (Analytics) - Premium feature + Task GetQRByTrackingIdAsync(string trackingId); + Task IncrementQRScanCountAsync(string trackingId); } } \ No newline at end of file diff --git a/Services/QRRapidoService.cs b/Services/QRRapidoService.cs index 3a9ca6c..eb7a0b1 100644 --- a/Services/QRRapidoService.cs +++ b/Services/QRRapidoService.cs @@ -45,6 +45,30 @@ namespace QRRapidoApp.Services { await _semaphore.WaitAsync(); + // Generate tracking ID for premium users with tracking enabled + string? trackingId = null; + string originalContent = request.Content; + + // DEBUG: Log all conditions + _logger.LogInformation("[TRACKING DEBUG] IsPremium: {IsPremium}, EnableTracking: {EnableTracking}, Type: {Type}", + request.IsPremium, request.EnableTracking, request.Type); + + if (request.IsPremium && request.EnableTracking && request.Type == "url") + { + // Only URL type QR codes can use tracking (redirect functionality) + trackingId = Guid.NewGuid().ToString("N").Substring(0, 12); // 12-char tracking ID + var baseUrl = _config.GetValue("App:BaseUrl", "https://qrrapido.site"); + request.Content = $"{baseUrl}/t/{trackingId}"; + + _logger.LogInformation("✅ Tracking enabled for QR - TrackingId: {TrackingId}, OriginalURL: {OriginalURL}", + trackingId, originalContent); + } + else + { + _logger.LogWarning("❌ Tracking NOT enabled - IsPremium: {IsPremium}, EnableTracking: {EnableTracking}, Type: {Type}", + request.IsPremium, request.EnableTracking, request.Type); + } + // Cache key based on content and settings var cacheKey = GenerateCacheKey(request); var cached = await _cache.GetStringAsync(cacheKey); @@ -59,13 +83,13 @@ namespace QRRapidoApp.Services { cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds; cachedResult.FromCache = true; - + // Se não tem análise de legibilidade no cache, gerar agora if (cachedResult.ReadabilityInfo == null) { cachedResult.ReadabilityInfo = await _readabilityAnalyzer.AnalyzeAsync(request, request.Size); } - + return cachedResult; } } @@ -86,9 +110,16 @@ namespace QRRapidoApp.Services Size = qrCode.Length, RequestSettings = request, Success = true, - ReadabilityInfo = readabilityInfo + ReadabilityInfo = readabilityInfo, + TrackingId = trackingId // Include tracking ID if enabled }; + // Restore original content if tracking was enabled + if (trackingId != null) + { + request.Content = originalContent; + } + // Cache for configurable time var cacheExpiration = TimeSpan.FromMinutes(_config.GetValue("Performance:CacheExpirationMinutes", 60)); await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result), new DistributedCacheEntryOptions diff --git a/Services/UserService.cs b/Services/UserService.cs index 4715d04..f21029f 100644 --- a/Services/UserService.cs +++ b/Services/UserService.cs @@ -231,7 +231,9 @@ namespace QRRapidoApp.Services GenerationTimeMs = qrResult.GenerationTimeMs, FromCache = qrResult.FromCache, IsActive = true, - LastAccessedAt = DateTime.UtcNow + LastAccessedAt = DateTime.UtcNow, + TrackingId = qrResult.TrackingId, // Save tracking ID for analytics + IsDynamic = !string.IsNullOrEmpty(qrResult.TrackingId) // Mark as dynamic if tracking is enabled }; await _context.QRCodeHistory.InsertOneAsync(qrHistory); @@ -448,5 +450,52 @@ namespace QRRapidoApp.Services var update = Builders.Update.Set(u => u.StripeCustomerId, stripeCustomerId); await _context.Users.UpdateOneAsync(u => u.Id == userId, update); } + + /// + /// Get QR code by tracking ID (for analytics) + /// + public async Task GetQRByTrackingIdAsync(string trackingId) + { + try + { + return await _context.QRCodeHistory + .Find(q => q.TrackingId == trackingId && q.IsActive) + .FirstOrDefaultAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting QR by tracking ID {TrackingId}: {ErrorMessage}", trackingId, ex.Message); + return null; + } + } + + /// + /// Increment scan count for QR code analytics + /// + public async Task IncrementQRScanCountAsync(string trackingId) + { + try + { + var filter = Builders.Filter.Eq(q => q.TrackingId, trackingId); + var update = Builders.Update + .Inc(q => q.ScanCount, 1) + .Set(q => q.LastAccessedAt, DateTime.UtcNow); + + var result = await _context.QRCodeHistory.UpdateOneAsync(filter, update); + + if (result.ModifiedCount > 0) + { + _logger.LogDebug("QR scan count incremented - TrackingId: {TrackingId}", trackingId); + } + else + { + _logger.LogWarning("Failed to increment scan count - TrackingId not found: {TrackingId}", trackingId); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error incrementing scan count for {TrackingId}: {ErrorMessage}", trackingId, ex.Message); + } + } } } \ No newline at end of file diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index 85e9f06..9a77f8b 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -682,7 +682,7 @@
- + Logo Personalizado - Premium

Adicione sua marca aos QR Codes! Agora com tamanho configurável (10-25%) e colorização automática.

@@ -692,6 +692,23 @@
} + + @* Analytics/Tracking - Premium Feature *@ + @if (isPremium) + { +
+
+ + +
+ @Localizer["TrackingInfo"] +
+
+
+ }
@@ -1195,6 +1212,7 @@
  • @Localizer["AdvancedCustomization"]
  • @Localizer["LogoSupport"]
  • @Localizer["HistoryAndDownloads"]
  • +
  • @Localizer["QRReadCounter"]
  • @Localizer["PrioritySupport"]
  • diff --git a/Views/Pagamento/SelecaoPlano.cshtml b/Views/Pagamento/SelecaoPlano.cshtml index 2f49a3d..ada57db 100644 --- a/Views/Pagamento/SelecaoPlano.cshtml +++ b/Views/Pagamento/SelecaoPlano.cshtml @@ -76,6 +76,7 @@
  • @Localizer["AdvancedCustomization"]
  • @Localizer["LogoSupport"]
  • @Localizer["HistoryAndDownloads"]
  • +
  • @Localizer["QRReadCounter"]
  • @Localizer["PrioritySupport"]
  • diff --git a/wwwroot/js/qr-speed-generator.js b/wwwroot/js/qr-speed-generator.js index 67789e7..f084cf6 100644 --- a/wwwroot/js/qr-speed-generator.js +++ b/wwwroot/js/qr-speed-generator.js @@ -1,9 +1,14 @@ // QR Rapido Speed Generator -class QRRapidoGenerator { +class QRRapidoGenerator { constructor() { this.startTime = 0; this.currentQR = null; this.timerInterval = null; + + // Initialize premium status from hidden input + this.isPremium = this.checkUserPremium(); + console.log('[INIT] QRRapidoGenerator - isPremium:', this.isPremium); + this.languageStrings = { 'pt-BR': { tagline: 'Gere QR codes em segundos!', @@ -737,6 +742,9 @@ class QRRapidoGenerator { const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF'); // Common data for both endpoints + const trackingEnabled = this.getTrackingEnabled(); + console.log('[DEBUG] getTrackingEnabled() returned:', trackingEnabled, 'type:', typeof trackingEnabled); + const commonData = { type: actualType, content: encodedContent, @@ -747,9 +755,12 @@ class QRRapidoGenerator { margin: parseInt(document.getElementById('qr-margin').value), cornerStyle: document.getElementById('corner-style')?.value || 'square', optimizeForSpeed: true, - language: this.currentLang + language: this.currentLang, + enableTracking: trackingEnabled // Analytics feature for premium users }; + console.log('[DEBUG] commonData.enableTracking:', commonData.enableTracking); + // Add dynamic QR data if it's a URL type if (type === 'url') { let dynamicData; @@ -772,11 +783,15 @@ class QRRapidoGenerator { // Use FormData for requests with logo const formData = new FormData(); - // Add all common data to FormData + // Add all common data to FormData with PascalCase for ASP.NET Core model binding Object.keys(commonData).forEach(key => { - formData.append(key, commonData[key]); + // Convert camelCase to PascalCase for FormData (ASP.NET Core compatibility) + const pascalKey = key.charAt(0).toUpperCase() + key.slice(1); + const value = commonData[key]; + console.log('[DEBUG] FormData.append:', pascalKey, '=', value, 'type:', typeof value); + formData.append(pascalKey, value); }); - + // NOVOS PARÂMETROS DE LOGO APRIMORADO const logoSettings = this.getLogoSettings(); formData.append('LogoSizePercent', logoSettings.logoSizePercent.toString()); @@ -808,13 +823,38 @@ class QRRapidoGenerator { getLogoSettings() { const logoSizeSlider = document.getElementById('logo-size-slider'); const logoColorizeToggle = document.getElementById('logo-colorize-toggle'); - + return { logoSizePercent: parseInt(logoSizeSlider?.value || '20'), applyColorization: logoColorizeToggle?.checked || false }; } + // Function to check if tracking is enabled (Premium feature) + getTrackingEnabled() { + const trackingCheckbox = document.getElementById('enable-tracking'); + + // DEBUG: Log each condition separately + console.log('[DEBUG] getTrackingEnabled() - Checkbox exists:', !!trackingCheckbox); + console.log('[DEBUG] getTrackingEnabled() - Checkbox checked:', trackingCheckbox?.checked); + console.log('[DEBUG] getTrackingEnabled() - this.isPremium:', this.isPremium); + + // Only return true if checkbox exists, is checked, and user is premium + // IMPORTANT: Always return boolean (not undefined) for FormData compatibility + const result = !!(trackingCheckbox && trackingCheckbox.checked && this.isPremium); + console.log('[DEBUG] getTrackingEnabled() - Final result:', result); + return result; + } + + // Check if user has premium status + checkUserPremium() { + const premiumStatus = document.getElementById('user-premium-status'); + if (premiumStatus) { + return premiumStatus.value === 'premium'; + } + return false; + } + // Função auxiliar para obter conteúdo baseado no tipo getContentForType(type) { if (type === 'url') {