diff --git a/admin_api/urls.py b/admin_api/urls.py index f6d4e4a..32f894f 100644 --- a/admin_api/urls.py +++ b/admin_api/urls.py @@ -42,6 +42,8 @@ urlpatterns = [ path('partners/me/staff//', views.PartnerMeStaffDetailView.as_view(), name='partner-me-staff-detail'), # Partner-Me: check-in (Sprint 7) path('partners/me/check-in/', views.PartnerMeCheckInView.as_view(), name='partner-me-check-in'), + # Partner-Me: dashboard (Sprint 8) + path('partners/me/dashboard/', views.PartnerDashboardView.as_view(), name='partner-me-dashboard'), path('users/metrics/', views.UserMetricsView.as_view(), name='user-metrics'), path('users/', views.UserListView.as_view(), name='user-list'), path('users//', views.UserDetailView.as_view(), name='user-detail'), diff --git a/admin_api/views.py b/admin_api/views.py index 30195c1..3c24926 100644 --- a/admin_api/views.py +++ b/admin_api/views.py @@ -4282,3 +4282,135 @@ class PartnerMeCheckInView(APIView): 'event': event.title if event else '—', 'ticketId': ticket.ticket_id, }) + + +# ============================================================ +# Sprint 8 — Partner Dashboard Aggregate +# ============================================================ + +class PartnerDashboardView(APIView): + """ + GET /api/v1/partners/me/dashboard/ + Returns partner KPIs, recent bookings, and upcoming events in one call. + """ + permission_classes = [IsAuthenticated] + + def get(self, request): + from bookings.models import Booking + from django.db.models import Sum, Count, F, Q, DecimalField, ExpressionWrapper + from datetime import date, timedelta + + partner, err = _require_partner(request) + if err: + return err + + today = date.today() + # Current period: last 30 days + period_start = today - timedelta(days=30) + # Previous period: 31–60 days ago (for change %) + prev_start = today - timedelta(days=60) + prev_end = today - timedelta(days=31) + + def booking_revenue_qs(start, end): + return Booking.objects.filter( + ticket_meta__event__partner=partner, + payment_status='paid', + created_date__gte=start, + created_date__lte=end, + ).aggregate( + total=Sum( + ExpressionWrapper( + F('price') * F('quantity'), + output_field=DecimalField(max_digits=14, decimal_places=2), + ) + ), + tickets=Sum('quantity'), + ) + + curr = booking_revenue_qs(period_start, today) + prev = booking_revenue_qs(prev_start, prev_end) + + curr_rev = float(curr['total'] or 0) + prev_rev = float(prev['total'] or 0) + curr_tickets = int(curr['tickets'] or 0) + prev_tickets = int(prev['tickets'] or 0) + + def pct_change(curr_v, prev_v): + if prev_v == 0: + return '+0%' if curr_v == 0 else '+100%' + delta = ((curr_v - prev_v) / prev_v) * 100 + sign = '+' if delta >= 0 else '' + return f'{sign}{delta:.1f}%' + + from events.models import Event + active_events = Event.objects.filter( + partner=partner, event_status__in=['published', 'live'] + ).count() + total_events = Event.objects.filter(partner=partner).count() + + # KPIs + kpis = { + 'totalRevenue': f'\u20b9{curr_rev:,.0f}', + 'revenueChange': pct_change(curr_rev, prev_rev), + 'ticketsSold': f'{curr_tickets:,}', + 'ticketsChange': pct_change(curr_tickets, prev_tickets), + 'activeEvents': str(active_events), + 'totalEvents': str(total_events), + } + + # Recent bookings (last 5 paid + pending, newest first) + recent_qs = Booking.objects.filter( + ticket_meta__event__partner=partner, + ).select_related('user', 'ticket_meta__event', 'ticket_type').order_by( + '-created_date', '-id' + )[:5] + + recent_bookings = [] + for b in recent_qs: + user = b.user + name = f"{user.first_name} {user.last_name}".strip() or user.username + event = b.ticket_meta.event if b.ticket_meta_id else None + recent_bookings.append({ + 'id': b.booking_id or f'BKG-{b.id}', + 'event': event.title if event else '—', + 'customer': name, + 'date': b.created_date.isoformat() if b.created_date else None, + 'amount': f'\u20b9{float(b.price * b.quantity):,.0f}', + 'status': b.payment_status, + }) + + # Upcoming events (next 3 by date, have a future start date or are published) + upcoming_qs = Event.objects.filter( + partner=partner, + event_status__in=['published', 'live', 'draft'], + ).order_by('event_date', 'id')[:5] + + upcoming_events = [] + for ev in upcoming_qs: + # Count tickets sold for this event + sold = Booking.objects.filter( + ticket_meta__event=ev, payment_status='paid' + ).aggregate(s=Sum('quantity'))['s'] or 0 + + # Get total capacity from TicketMeta + from bookings.models import TicketMeta + try: + meta = TicketMeta.objects.get(event=ev) + total_capacity = meta.maximum_quantity or 0 + except TicketMeta.DoesNotExist: + total_capacity = 0 + + upcoming_events.append({ + 'id': str(ev.id), + 'name': ev.title, + 'date': ev.event_date.isoformat() if ev.event_date else None, + 'sold': sold, + 'total': total_capacity, + 'status': ev.event_status, + }) + + return Response({ + 'kpis': kpis, + 'recentBookings': recent_bookings, + 'upcomingEvents': upcoming_events, + })