feat(sprint8): add PartnerDashboardView + URL for partner me dashboard

Returns KPIs (revenue/tickets/events) with 30d vs prev-30d % change,
last 5 bookings, and next 5 upcoming events with capacity progress.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 11:50:07 +05:30
parent b6c2b93fd0
commit 8bc176b2f6
2 changed files with 134 additions and 0 deletions

View File

@@ -42,6 +42,8 @@ urlpatterns = [
path('partners/me/staff/<int:pk>/', views.PartnerMeStaffDetailView.as_view(), name='partner-me-staff-detail'), path('partners/me/staff/<int:pk>/', views.PartnerMeStaffDetailView.as_view(), name='partner-me-staff-detail'),
# Partner-Me: check-in (Sprint 7) # Partner-Me: check-in (Sprint 7)
path('partners/me/check-in/', views.PartnerMeCheckInView.as_view(), name='partner-me-check-in'), 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/metrics/', views.UserMetricsView.as_view(), name='user-metrics'),
path('users/', views.UserListView.as_view(), name='user-list'), path('users/', views.UserListView.as_view(), name='user-list'),
path('users/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'), path('users/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),

View File

@@ -4282,3 +4282,135 @@ class PartnerMeCheckInView(APIView):
'event': event.title if event else '', 'event': event.title if event else '',
'ticketId': ticket.ticket_id, '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: 3160 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,
})