from __future__ import annotations from django.shortcuts import render, get_object_or_404, redirect from django.contrib.admin.views.decorators import staff_member_required from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger import django_filters as filters from django.contrib import messages from django.urls import reverse from django.contrib.auth import get_user_model from datetime import datetime from django.contrib.contenttypes.models import ContentType from admin_frontend.templatetags.public_urls import public_route from admin_frontend.nav import _nav_items from .models import Lead @staff_member_required @public_route(label="Leads", order=22, icon="envelope") def leads_list(request): """List Leads captured from the public site with filtering and pagination.""" class LeadFilter(filters.FilterSet): name = filters.CharFilter(field_name="name", lookup_expr="icontains", label="Name") email = filters.CharFilter(field_name="email", lookup_expr="icontains", label="Email") phone = filters.CharFilter(field_name="phone", lookup_expr="icontains", label="Phone") org = filters.CharFilter(field_name="organization__code", lookup_expr="icontains", label="Org Code") source = filters.ChoiceFilter(field_name="source", label="Source", choices=Lead.LeadSource.choices) created_at = filters.DateTimeFromToRangeFilter( field_name="created_at", label="Created between", widget=filters.widgets.RangeWidget( attrs={ "type": "datetime-local", "style": "color-scheme: light;", "class": "border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500", } ), ) class Meta: model = Lead fields = ["name", "email", "phone", "org", "source", "created_at"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) try: from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Field helper = FormHelper() helper.form_tag = False helper.layout = Layout( Field("name", template="crispy/field_nowrap.html"), Field("email", template="crispy/field_nowrap.html"), Field("phone", template="crispy/field_nowrap.html"), Field("org", template="crispy/field_nowrap.html"), Field("source", template="crispy/field_nowrap.html"), Field("created_at", template="crispy/field_nowrap.html"), ) self.form.helper = helper except Exception: pass qs = Lead.objects.select_related("organization").order_by("-created_at") lead_filter = LeadFilter(request.GET, queryset=qs) paginator = Paginator(lead_filter.qs, 10) page = request.GET.get("page") try: page_obj = paginator.page(page) except PageNotAnInteger: page_obj = paginator.page(1) except EmptyPage: page_obj = paginator.page(paginator.num_pages) context = { "nav": _nav_items(), "leads": page_obj.object_list, "page_obj": page_obj, "filter": lead_filter, } return render(request, "admin_frontend/leads_list.html", context) @staff_member_required def lead_detail(request, pk: int): lead = get_object_or_404(Lead.objects.select_related("organization"), pk=pk) def _parse_pickup_details(message: str): data = {"address": "", "preferred": "", "materials": ""} try: for line in (message or "").splitlines(): if line.lower().startswith("address:"): data["address"] = line.split(":", 1)[1].strip() elif line.lower().startswith("preferred:"): data["preferred"] = line.split(":", 1)[1].strip() elif line.lower().startswith("materials:"): data["materials"] = line.split(":", 1)[1].strip() except Exception: pass return data suggested = _parse_pickup_details(getattr(lead, "message", "") or "") # Fetch any uploaded Documents attached to this Lead try: from recycle_core.models import Document ct = ContentType.objects.get_for_model(Lead) documents = Document.objects.filter(content_type=ct, object_id=lead.id).order_by("-created_at") except Exception: documents = [] User = get_user_model() drivers = User.objects.all().order_by("username") context = {"nav": _nav_items(), "lead": lead, "drivers": drivers, "suggested": suggested, "documents": documents} return render(request, "admin_frontend/lead_detail.html", context) @staff_member_required def lead_convert_to_pickup(request, pk: int): lead = get_object_or_404(Lead.objects.select_related("organization"), pk=pk) if request.method != "POST": return redirect("public_frontend_admin:lead_detail", pk=pk) from recycle_core.models import Customer, CustomerSite, PickupOrder address = (request.POST.get("address") or "").strip() scheduled_at_raw = (request.POST.get("scheduled_at") or "").strip() driver_id = (request.POST.get("driver") or "").strip() if not address: messages.error(request, "Address is required to create a pickup.") return redirect("public_frontend_admin:lead_detail", pk=pk) scheduled_at = None if scheduled_at_raw: try: scheduled_at = datetime.fromisoformat(scheduled_at_raw) except Exception: messages.error(request, "Invalid date/time format for schedule.") return redirect("public_frontend_admin:lead_detail", pk=pk) # Find or create customer for this lead under the same organization org = lead.organization customer = None if lead.email: customer = Customer.objects.filter(organization=org, email__iexact=lead.email).first() if customer is None: customer = Customer.objects.filter(organization=org, name__iexact=lead.name).first() if customer is None: customer = Customer.objects.create( organization=org, name=lead.name, email=lead.email, phone=lead.phone, billing_address="", ) # Create a site for the pickup site = CustomerSite.objects.create( customer=customer, name="", address=address, contact_name=lead.name, contact_phone=lead.phone, contact_email=lead.email, ) pickup = PickupOrder.objects.create( organization=org, customer=customer, site=site, status=PickupOrder.STATUS_REQUESTED, scheduled_at=scheduled_at, ) # Optionally assign a driver and mark scheduled if driver_id: try: User = get_user_model() driver = User.objects.get(pk=int(driver_id)) pickup.assigned_driver = driver if scheduled_at: pickup.status = PickupOrder.STATUS_SCHEDULED pickup.save(update_fields=["assigned_driver", "status"]) except Exception: pass messages.success(request, f"Created pickup #{pickup.id} for customer '{customer.name}'.") return redirect("recycle_core:pickups_list")