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'),
|
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'),
|
||||||
|
|||||||
@@ -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: 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