feat(leads): add Lead Manager module with full admin and consumer endpoints

- Lead model in admin_api with status/priority/source/assigned_to fields
- Admin API: metrics, list, detail, update views at /api/v1/leads/
- Consumer API: public ScheduleCallView at /api/leads/schedule-call/
- RBAC: 'leads' module registered in ALL_MODULES and StaffProfile scopes
- Migration 0003_lead with indexes on status, priority, created_at, email

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 10:48:04 +05:30
parent d74698f0b8
commit 2434805738
8 changed files with 441 additions and 3 deletions

View File

@@ -684,6 +684,7 @@ def _serialize_event(e):
'isFeatured': bool(e.is_featured),
'isTopEvent': bool(e.is_top_event),
'source': e.source or 'eventify',
'contributedBy': getattr(e, 'contributed_by', '') or '',
'eventTypeId': e.event_type_id,
'eventTypeName': e.event_type.event_type if e.event_type_id and e.event_type else '',
}
@@ -796,6 +797,7 @@ class EventUpdateView(APIView):
'pincode': 'pincode',
'importantInformation': 'important_information',
'source': 'source',
'contributedBy': 'contributed_by',
'cancelledReason': 'cancelled_reason',
'outsideEventUrl': 'outside_event_url',
}
@@ -2343,3 +2345,154 @@ class ShopRedeemView(APIView):
},
'message': 'Reward redeemed successfully!',
})
# ---------------------------------------------------------------------------
# Lead Manager
# ---------------------------------------------------------------------------
def _serialize_lead(lead):
assigned_name = ''
assigned_id = None
if lead.assigned_to:
assigned_name = lead.assigned_to.get_full_name() or lead.assigned_to.username
assigned_id = lead.assigned_to.pk
return {
'id': lead.pk,
'name': lead.name,
'email': lead.email,
'phone': lead.phone,
'eventType': lead.event_type,
'message': lead.message,
'status': lead.status,
'source': lead.source,
'priority': lead.priority,
'assignedTo': assigned_id,
'assignedToName': assigned_name,
'notes': lead.notes,
'createdAt': lead.created_at.isoformat(),
'updatedAt': lead.updated_at.isoformat(),
}
class LeadMetricsView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
from admin_api.models import Lead
from django.utils import timezone
today = timezone.now().date()
return Response({
'total': Lead.objects.count(),
'newToday': Lead.objects.filter(created_at__date=today).count(),
'new': Lead.objects.filter(status='new').count(),
'contacted': Lead.objects.filter(status='contacted').count(),
'qualified': Lead.objects.filter(status='qualified').count(),
'converted': Lead.objects.filter(status='converted').count(),
'closed': Lead.objects.filter(status='closed').count(),
})
class LeadListView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
from admin_api.models import Lead
from django.db.models import Q
qs = Lead.objects.select_related('assigned_to').order_by('-created_at')
# Filters
status_f = request.query_params.get('status', '').strip()
if status_f and status_f in dict(Lead.STATUS_CHOICES):
qs = qs.filter(status=status_f)
priority_f = request.query_params.get('priority', '').strip()
if priority_f and priority_f in dict(Lead.PRIORITY_CHOICES):
qs = qs.filter(priority=priority_f)
source_f = request.query_params.get('source', '').strip()
if source_f and source_f in dict(Lead.SOURCE_CHOICES):
qs = qs.filter(source=source_f)
search = request.query_params.get('search', '').strip()
if search:
qs = qs.filter(
Q(name__icontains=search) |
Q(email__icontains=search) |
Q(phone__icontains=search)
)
date_from = request.query_params.get('date_from', '').strip()
if date_from:
qs = qs.filter(created_at__date__gte=date_from)
date_to = request.query_params.get('date_to', '').strip()
if date_to:
qs = qs.filter(created_at__date__lte=date_to)
# Pagination
try:
page = max(1, int(request.query_params.get('page', 1)))
page_size = min(100, int(request.query_params.get('page_size', 20)))
except (ValueError, TypeError):
page, page_size = 1, 20
total = qs.count()
leads = qs[(page - 1) * page_size: page * page_size]
return Response({'count': total, 'results': [_serialize_lead(l) for l in leads]})
class LeadDetailView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, pk):
from admin_api.models import Lead
from django.shortcuts import get_object_or_404
lead = get_object_or_404(Lead.objects.select_related('assigned_to'), pk=pk)
return Response(_serialize_lead(lead))
class LeadUpdateView(APIView):
permission_classes = [IsAuthenticated]
def patch(self, request, pk):
from admin_api.models import Lead
from django.shortcuts import get_object_or_404
from eventify_logger.services import log
lead = get_object_or_404(Lead, pk=pk)
changed = []
new_status = request.data.get('status')
if new_status:
if new_status not in dict(Lead.STATUS_CHOICES):
return Response({'error': f'Invalid status: {new_status}'}, status=400)
lead.status = new_status
changed.append('status')
new_priority = request.data.get('priority')
if new_priority:
if new_priority not in dict(Lead.PRIORITY_CHOICES):
return Response({'error': f'Invalid priority: {new_priority}'}, status=400)
lead.priority = new_priority
changed.append('priority')
assigned_to_id = request.data.get('assignedTo')
if assigned_to_id is not None:
if assigned_to_id == '' or assigned_to_id is False:
lead.assigned_to = None
changed.append('assigned_to')
else:
try:
lead.assigned_to = User.objects.get(pk=int(assigned_to_id))
changed.append('assigned_to')
except (User.DoesNotExist, ValueError, TypeError):
return Response({'error': 'Invalid assignedTo user'}, status=400)
notes = request.data.get('notes')
if notes is not None:
lead.notes = notes
changed.append('notes')
lead.save()
log("info", f"Lead #{pk} updated: {', '.join(changed)}", request=request, user=request.user)
return Response(_serialize_lead(lead))