Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tickets/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@

path('scan/', admin.scan_tickets, name='scan_tickets'),
path('scan/<slug:event_slug>/', admin.scan_tickets_event, name='scan_tickets_event'),
path('api/tickets/check-by-dni/', admin.check_ticket_by_dni, name='check_ticket_by_dni'),
path('api/tickets/<str:ticket_key>/check/', admin.check_ticket, name='check_ticket'),
path('api/tickets/<str:ticket_key>/check-public/', admin.check_ticket_public, name='check_ticket_public'),
path('api/tickets/<str:ticket_key>/mark-used/', admin.mark_ticket_used, name='mark_ticket_used'),
Expand Down
98 changes: 68 additions & 30 deletions tickets/views/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from tickets.models import NewTicket
from events.models import Event
from django.core.exceptions import ObjectDoesNotExist
from user_profile.models import Profile

def is_admin_or_puerta(user):
return user.is_superuser or user.groups.filter(name='Puerta').exists()
Expand All @@ -23,6 +24,45 @@ def has_scanner_access(user, event=None):
# Fallback to old group-based logic for backward compatibility
return user.groups.filter(name='Puerta').exists()


def _ticket_check_response(ticket):
"""Build the JSON response dict for check_ticket / check_ticket_by_dni."""
profile = getattr(ticket.holder, 'profile', None) if ticket.holder else None
user_info = None
if ticket.holder:
user_info = {
'first_name': ticket.holder.first_name,
'last_name': ticket.holder.last_name,
'document_type': profile.document_type if profile else None,
'document_number': profile.document_number if profile else None,
}
return {
'key': ticket.key,
'ticket_type': str(ticket.ticket_type),
'is_used': ticket.is_used,
'used_at': ticket.used_at.isoformat() if ticket.used_at else None,
'scanned_by': {
'id': ticket.scanned_by.id,
'username': ticket.scanned_by.username,
'full_name': ticket.scanned_by.get_full_name() or ticket.scanned_by.username,
'email': ticket.scanned_by.email,
} if ticket.scanned_by else None,
'notes': ticket.notes,
'photos': [
{
'id': photo.id,
'url': photo.photo.url,
'name': photo.photo.name.split('/')[-1],
'uploaded_at': photo.created_at.isoformat() if photo.created_at else None,
'uploaded_by': photo.uploaded_by.get_full_name() or photo.uploaded_by.username,
}
for photo in ticket.ticket_photos.all()
],
'owner_name': f"{ticket.owner.first_name} {ticket.owner.last_name}" if ticket.owner else None,
'user_info': user_info,
}


@user_passes_test(is_admin_or_puerta)
def scan_tickets(request):
return render(request, 'mi_fuego/admin/scan_tickets.html')
Expand Down Expand Up @@ -158,41 +198,39 @@ def check_ticket(request, ticket_key):
if not has_scanner_access(request.user, ticket.event):
return JsonResponse({'error': 'No tienes permisos para verificar tickets de este evento'}, status=403)

