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