暂无描述

views_admin.py 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. from __future__ import annotations
  2. from django.shortcuts import render, get_object_or_404, redirect
  3. from django.contrib.admin.views.decorators import staff_member_required
  4. from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
  5. import django_filters as filters
  6. from django.contrib import messages
  7. from django.urls import reverse
  8. from django.contrib.auth import get_user_model
  9. from datetime import datetime
  10. from django.contrib.contenttypes.models import ContentType
  11. from admin_frontend.templatetags.public_urls import public_route
  12. from admin_frontend.nav import _nav_items
  13. from .models import Lead
  14. @staff_member_required
  15. @public_route(label="Leads", order=22, icon="envelope")
  16. def leads_list(request):
  17. """List Leads captured from the public site with filtering and pagination."""
  18. class LeadFilter(filters.FilterSet):
  19. name = filters.CharFilter(field_name="name", lookup_expr="icontains", label="Name")
  20. email = filters.CharFilter(field_name="email", lookup_expr="icontains", label="Email")
  21. phone = filters.CharFilter(field_name="phone", lookup_expr="icontains", label="Phone")
  22. org = filters.CharFilter(field_name="organization__code", lookup_expr="icontains", label="Org Code")
  23. source = filters.ChoiceFilter(field_name="source", label="Source", choices=Lead.LeadSource.choices)
  24. created_at = filters.DateTimeFromToRangeFilter(
  25. field_name="created_at",
  26. label="Created between",
  27. widget=filters.widgets.RangeWidget(
  28. attrs={
  29. "type": "datetime-local",
  30. "style": "color-scheme: light;",
  31. "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",
  32. }
  33. ),
  34. )
  35. class Meta:
  36. model = Lead
  37. fields = ["name", "email", "phone", "org", "source", "created_at"]
  38. def __init__(self, *args, **kwargs):
  39. super().__init__(*args, **kwargs)
  40. try:
  41. from crispy_forms.helper import FormHelper
  42. from crispy_forms.layout import Layout, Field
  43. helper = FormHelper()
  44. helper.form_tag = False
  45. helper.layout = Layout(
  46. Field("name", template="crispy/field_nowrap.html"),
  47. Field("email", template="crispy/field_nowrap.html"),
  48. Field("phone", template="crispy/field_nowrap.html"),
  49. Field("org", template="crispy/field_nowrap.html"),
  50. Field("source", template="crispy/field_nowrap.html"),
  51. Field("created_at", template="crispy/field_nowrap.html"),
  52. )
  53. self.form.helper = helper
  54. except Exception:
  55. pass
  56. qs = Lead.objects.select_related("organization").order_by("-created_at")
  57. lead_filter = LeadFilter(request.GET, queryset=qs)
  58. paginator = Paginator(lead_filter.qs, 10)
  59. page = request.GET.get("page")
  60. try:
  61. page_obj = paginator.page(page)
  62. except PageNotAnInteger:
  63. page_obj = paginator.page(1)
  64. except EmptyPage:
  65. page_obj = paginator.page(paginator.num_pages)
  66. context = {
  67. "nav": _nav_items(),
  68. "leads": page_obj.object_list,
  69. "page_obj": page_obj,
  70. "filter": lead_filter,
  71. }
  72. return render(request, "admin_frontend/leads_list.html", context)
  73. @staff_member_required
  74. def lead_detail(request, pk: int):
  75. lead = get_object_or_404(Lead.objects.select_related("organization"), pk=pk)
  76. def _parse_pickup_details(message: str):
  77. data = {"address": "", "preferred": "", "materials": ""}
  78. try:
  79. for line in (message or "").splitlines():
  80. if line.lower().startswith("address:"):
  81. data["address"] = line.split(":", 1)[1].strip()
  82. elif line.lower().startswith("preferred:"):
  83. data["preferred"] = line.split(":", 1)[1].strip()
  84. elif line.lower().startswith("materials:"):
  85. data["materials"] = line.split(":", 1)[1].strip()
  86. except Exception:
  87. pass
  88. return data
  89. suggested = _parse_pickup_details(getattr(lead, "message", "") or "")
  90. # Fetch any uploaded Documents attached to this Lead
  91. try:
  92. from recycle_core.models import Document
  93. ct = ContentType.objects.get_for_model(Lead)
  94. documents = Document.objects.filter(content_type=ct, object_id=lead.id).order_by("-created_at")
  95. except Exception:
  96. documents = []
  97. User = get_user_model()
  98. drivers = User.objects.all().order_by("username")
  99. context = {"nav": _nav_items(), "lead": lead, "drivers": drivers, "suggested": suggested, "documents": documents}
  100. return render(request, "admin_frontend/lead_detail.html", context)
  101. @staff_member_required
  102. def lead_convert_to_pickup(request, pk: int):
  103. lead = get_object_or_404(Lead.objects.select_related("organization"), pk=pk)
  104. if request.method != "POST":
  105. return redirect("public_frontend_admin:lead_detail", pk=pk)
  106. from recycle_core.models import Customer, CustomerSite, PickupOrder
  107. address = (request.POST.get("address") or "").strip()
  108. scheduled_at_raw = (request.POST.get("scheduled_at") or "").strip()
  109. driver_id = (request.POST.get("driver") or "").strip()
  110. if not address:
  111. messages.error(request, "Address is required to create a pickup.")
  112. return redirect("public_frontend_admin:lead_detail", pk=pk)
  113. scheduled_at = None
  114. if scheduled_at_raw:
  115. try:
  116. scheduled_at = datetime.fromisoformat(scheduled_at_raw)
  117. except Exception:
  118. messages.error(request, "Invalid date/time format for schedule.")
  119. return redirect("public_frontend_admin:lead_detail", pk=pk)
  120. # Find or create customer for this lead under the same organization
  121. org = lead.organization
  122. customer = None
  123. if lead.email:
  124. customer = Customer.objects.filter(organization=org, email__iexact=lead.email).first()
  125. if customer is None:
  126. customer = Customer.objects.filter(organization=org, name__iexact=lead.name).first()
  127. if customer is None:
  128. customer = Customer.objects.create(
  129. organization=org,
  130. name=lead.name,
  131. email=lead.email,
  132. phone=lead.phone,
  133. billing_address="",
  134. )
  135. # Create a site for the pickup
  136. site = CustomerSite.objects.create(
  137. customer=customer,
  138. name="",
  139. address=address,
  140. contact_name=lead.name,
  141. contact_phone=lead.phone,
  142. contact_email=lead.email,
  143. )
  144. pickup = PickupOrder.objects.create(
  145. organization=org,
  146. customer=customer,
  147. site=site,
  148. status=PickupOrder.STATUS_REQUESTED,
  149. scheduled_at=scheduled_at,
  150. )
  151. # Optionally assign a driver and mark scheduled
  152. if driver_id:
  153. try:
  154. User = get_user_model()
  155. driver = User.objects.get(pk=int(driver_id))
  156. pickup.assigned_driver = driver
  157. if scheduled_at:
  158. pickup.status = PickupOrder.STATUS_SCHEDULED
  159. pickup.save(update_fields=["assigned_driver", "status"])
  160. except Exception:
  161. pass
  162. messages.success(request, f"Created pickup #{pickup.id} for customer '{customer.name}'.")
  163. return redirect("recycle_core:pickups_list")