From 993d4bc7123fb72db12bc4bc286ece7427e5a4f1 Mon Sep 17 00:00:00 2001 From: ismail <=> Date: Sun, 11 May 2025 19:23:54 +0300 Subject: [PATCH] update notification + lead and add lead tracking --- .../migrations/0013_lead_converted_at.py | 18 ++ ...is_converted_alter_lead_status_and_more.py | 50 ++++ ..._lead_next_action_lead_next_action_date.py | 23 ++ ...ivity_activity_type_delete_leadactivity.py | 21 ++ .../0017_alter_activity_activity_type.py | 18 ++ inventory/models.py | 81 ++++- inventory/urls.py | 16 + inventory/views.py | 189 +++++++++--- scripts/r.py | 62 ++-- templates/crm/leads/lead_list.html | 209 ++++++++++++- templates/crm/leads/lead_tracking.html | 135 +++++++++ templates/header.html | 7 + templates/notifications-copy.html | 50 ++++ templates/notifications.html | 277 +++++++++++++++--- 14 files changed, 1022 insertions(+), 134 deletions(-) create mode 100644 inventory/migrations/0013_lead_converted_at.py create mode 100644 inventory/migrations/0014_lead_is_converted_alter_lead_status_and_more.py create mode 100644 inventory/migrations/0015_lead_next_action_lead_next_action_date.py create mode 100644 inventory/migrations/0016_alter_activity_activity_type_delete_leadactivity.py create mode 100644 inventory/migrations/0017_alter_activity_activity_type.py create mode 100644 templates/crm/leads/lead_tracking.html create mode 100644 templates/notifications-copy.html diff --git a/inventory/migrations/0013_lead_converted_at.py b/inventory/migrations/0013_lead_converted_at.py new file mode 100644 index 00000000..07376e8d --- /dev/null +++ b/inventory/migrations/0013_lead_converted_at.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-05-11 14:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0012_alter_customer_dob_alter_customer_national_id'), + ] + + operations = [ + migrations.AddField( + model_name='lead', + name='converted_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/inventory/migrations/0014_lead_is_converted_alter_lead_status_and_more.py b/inventory/migrations/0014_lead_is_converted_alter_lead_status_and_more.py new file mode 100644 index 00000000..c74834a2 --- /dev/null +++ b/inventory/migrations/0014_lead_is_converted_alter_lead_status_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 5.1.7 on 2025-05-11 14:26 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0013_lead_converted_at'), + ] + + operations = [ + migrations.AddField( + model_name='lead', + name='is_converted', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='lead', + name='status', + field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Needs Follow-up'), ('negotiation', 'Under Negotiation'), ('won', 'Converted'), ('lost', 'Lost'), ('closed', 'Closed')], db_index=True, default='new', max_length=50, verbose_name='Status'), + ), + migrations.AlterField( + model_name='leadstatushistory', + name='new_status', + field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Needs Follow-up'), ('negotiation', 'Under Negotiation'), ('won', 'Converted'), ('lost', 'Lost'), ('closed', 'Closed')], max_length=50, verbose_name='New Status'), + ), + migrations.AlterField( + model_name='leadstatushistory', + name='old_status', + field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Needs Follow-up'), ('negotiation', 'Under Negotiation'), ('won', 'Converted'), ('lost', 'Lost'), ('closed', 'Closed')], max_length=50, verbose_name='Old Status'), + ), + migrations.AlterField( + model_name='opportunity', + name='status', + field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Needs Follow-up'), ('negotiation', 'Under Negotiation'), ('won', 'Converted'), ('lost', 'Lost'), ('closed', 'Closed')], default='new', max_length=20, verbose_name='Status'), + ), + migrations.CreateModel( + name='LeadActivity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('action', models.CharField(max_length=255)), + ('notes', models.TextField(blank=True)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.staff')), + ('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='inventory.lead')), + ], + ), + ] diff --git a/inventory/migrations/0015_lead_next_action_lead_next_action_date.py b/inventory/migrations/0015_lead_next_action_lead_next_action_date.py new file mode 100644 index 00000000..e16fac75 --- /dev/null +++ b/inventory/migrations/0015_lead_next_action_lead_next_action_date.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.7 on 2025-05-11 14:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0014_lead_is_converted_alter_lead_status_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='lead', + name='next_action', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Next Action'), + ), + migrations.AddField( + model_name='lead', + name='next_action_date', + field=models.DateTimeField(blank=True, null=True, verbose_name='Next Action Date'), + ), + ] diff --git a/inventory/migrations/0016_alter_activity_activity_type_delete_leadactivity.py b/inventory/migrations/0016_alter_activity_activity_type_delete_leadactivity.py new file mode 100644 index 00000000..8375c340 --- /dev/null +++ b/inventory/migrations/0016_alter_activity_activity_type_delete_leadactivity.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.7 on 2025-05-11 15:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0015_lead_next_action_lead_next_action_date'), + ] + + operations = [ + migrations.AlterField( + model_name='activity', + name='activity_type', + field=models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed'), ('converted', 'Converted'), ('transfer', 'Transfer'), ('add_car', 'Add Car'), ('sale_car', 'Sale Car'), ('reserve_car', 'Reserve Car'), ('transfer_car', 'Transfer Car'), ('remove_car', 'Remove Car'), ('create_quotation', 'Create Quotation'), ('cancel_quotation', 'Cancel Quotation'), ('create_order', 'Create Order'), ('cancel_order', 'Cancel Order'), ('create_invoice', 'Create Invoice'), ('cancel_invoice', 'Cancel Invoice')], max_length=50, verbose_name='Activity Type'), + ), + migrations.DeleteModel( + name='LeadActivity', + ), + ] diff --git a/inventory/migrations/0017_alter_activity_activity_type.py b/inventory/migrations/0017_alter_activity_activity_type.py new file mode 100644 index 00000000..cfda3309 --- /dev/null +++ b/inventory/migrations/0017_alter_activity_activity_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-05-11 15:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0016_alter_activity_activity_type_delete_leadactivity'), + ] + + operations = [ + migrations.AlterField( + model_name='activity', + name='activity_type', + field=models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('negotiation', 'Negotiation'), ('follow_up', 'Follow Up'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed'), ('converted', 'Converted'), ('transfer', 'Transfer'), ('add_car', 'Add Car'), ('sale_car', 'Sale Car'), ('reserve_car', 'Reserve Car'), ('transfer_car', 'Transfer Car'), ('remove_car', 'Remove Car'), ('create_quotation', 'Create Quotation'), ('cancel_quotation', 'Cancel Quotation'), ('create_order', 'Create Order'), ('cancel_order', 'Cancel Order'), ('create_invoice', 'Create Invoice'), ('cancel_invoice', 'Cancel Invoice')], max_length=50, verbose_name='Activity Type'), + ), + ] diff --git a/inventory/models.py b/inventory/models.py index 5ec5c4ea..9ef68e2c 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -973,12 +973,11 @@ class Channel(models.TextChoices): class Status(models.TextChoices): NEW = "new", _("New") - PENDING = "pending", _("Pending") - IN_PROGRESS = "in_progress", _("In Progress") - QUALIFIED = "qualified", _("Qualified") - CONTACTED = "contacted", _("Contacted") - CONVERTED = "converted", _("Converted") - CANCELED = "canceled", _("Canceled") + FOLLOW_UP = "follow_up", _("Needs Follow-up") + NEGOTIATION = "negotiation", _("Under Negotiation") + WON = "won", _("Converted") + LOST = "lost", _("Lost") + CLOSED = "closed", _("Closed") class Title(models.TextChoices): @@ -1000,6 +999,13 @@ class ActionChoices(models.TextChoices): EMAIL = "email", _("Email") WHATSAPP = "whatsapp", _("WhatsApp") VISIT = "visit", _("Visit") + LEAD_NEGOTIATION = "negotiation", _("Negotiation") + LEAD_FOLLOW_UP = "follow_up", _("Follow Up") + LEAD_WON = "won", _("Won") + LEAD_LOST = "lost", _("Lost") + LEAD_CLOSED = "closed", _("Closed") + LEAD_CONVERTED = "converted", _("Converted") + LEAD_TRANSFER = "transfer", _("Transfer") ADD_CAR = "add_car", _("Add Car") SALE_CAR = "sale_car", _("Sale Car") RESERVE_CAR = "reserve_car", _("Reserve Car") @@ -1324,6 +1330,20 @@ class Lead(models.Model): db_index=True, default=Status.NEW, ) + next_action = models.CharField( + max_length=255, + verbose_name=_("Next Action"), + blank=True, + null=True + ) + + next_action_date = models.DateTimeField( + verbose_name=_("Next Action Date"), + blank=True, + null=True + ) + is_converted = models.BooleanField(default=False) + converted_at = models.DateTimeField(null=True, blank=True) created = models.DateTimeField( auto_now_add=True, verbose_name=_("Created"), db_index=True ) @@ -1339,8 +1359,8 @@ class Lead(models.Model): def get_user_model(self): return User.objects.get(email=self.email) or None @property - def is_converted(self): - return bool(self.customer) + def activities(self): + return Activity.objects.filter(dealer=self.dealer, object_id=self.id) def to_dict(self): return { @@ -1360,6 +1380,34 @@ class Lead(models.Model): self.status = Status.QUALIFIED self.save() return self.get_customer_model() + def get_status(self): + if self.is_converted: + return Status.WON + + latest_activity = self.activities.order_by('-updated').first() + if latest_activity: + time_since_last = timezone.now() - latest_activity.updated + if "negotiation" in latest_activity.activity_type.lower(): + return Status.NEGOTIATION + elif time_since_last > timedelta(days=3): + return Status.FOLLOW_UP + else: + return Status.NEW + + return self.status + + @property + def needs_follow_up(self): + latest = self.activities.order_by('-updated').first() + if not latest: + return True + return (timezone.now() - latest.updated).days > 3 + @property + def stale_leads(self): + latest = self.activities.order_by('-updated').first() + if not latest: + return True + return (timezone.now() - latest.updated).days > 7 def get_customer_model(self): if self.customer: @@ -1381,7 +1429,14 @@ class Lead(models.Model): def get_notes(self): return Notes.objects.filter(content_type__model="lead", object_id=self.pk) def get_activities(self): - return Activity.objects.filter(content_type__model="lead", object_id=self.pk) + return Activity.objects.filter(dealer=self.dealer,content_type__model="lead", object_id=self.pk).order_by('-updated') + @property + def get_current_action(self): + return Activity.objects.filter(dealer=self.dealer,content_type__model="lead", object_id=self.pk).order_by('-updated').first() + + def save(self, *args, **kwargs): + self.status = self.get_status() + super().save(*args, **kwargs) class Schedule(models.Model): PURPOSE_CHOICES = [ @@ -1576,6 +1631,14 @@ class Notification(models.Model): def __str__(self): return self.message + @classmethod + def has_new_notifications(cls, user): + return cls.objects.filter(user=user,is_read=False).exists() + + @classmethod + def get_notification_data(cls, user): + return cls.objects.filter(user=user) + class Vendor(models.Model, LocalizedNameMixin): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="vendors") diff --git a/inventory/urls.py b/inventory/urls.py index c3f82d69..4d0a7a85 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -99,6 +99,9 @@ urlpatterns = [ views.add_note_to_customer, name="add_note_to_customer", ), + path('update-lead-actions/', views.update_lead_actions, name='update_lead_actions'), + path('crm/leads/lead_tracking/', views.lead_tracking, name='lead_tracking'), + path("crm/leads/", views.LeadListView.as_view(), name="lead_list"), path( "crm/leads//view/", views.LeadDetailView.as_view(), name="lead_detail" @@ -184,6 +187,19 @@ urlpatterns = [ name="opportunity_update_status", ), # path('crm/opportunities//logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'), + # ####################### + path('stream/', views.sse_stream, name='sse_stream'), + path('fetch/', views.fetch_notifications, name='fetch_notifications'), + + # Mark single notification as read + path('/mark-read/', views.mark_notification_as_read, name='mark_notification_as_read'), + + # Mark all notifications as read + path('mark-all-read/', views.mark_all_notifications_as_read, name='mark_all_notifications_as_read'), + + # Notification history + path('history/', views.notifications_history, name='notifications_history'), + # ####################### path( "crm/notifications/", views.NotificationListView.as_view(), diff --git a/inventory/views.py b/inventory/views.py index 288d4dca..a56acefb 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -2,7 +2,8 @@ import cv2 import json import logging -import datetime +from datetime import datetime +from time import sleep import numpy as np # from rich import print from random import randint @@ -11,12 +12,12 @@ from datetime import timedelta from calendar import month_name from pyzbar.pyzbar import decode from urllib.parse import urlparse, urlunparse - ##################################################################### +from inventory.models import Status as LeadStatus from background_task.models import Task from django.db.models.deletion import RestrictedError - +from django.http.response import StreamingHttpResponse # Django from django.db.models import Q from django.conf import settings @@ -4649,7 +4650,7 @@ def lead_create(request): organization.create_customer_model() organization.save() instance.organization = organization - + instance.next_action = LeadStatus.FOLLOW_UP instance.save() messages.success(request, _("Lead created successfully")) return redirect("lead_list") @@ -4668,6 +4669,53 @@ def lead_create(request): ) return render(request, "crm/leads/lead_form.html", {"form": form}) +def lead_tracking(request): + dealer = get_user_type(request) + new = models.Lead.objects.filter(dealer=dealer) + follow_up = models.Lead.objects.filter(dealer=dealer, next_action__in=["call", "meeting"]) + won = models.Lead.objects.filter(dealer=dealer, status="won") + lose = models.Lead.objects.filter(dealer=dealer, status="lose") + negotiation = models.Lead.objects.filter(dealer=dealer, status="negotiation") + context = {"new": new,"follow_up": follow_up,"won": won,"lose": lose,"negotiation": negotiation} + return render(request, "crm/leads/lead_tracking.html", context) + +# @require_POST +def update_lead_actions(request): + try: + lead_id = request.POST.get('lead_id') + current_action = request.POST.get('current_action') + next_action = request.POST.get('next_action') + next_action_date = request.POST.get('next_action_date') + action_notes = request.POST.get('action_notes', '') + + # Validate required fields + if not all([lead_id, current_action, next_action, next_action_date]): + return JsonResponse({'success': False, 'message': 'All fields are required'}, status=400) + + # Get the lead + lead = models.Lead.objects.get(id=lead_id) + + # Update lead fields + lead.action = current_action + lead.next_action = next_action + lead.next_action_date = next_action_date + + # Parse the datetime string + try: + next_action_datetime = datetime.strptime(next_action_date, '%Y-%m-%dT%H:%M') + lead.next_action_date = timezone.make_aware(next_action_datetime) + except ValueError: + return JsonResponse({'success': False, 'message': 'Invalid date format'}, status=400) + + # Save the lead + lead.save() + + return JsonResponse({'success': True, 'message': 'Actions updated successfully'}) + + except models.Lead.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Lead not found'}, status=404) + except Exception as e: + return JsonResponse({'success': False, 'message': str(e)}, status=500) class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ @@ -5342,49 +5390,49 @@ class NotificationListView(LoginRequiredMixin, ListView): return models.Notification.objects.filter(user=self.request.user) -@login_required -def mark_notification_as_read(request, pk): - """ - Marks a user notification as read. +# @login_required +# def mark_notification_as_read(request, pk): +# """ +# Marks a user notification as read. - This view allows an authenticated user to mark a specific notification, - identified by its primary key, as read. Upon successfully marking the - notification, a success message is displayed, and the user is redirected - to their notification history page. +# This view allows an authenticated user to mark a specific notification, +# identified by its primary key, as read. Upon successfully marking the +# notification, a success message is displayed, and the user is redirected +# to their notification history page. - :param request: The HTTP request object. - :type request: HttpRequest - :param pk: Primary key of the notification to be marked as read. - :type pk: int - :return: An HTTP response redirecting to the notification history page. - :rtype: HttpResponse - """ - notification = get_object_or_404(models.Notification, pk=pk, user=request.user) - notification.is_read = True - notification.save() - messages.success(request, _("Notification marked as read")) - return redirect("notifications_history") +# :param request: The HTTP request object. +# :type request: HttpRequest +# :param pk: Primary key of the notification to be marked as read. +# :type pk: int +# :return: An HTTP response redirecting to the notification history page. +# :rtype: HttpResponse +# """ +# notification = get_object_or_404(models.Notification, pk=pk, user=request.user) +# notification.is_read = True +# notification.save() +# messages.success(request, _("Notification marked as read")) +# return redirect("notifications_history") -@login_required -def fetch_notifications(request): - """ - Fetches unread notifications for the currently logged-in user and renders them - to the `notifications.html` template. The notifications are filtered to include - only those belonging to the logged-in user and are sorted by creation date in - descending order. +# @login_required +# def fetch_notifications(request): +# """ +# Fetches unread notifications for the currently logged-in user and renders them +# to the `notifications.html` template. The notifications are filtered to include +# only those belonging to the logged-in user and are sorted by creation date in +# descending order. - :param request: The HTTP request object representing the current user request. - Must include details of the logged-in user. - :return: An HttpResponse object that renders the `notifications.html` template - with the fetched notifications. +# :param request: The HTTP request object representing the current user request. +# Must include details of the logged-in user. +# :return: An HttpResponse object that renders the `notifications.html` template +# with the fetched notifications. - """ - notifications = models.Notification.objects.filter( - user=request.user, is_read=False - ).order_by("-created") +# """ +# notifications = models.Notification.objects.filter( +# user=request.user, is_read=False +# ).order_by("-created") - return render(request, "notifications.html", {"notifications_": notifications}) +# return render(request, "notifications.html", {"notifications_": notifications}) class ItemServiceCreateView( @@ -7596,4 +7644,63 @@ def task_list(request): page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) - return render(request, 'tasks/task_list.html', {'page_obj': page_obj}) \ No newline at end of file + return render(request, 'tasks/task_list.html', {'page_obj': page_obj}) + +def sse_stream(request): + def event_stream(): + last_id = request.GET.get('last_id', 0) + while True: + # Check for new notifications + notifications = models.Notification.objects.filter( + user=request.user, + id__gt=last_id, + is_read=False + ).order_by('created') + for notification in notifications: + notification_data = { + 'id': notification.id, + 'message': notification.message, + 'created': notification.created.isoformat(), + } + + yield ( + f"id: {notification.id}\n" + f"event: notification\n" + f"data: {json.dumps(notification_data)}\n\n" + ) + last_id = notification.id + + sleep(2) + response = StreamingHttpResponse( + event_stream(), + content_type='text/event-stream' + ) + response['Cache-Control'] = 'no-cache' + return response + + +@login_required +def fetch_notifications(request): + notifications = models.Notification.objects.filter( + user=request.user, + is_read=False + ).order_by('-created')[:10] # Get 10 most recent + + return JsonResponse({'notifications': list(notifications.values())}) + +@login_required +def mark_notification_as_read(request, notification_id): + notification = get_object_or_404(models.Notification, id=notification_id, user=request.user) + notification.read = True + notification.save() + return JsonResponse({'status': 'success'}) + +@login_required +def mark_all_notifications_as_read(request): + models.Notification.objects.filter(user=request.user, is_read=False).update(read=True) + return JsonResponse({'status': 'success'}) + +@login_required +def notifications_history(request): + models.Notification.objects.filter(user=request.user, is_read=False).update(read=True) + return JsonResponse({'status': 'success'}) diff --git a/scripts/r.py b/scripts/r.py index 3e3f03fc..24e25071 100644 --- a/scripts/r.py +++ b/scripts/r.py @@ -3,39 +3,41 @@ import requests from django.urls import reverse from django.conf import settings from django.contrib.auth.models import User -from inventory.models import PaymentHistory +from inventory.models import PaymentHistory,Notification from plans.models import Order, PlanPricing,AbstractOrder def run(): - request = { - "csrfmiddlewaretoken": [ - "mAnzSt7JjHkHGb27cyF1AiFvuVF7iKhONDVUzyzYuH1U0b7hxXL89D1UA4XQInuu" - ], - "selected_plan": ["33"], - "first_name": ["ismail"], - "last_name": ["mosa"], - "email": ["ismail.mosa.ibrahim@gmail.com"], - "phone": ["0566703794"], - "company": ["Tenhal"], - "card_name": ["ppppppppppp"], - "card_number": ["4111 1111 1111 1111"], - "card_expiry": ["08/28"], - "card_cvv": ["123"], - } - - selected_plan_id = request.get("selected_plan")[0] - - pp = PlanPricing.objects.get(pk=selected_plan_id) user = User.objects.first() - order = Order.objects.create( - user=user, - plan=pp.plan, - pricing=pp.pricing, - amount=pp.price, - currency="SAR", - tax=15, - status=AbstractOrder.STATUS.NEW - ) + print(Notification.get_notification_data(user)) + # request = { + # "csrfmiddlewaretoken": [ + # "mAnzSt7JjHkHGb27cyF1AiFvuVF7iKhONDVUzyzYuH1U0b7hxXL89D1UA4XQInuu" + # ], + # "selected_plan": ["33"], + # "first_name": ["ismail"], + # "last_name": ["mosa"], + # "email": ["ismail.mosa.ibrahim@gmail.com"], + # "phone": ["0566703794"], + # "company": ["Tenhal"], + # "card_name": ["ppppppppppp"], + # "card_number": ["4111 1111 1111 1111"], + # "card_expiry": ["08/28"], + # "card_cvv": ["123"], + # } - handle_payment(request,order) + # selected_plan_id = request.get("selected_plan")[0] + + # pp = PlanPricing.objects.get(pk=selected_plan_id) + # user = User.objects.first() + # order = Order.objects.create( + # user=user, + # plan=pp.plan, + # pricing=pp.pricing, + # amount=pp.price, + # currency="SAR", + # tax=15, + # status=AbstractOrder.STATUS.NEW + # ) + + # handle_payment(request,order) diff --git a/templates/crm/leads/lead_list.html b/templates/crm/leads/lead_list.html index 34cd1475..93d880d1 100644 --- a/templates/crm/leads/lead_list.html +++ b/templates/crm/leads/lead_list.html @@ -5,6 +5,62 @@ {% block content %}

{{ _("Leads")|capfirst }}

+ + +
@@ -53,6 +109,24 @@ {{ _("Schedule") }}
+ +
+
+ {{ _("Current Action")|capfirst }} +
+ + +
+
+ {{ _("Next Action")|capfirst }} +
+ + +
+
+ {{ _("Next Action Date")|capfirst }} +
+
@@ -180,6 +254,9 @@
+ {{ lead.get_status|upper }} + {% if lead.next_action %}{{ lead.next_action|upper }}{% endif %} + {% if lead.next_action %}{{ lead.next_action_date|upper }}{% endif %} {{ lead.staff|upper }} {{ lead.source|upper }} {{ lead.channel|upper }} @@ -219,19 +296,22 @@ + {% if perms.inventory.change_lead %} + {% trans "Edit" %} + {% endif %} + + {% trans "Send Email" %} + {% trans "Schedule Event" %} + {% if not lead.opportunity %} + {% trans "Convert" %} + {% endif %} + + {% if perms.inventory.delete_lead %} + + {% endif %} +
{% endif %} @@ -287,3 +367,106 @@ {% endblock %} + +{% block customJS %} + + +{% endblock customJS %} \ No newline at end of file diff --git a/templates/crm/leads/lead_tracking.html b/templates/crm/leads/lead_tracking.html new file mode 100644 index 00000000..f2c02ed9 --- /dev/null +++ b/templates/crm/leads/lead_tracking.html @@ -0,0 +1,135 @@ +{% extends 'base.html' %} +{% load i18n static %} +{% block title %}{{ _('Leads')|capfirst }}{% endblock title %} + +{% block customCSS %} + +{% endblock customCSS %} +{% block content %} +
+
+

Lead Tracking

+
+ +
+ +
+
+
New Leads ({{new|length}})
+ {% for lead in new %} + +
+ {{lead.full_name|capfirst}}
+ {{lead.email}}
+ {{lead.phone_number}} +
+
+ {% endfor %} +
+
+ + +
+
+
Follow Ups ({{follow_up|length}})
+ {% for lead in follow_up %} + +
+ {{lead.full_name|capfirst}}
+ {{lead.email}}
+ {{lead.phone_number}} +
+
+ {% endfor %} +
+
+ + +
+
+
Won ({{won|length}})
+ {% for lead in won %} + +
+ {{lead.full_name|capfirst}}
+ {{lead.email}}
+ {{lead.phone_number}} +
+
+ {% endfor %} +
+
+ + +
+
+
Lose ({{lose|length}})
+ {% for lead in lose %} + +
+ {{lead.full_name|capfirst}}
+ {{lead.email}}
+ {{lead.phone_number}} +
+
+ {% endfor %} +
+
+ + +
+
+
Negotiation ({{negotiation|length}})
+ {% for lead in negotiation %} + +
+ {{lead.full_name|capfirst}}
+ {{lead.email}}
+ {{lead.phone_number}} +
+
+ {% endfor %} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/header.html b/templates/header.html index 1280a869..a6e1754e 100644 --- a/templates/header.html +++ b/templates/header.html @@ -135,6 +135,13 @@ + {% endif %} {% if perms.django_ledger.view_customermodel %} + \ No newline at end of file diff --git a/templates/notifications.html b/templates/notifications.html index 5bf7e6b0..bfc09ef9 100644 --- a/templates/notifications.html +++ b/templates/notifications.html @@ -1,50 +1,245 @@ + + \ No newline at end of file