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:
@@ -42,6 +42,8 @@ urlpatterns = [
|
||||
path('partners/me/staff/<int:pk>/', 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/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user