2026-03-24 14:46:03 +00:00
|
|
|
from django.contrib.auth import authenticate, get_user_model
|
|
|
|
|
from rest_framework.views import APIView
|
|
|
|
|
from rest_framework.response import Response
|
|
|
|
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
|
|
|
from rest_framework import status
|
|
|
|
|
from rest_framework_simplejwt.tokens import RefreshToken
|
|
|
|
|
from rest_framework_simplejwt.views import TokenRefreshView
|
|
|
|
|
from django.db import connection
|
|
|
|
|
from .serializers import UserSerializer
|
|
|
|
|
|
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
|
|
|
|
class AdminLoginView(APIView):
|
|
|
|
|
permission_classes = [AllowAny]
|
|
|
|
|
def post(self, request):
|
|
|
|
|
identifier = request.data.get('username') or request.data.get('email')
|
|
|
|
|
password = request.data.get('password')
|
|
|
|
|
if not identifier or not password:
|
|
|
|
|
return Response({'error': 'username/email and password required'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
# Try username first, then email
|
|
|
|
|
user = authenticate(request, username=identifier, password=password)
|
|
|
|
|
if not user:
|
|
|
|
|
try:
|
|
|
|
|
u = User.objects.get(email=identifier)
|
|
|
|
|
user = authenticate(request, username=u.username, password=password)
|
|
|
|
|
except User.DoesNotExist:
|
|
|
|
|
pass
|
|
|
|
|
if not user:
|
|
|
|
|
return Response({'error': 'Invalid credentials'}, status=status.HTTP_401_UNAUTHORIZED)
|
|
|
|
|
if not user.is_active:
|
|
|
|
|
return Response({'error': 'Account is disabled'}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
|
refresh = RefreshToken.for_user(user)
|
|
|
|
|
return Response({
|
|
|
|
|
'access': str(refresh.access_token),
|
|
|
|
|
'refresh': str(refresh),
|
|
|
|
|
'user': UserSerializer(user).data,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
class MeView(APIView):
|
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
|
def get(self, request):
|
|
|
|
|
return Response({'user': UserSerializer(request.user).data})
|
|
|
|
|
|
|
|
|
|
class HealthView(APIView):
|
|
|
|
|
permission_classes = [AllowAny]
|
|
|
|
|
def get(self, request):
|
|
|
|
|
try:
|
|
|
|
|
connection.ensure_connection()
|
|
|
|
|
db_status = 'ok'
|
|
|
|
|
except Exception:
|
|
|
|
|
db_status = 'error'
|
|
|
|
|
return Response({'status': 'ok', 'db': db_status})
|
feat: Phase 1+2 - JWT auth, dashboard metrics API, DB indexes
Phase 1 - JWT Auth Foundation:
- Replace token auth with djangorestframework-simplejwt
- POST /api/v1/admin/auth/login/ - returns access + refresh JWT
- POST /api/v1/auth/refresh/ - JWT refresh
- GET /api/v1/auth/me/ - current admin profile
- GET /api/v1/health/ - DB health check
- Add ledger app to INSTALLED_APPS
Phase 2 - Dashboard Metrics API:
- GET /api/v1/dashboard/metrics/ - revenue, partners, events, tickets
- GET /api/v1/dashboard/revenue/ - 7-day revenue vs payouts chart data
- GET /api/v1/dashboard/activity/ - last 10 platform events feed
- GET /api/v1/dashboard/actions/ - KYC queue, flagged events, pending payouts
DB Indexes (dashboard query optimisation):
- RazorpayTransaction: status, captured_at
- Partner: status, kyc_compliance_status
- Event: event_status, start_date, created_date
- Booking: created_date
- PaymentTransaction: payment_type, payment_transaction_status, payment_transaction_date
Infra:
- Add Dockerfile for eventify-backend container
- Add simplejwt to requirements.txt
- All 4 dashboard views use IsAuthenticated permission class
2026-03-24 17:46:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Phase 2: Dashboard Views
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class DashboardMetricsView(APIView):
|
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
|
|
|
|
|
|
def get(self, request):
|
|
|
|
|
from ledger.models import RazorpayTransaction
|
|
|
|
|
from partner.models import Partner
|
|
|
|
|
from events.models import Event
|
|
|
|
|
from bookings.models import Ticket, Booking
|
|
|
|
|
from django.db.models import Sum
|
|
|
|
|
from django.utils import timezone
|
|
|
|
|
import datetime
|
|
|
|
|
|
|
|
|
|
today = timezone.now().date()
|
|
|
|
|
|
|
|
|
|
# --- Revenue ---
|
|
|
|
|
total_paise = (
|
|
|
|
|
RazorpayTransaction.objects
|
|
|
|
|
.filter(status='captured')
|
|
|
|
|
.aggregate(total=Sum('amount'))['total'] or 0
|
|
|
|
|
)
|
|
|
|
|
total_revenue = total_paise / 100
|
|
|
|
|
|
|
|
|
|
# This-month / last-month revenue for growth
|
|
|
|
|
first_of_this_month = today.replace(day=1)
|
|
|
|
|
first_of_last_month = (first_of_this_month - datetime.timedelta(days=1)).replace(day=1)
|
|
|
|
|
|
|
|
|
|
this_month_paise = (
|
|
|
|
|
RazorpayTransaction.objects
|
|
|
|
|
.filter(status='captured', captured_at__date__gte=first_of_this_month)
|
|
|
|
|
.aggregate(total=Sum('amount'))['total'] or 0
|
|
|
|
|
)
|
|
|
|
|
last_month_paise = (
|
|
|
|
|
RazorpayTransaction.objects
|
|
|
|
|
.filter(
|
|
|
|
|
status='captured',
|
|
|
|
|
captured_at__date__gte=first_of_last_month,
|
|
|
|
|
captured_at__date__lt=first_of_this_month,
|
|
|
|
|
)
|
|
|
|
|
.aggregate(total=Sum('amount'))['total'] or 0
|
|
|
|
|
)
|
|
|
|
|
this_month_rev = this_month_paise / 100
|
|
|
|
|
last_month_rev = last_month_paise / 100
|
|
|
|
|
revenue_growth = (
|
|
|
|
|
round((this_month_rev - last_month_rev) / last_month_rev * 100, 2)
|
|
|
|
|
if last_month_rev else 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# --- Partners ---
|
|
|
|
|
active_partners = Partner.objects.filter(status='active').count()
|
|
|
|
|
pending_partners = Partner.objects.filter(kyc_compliance_status='pending').count()
|
|
|
|
|
|
|
|
|
|
# --- Events ---
|
|
|
|
|
live_events = Event.objects.filter(event_status='live').count()
|
|
|
|
|
events_today = Event.objects.filter(start_date=today).count()
|
|
|
|
|
|
|
|
|
|
# --- Tickets ---
|
|
|
|
|
ticket_sales = Ticket.objects.count()
|
|
|
|
|
|
|
|
|
|
# This-week / last-week ticket growth
|
|
|
|
|
week_start = today - datetime.timedelta(days=today.weekday()) # Monday
|
|
|
|
|
last_week_start = week_start - datetime.timedelta(days=7)
|
|
|
|
|
|
|
|
|
|
this_week_tickets = Ticket.objects.filter(
|
|
|
|
|
booking__created_date__gte=week_start
|
|
|
|
|
).count()
|
|
|
|
|
last_week_tickets = Ticket.objects.filter(
|
|
|
|
|
booking__created_date__gte=last_week_start,
|
|
|
|
|
booking__created_date__lt=week_start,
|
|
|
|
|
).count()
|
|
|
|
|
ticket_growth = (
|
|
|
|
|
round((this_week_tickets - last_week_tickets) / last_week_tickets * 100, 2)
|
|
|
|
|
if last_week_tickets else 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return Response({
|
|
|
|
|
'totalRevenue': total_revenue,
|
|
|
|
|
'revenueGrowth': revenue_growth,
|
|
|
|
|
'activePartners': active_partners,
|
|
|
|
|
'pendingPartners': pending_partners,
|
|
|
|
|
'liveEvents': live_events,
|
|
|
|
|
'eventsToday': events_today,
|
|
|
|
|
'ticketSales': ticket_sales,
|
|
|
|
|
'ticketGrowth': ticket_growth,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DashboardRevenueView(APIView):
|
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
|
|
|
|
|
|
def get(self, request):
|
|
|
|
|
from ledger.models import RazorpayTransaction
|
|
|
|
|
from banking_operations.models import PaymentTransaction
|
|
|
|
|
from django.db.models import Sum
|
|
|
|
|
from django.utils import timezone
|
|
|
|
|
import datetime
|
|
|
|
|
|
|
|
|
|
today = timezone.now().date()
|
|
|
|
|
result = []
|
|
|
|
|
|
|
|
|
|
for i in range(6, -1, -1):
|
|
|
|
|
day = today - datetime.timedelta(days=i)
|
|
|
|
|
|
|
|
|
|
rev_paise = (
|
|
|
|
|
RazorpayTransaction.objects
|
|
|
|
|
.filter(status='captured', captured_at__date=day)
|
|
|
|
|
.aggregate(total=Sum('amount'))['total'] or 0
|
|
|
|
|
)
|
|
|
|
|
revenue = rev_paise / 100
|
|
|
|
|
|
|
|
|
|
payouts = (
|
|
|
|
|
PaymentTransaction.objects
|
|
|
|
|
.filter(
|
|
|
|
|
payment_type='debit',
|
|
|
|
|
payment_transaction_status='completed',
|
|
|
|
|
payment_transaction_date=day,
|
|
|
|
|
)
|
|
|
|
|
.aggregate(total=Sum('payment_transaction_amount'))['total'] or 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result.append({
|
|
|
|
|
'day': day.strftime('%a'),
|
|
|
|
|
'revenue': float(revenue),
|
|
|
|
|
'payouts': float(payouts),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return Response(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DashboardActivityView(APIView):
|
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
|
|
|
|
|
|
def get(self, request):
|
|
|
|
|
from partner.models import Partner
|
|
|
|
|
from events.models import Event
|
|
|
|
|
from bookings.models import Booking
|
|
|
|
|
from django.utils import timezone
|
|
|
|
|
|
|
|
|
|
items = []
|
|
|
|
|
|
|
|
|
|
# --- Partners (last 5 by id desc; no date field — timestamp=None, filtered out later) ---
|
|
|
|
|
for p in Partner.objects.order_by('-id')[:5]:
|
|
|
|
|
items.append({
|
|
|
|
|
'id': f'partner-{p.id}',
|
|
|
|
|
'type': 'partner',
|
|
|
|
|
'title': f'{p.name} registered',
|
|
|
|
|
'description': f'New partner — {getattr(p, "partner_type", "individual")}',
|
|
|
|
|
'timestamp': None,
|
|
|
|
|
'status': p.kyc_compliance_status,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# --- Events (last 5 by created_date desc) ---
|
|
|
|
|
for e in Event.objects.order_by('-created_date')[:5]:
|
|
|
|
|
display_name = e.title if getattr(e, 'title', None) else getattr(e, 'name', '')
|
|
|
|
|
ts = e.created_date
|
|
|
|
|
items.append({
|
|
|
|
|
'id': f'event-{e.id}',
|
|
|
|
|
'type': 'event',
|
|
|
|
|
'title': f'{display_name} created',
|
|
|
|
|
'description': f'Event status: {e.event_status}',
|
|
|
|
|
'timestamp': ts.isoformat() if ts else None,
|
|
|
|
|
'status': e.event_status,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# --- Bookings (last 5 by id desc) ---
|
|
|
|
|
for b in Booking.objects.order_by('-id')[:5]:
|
|
|
|
|
ts = b.created_date
|
|
|
|
|
items.append({
|
|
|
|
|
'id': f'booking-{b.booking_id}',
|
|
|
|
|
'type': 'booking',
|
|
|
|
|
'title': f'Booking {b.booking_id} placed',
|
|
|
|
|
'description': f'{b.quantity} ticket(s) at ₹{b.price}',
|
|
|
|
|
'timestamp': ts.isoformat() if ts else None,
|
|
|
|
|
'status': 'confirmed',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Filter out items with no timestamp, sort desc, return top 10
|
|
|
|
|
dated = [item for item in items if item['timestamp'] is not None]
|
|
|
|
|
dated.sort(key=lambda x: x['timestamp'], reverse=True)
|
|
|
|
|
|
|
|
|
|
return Response(dated[:10])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DashboardActionsView(APIView):
|
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
|
|
|
|
|
|
def get(self, request):
|
|
|
|
|
from partner.models import Partner
|
|
|
|
|
from events.models import Event
|
|
|
|
|
from banking_operations.models import PaymentTransaction
|
|
|
|
|
from django.db.models import Sum
|
|
|
|
|
|
|
|
|
|
kyc_count = Partner.objects.filter(kyc_compliance_status='pending').count()
|
|
|
|
|
flagged_count = Event.objects.filter(event_status='flagged').count()
|
|
|
|
|
pending_payouts = float(
|
|
|
|
|
PaymentTransaction.objects
|
|
|
|
|
.filter(payment_type='debit', payment_transaction_status='pending')
|
|
|
|
|
.aggregate(total=Sum('payment_transaction_amount'))['total'] or 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
actions = [
|
|
|
|
|
{
|
|
|
|
|
'id': 'kyc',
|
|
|
|
|
'type': 'kyc',
|
|
|
|
|
'count': kyc_count,
|
|
|
|
|
'title': 'Partner Approval Queue',
|
|
|
|
|
'description': f'{kyc_count} partners awaiting KYC review',
|
|
|
|
|
'href': '/partners?filter=pending',
|
|
|
|
|
'priority': 'high',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
'id': 'flagged',
|
|
|
|
|
'type': 'flagged',
|
|
|
|
|
'count': flagged_count,
|
|
|
|
|
'title': 'Flagged Events',
|
|
|
|
|
'description': f'{flagged_count} events reported for review',
|
|
|
|
|
'href': '/events?filter=flagged',
|
|
|
|
|
'priority': 'high',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
'id': 'payout',
|
|
|
|
|
'type': 'payout',
|
|
|
|
|
'count': pending_payouts,
|
|
|
|
|
'title': 'Pending Payouts',
|
|
|
|
|
'description': f'₹{pending_payouts:,.0f} ready for release',
|
|
|
|
|
'href': '/financials?tab=payouts',
|
|
|
|
|
'priority': 'medium',
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return Response(actions)
|