return JsonResponse({
'key': ticket.key,
'ticket_type': str(ticket.ticket_type),
'is_used': ticket.is_used,
'used_at': ticket.used_at.isoformat() if ticket.used_at else None,
'scanned_by': {
'id': ticket.scanned_by.id,
'username': ticket.scanned_by.username,
'full_name': ticket.scanned_by.get_full_name() or ticket.scanned_by.username,
'email': ticket.scanned_by.email
} if ticket.scanned_by else None,
'notes': ticket.notes,
'photos': [
{
'id': photo.id,
'url': photo.photo.url,
'name': photo.photo.name.split('/')[-1], # Get filename
'uploaded_at': photo.created_at.isoformat() if photo.created_at else None,
'uploaded_by': photo.uploaded_by.get_full_name() or photo.uploaded_by.username
}
for photo in ticket.ticket_photos.all()
],
'owner_name': f"{ticket.owner.first_name} {ticket.owner.last_name}" if ticket.owner else None,
'user_info': {
'first_name': ticket.holder.first_name,
'last_name': ticket.holder.last_name,
'document_type': ticket.holder.profile.document_type,
'document_number': ticket.holder.profile.document_number
} if ticket.holder else None
})
return JsonResponse(_ticket_check_response(ticket))
except ObjectDoesNotExist:
return JsonResponse({'error': 'Bono no encontrado'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)


@login_required
def check_ticket_by_dni(request):
"""Look up a ticket by holder DNI for the current event (scanner search by DNI)."""
event_slug = request.GET.get('event_slug')
dni = (request.GET.get('dni') or '').strip()
if not event_slug:
return JsonResponse({'error': 'Falta el evento (event_slug)'}, status=400)
if not dni:
return JsonResponse({'error': 'Ingresá el DNI para buscar'}, status=400)
try:
event = Event.objects.get(slug=event_slug)
if not has_scanner_access(request.user, event):
return JsonResponse({'error': 'No tienes permisos para verificar tickets de este evento'}, status=403)
profile = Profile.objects.filter(document_number__iexact=dni).first()
if not profile:
return JsonResponse({'error': 'No se encontró ningún bono con ese DNI en este evento'}, status=404)
ticket = NewTicket.objects.filter(event=event, holder=profile.user).order_by('id').first()
if not ticket:
return JsonResponse({'error': 'No se encontró ningún bono con ese DNI en este evento'}, status=404)
return JsonResponse(_ticket_check_response(ticket))
except Event.DoesNotExist:
return JsonResponse({'error': 'Evento no encontrado'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)


@login_required
def mark_ticket_used(request, ticket_key):
if request.method != 'POST':
Expand Down
75 changes: 75 additions & 0 deletions user_profile/templates/mi_fuego/admin/scan_tickets.html
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@
<i class="fas fa-stop me-2"></i>Detener Cámara
</button>
</div>
{% if event %}
<!-- Buscar por DNI -->
<div class="border-top pt-3 mt-3">
<label class="form-label text-muted small mb-2">Buscar por DNI</label>
<div class="input-group">
<input type="text" id="dni-search-input" class="form-control form-control-lg" placeholder="Ingresá el DNI" inputmode="numeric" autocomplete="off">
<button type="button" id="dni-search-btn" class="btn btn-outline-primary btn-lg">
<i class="fas fa-search me-1"></i> Buscar
</button>
</div>
</div>
{% endif %}
</div>
</div>
</div>
Expand Down Expand Up @@ -628,6 +640,54 @@ <h5 class="modal-title" id="photoModalLabel">Imagen</h5>
}
}

async function checkTicketByDni() {
const dniInput = document.getElementById('dni-search-input');
const btn = document.getElementById('dni-search-btn');
if (!dniInput || !btn) return;
const dni = (dniInput.value || '').trim();
if (!dni) {
showError('Ingresá el DNI para buscar');
return;
}
const eventSlug = '{{ event.slug|default:"" }}';
if (!eventSlug) {
showError('No hay evento seleccionado');
return;
}
try {
btn.disabled = true;
document.getElementById('error-message').classList.add('d-none');
const url = `/api/tickets/check-by-dni/?event_slug=${encodeURIComponent(eventSlug)}&dni=${encodeURIComponent(dni)}`;
const response = await fetch(url, {
headers: {
'X-CSRFToken': getCSRFToken(),
'Content-Type': 'application/json',
}
});
const data = await response.json();
if (response.ok) {
displayTicketInfo(data);
showTicketView();
if (isScanning) {
await stopScanning();
const toggleButton = document.getElementById('toggleCamera');
const startButton = document.getElementById('startCameraBtn');
startButton.style.display = 'block';
toggleButton.classList.add('d-none');
isScanning = false;
}
dniInput.value = '';
} else {
showError(data.error || 'No se encontró ningún bono con ese DNI');
}
} catch (err) {
console.error('Check by DNI error:', err);
showError('Error al buscar por DNI: ' + (err.message || 'Error desconocido'));
} finally {
btn.disabled = false;
}
}

async function markTicketAsUsed(ticketKey) {
try {
const response = await fetch(`/api/tickets/${ticketKey}/mark-used/`, {
Expand Down Expand Up @@ -1217,6 +1277,21 @@ <h5 class="modal-title" id="photoModalLabel">Imagen</h5>
initializeScanner();
hideWhatsAppWidgets();

// Buscar por DNI
const dniSearchBtn = document.getElementById('dni-search-btn');
const dniSearchInput = document.getElementById('dni-search-input');
if (dniSearchBtn) {
dniSearchBtn.addEventListener('click', checkTicketByDni);
}
if (dniSearchInput) {
dniSearchInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
checkTicketByDni();
}
});
}

// Add event listener for increment attendees left button
const incrementBtn = document.getElementById('decrement-occupancy-btn');
if (incrementBtn) {
Expand Down