update notification + lead and add lead tracking

This commit is contained in:
ismail 2025-05-11 19:23:54 +03:00
parent 03dd57d4d1
commit 993d4bc712
14 changed files with 1022 additions and 134 deletions

View File

@ -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),
),
]

View File

@ -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')),
],
),
]

View File

@ -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'),
),
]

View File

@ -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',
),
]

View File

@ -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'),
),
]

View File

@ -973,12 +973,11 @@ class Channel(models.TextChoices):
class Status(models.TextChoices): class Status(models.TextChoices):
NEW = "new", _("New") NEW = "new", _("New")
PENDING = "pending", _("Pending") FOLLOW_UP = "follow_up", _("Needs Follow-up")
IN_PROGRESS = "in_progress", _("In Progress") NEGOTIATION = "negotiation", _("Under Negotiation")
QUALIFIED = "qualified", _("Qualified") WON = "won", _("Converted")
CONTACTED = "contacted", _("Contacted") LOST = "lost", _("Lost")
CONVERTED = "converted", _("Converted") CLOSED = "closed", _("Closed")
CANCELED = "canceled", _("Canceled")
class Title(models.TextChoices): class Title(models.TextChoices):
@ -1000,6 +999,13 @@ class ActionChoices(models.TextChoices):
EMAIL = "email", _("Email") EMAIL = "email", _("Email")
WHATSAPP = "whatsapp", _("WhatsApp") WHATSAPP = "whatsapp", _("WhatsApp")
VISIT = "visit", _("Visit") 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") ADD_CAR = "add_car", _("Add Car")
SALE_CAR = "sale_car", _("Sale Car") SALE_CAR = "sale_car", _("Sale Car")
RESERVE_CAR = "reserve_car", _("Reserve Car") RESERVE_CAR = "reserve_car", _("Reserve Car")
@ -1324,6 +1330,20 @@ class Lead(models.Model):
db_index=True, db_index=True,
default=Status.NEW, 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( created = models.DateTimeField(
auto_now_add=True, verbose_name=_("Created"), db_index=True auto_now_add=True, verbose_name=_("Created"), db_index=True
) )
@ -1339,8 +1359,8 @@ class Lead(models.Model):
def get_user_model(self): def get_user_model(self):
return User.objects.get(email=self.email) or None return User.objects.get(email=self.email) or None
@property @property
def is_converted(self): def activities(self):
return bool(self.customer) return Activity.objects.filter(dealer=self.dealer, object_id=self.id)
def to_dict(self): def to_dict(self):
return { return {
@ -1360,6 +1380,34 @@ class Lead(models.Model):
self.status = Status.QUALIFIED self.status = Status.QUALIFIED
self.save() self.save()
return self.get_customer_model() 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): def get_customer_model(self):
if self.customer: if self.customer:
@ -1381,7 +1429,14 @@ class Lead(models.Model):
def get_notes(self): def get_notes(self):
return Notes.objects.filter(content_type__model="lead", object_id=self.pk) return Notes.objects.filter(content_type__model="lead", object_id=self.pk)
def get_activities(self): 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): class Schedule(models.Model):
PURPOSE_CHOICES = [ PURPOSE_CHOICES = [
@ -1576,6 +1631,14 @@ class Notification(models.Model):
def __str__(self): def __str__(self):
return self.message 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): class Vendor(models.Model, LocalizedNameMixin):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="vendors") dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="vendors")

View File

@ -99,6 +99,9 @@ urlpatterns = [
views.add_note_to_customer, views.add_note_to_customer,
name="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/", views.LeadListView.as_view(), name="lead_list"),
path( path(
"crm/leads/<int:pk>/view/", views.LeadDetailView.as_view(), name="lead_detail" "crm/leads/<int:pk>/view/", views.LeadDetailView.as_view(), name="lead_detail"
@ -184,6 +187,19 @@ urlpatterns = [
name="opportunity_update_status", name="opportunity_update_status",
), ),
# path('crm/opportunities/<int:pk>/logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'), # path('crm/opportunities/<int:pk>/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('<int:notification_id>/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( path(
"crm/notifications/", "crm/notifications/",
views.NotificationListView.as_view(), views.NotificationListView.as_view(),

View File

@ -2,7 +2,8 @@
import cv2 import cv2
import json import json
import logging import logging
import datetime from datetime import datetime
from time import sleep
import numpy as np import numpy as np
# from rich import print # from rich import print
from random import randint from random import randint
@ -11,12 +12,12 @@ from datetime import timedelta
from calendar import month_name from calendar import month_name
from pyzbar.pyzbar import decode from pyzbar.pyzbar import decode
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
##################################################################### #####################################################################
from inventory.models import Status as LeadStatus
from background_task.models import Task from background_task.models import Task
from django.db.models.deletion import RestrictedError from django.db.models.deletion import RestrictedError
from django.http.response import StreamingHttpResponse
# Django # Django
from django.db.models import Q from django.db.models import Q
from django.conf import settings from django.conf import settings
@ -4649,7 +4650,7 @@ def lead_create(request):
organization.create_customer_model() organization.create_customer_model()
organization.save() organization.save()
instance.organization = organization instance.organization = organization
instance.next_action = LeadStatus.FOLLOW_UP
instance.save() instance.save()
messages.success(request, _("Lead created successfully")) messages.success(request, _("Lead created successfully"))
return redirect("lead_list") return redirect("lead_list")
@ -4668,6 +4669,53 @@ def lead_create(request):
) )
return render(request, "crm/leads/lead_form.html", {"form": form}) 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): class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
""" """
@ -5342,49 +5390,49 @@ class NotificationListView(LoginRequiredMixin, ListView):
return models.Notification.objects.filter(user=self.request.user) return models.Notification.objects.filter(user=self.request.user)
@login_required # @login_required
def mark_notification_as_read(request, pk): # def mark_notification_as_read(request, pk):
""" # """
Marks a user notification as read. # Marks a user notification as read.
This view allows an authenticated user to mark a specific notification, # This view allows an authenticated user to mark a specific notification,
identified by its primary key, as read. Upon successfully marking the # identified by its primary key, as read. Upon successfully marking the
notification, a success message is displayed, and the user is redirected # notification, a success message is displayed, and the user is redirected
to their notification history page. # to their notification history page.
:param request: The HTTP request object. # :param request: The HTTP request object.
:type request: HttpRequest # :type request: HttpRequest
:param pk: Primary key of the notification to be marked as read. # :param pk: Primary key of the notification to be marked as read.
:type pk: int # :type pk: int
:return: An HTTP response redirecting to the notification history page. # :return: An HTTP response redirecting to the notification history page.
:rtype: HttpResponse # :rtype: HttpResponse
""" # """
notification = get_object_or_404(models.Notification, pk=pk, user=request.user) # notification = get_object_or_404(models.Notification, pk=pk, user=request.user)
notification.is_read = True # notification.is_read = True
notification.save() # notification.save()
messages.success(request, _("Notification marked as read")) # messages.success(request, _("Notification marked as read"))
return redirect("notifications_history") # return redirect("notifications_history")
@login_required # @login_required
def fetch_notifications(request): # def fetch_notifications(request):
""" # """
Fetches unread notifications for the currently logged-in user and renders them # Fetches unread notifications for the currently logged-in user and renders them
to the `notifications.html` template. The notifications are filtered to include # 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 # only those belonging to the logged-in user and are sorted by creation date in
descending order. # descending order.
:param request: The HTTP request object representing the current user request. # :param request: The HTTP request object representing the current user request.
Must include details of the logged-in user. # Must include details of the logged-in user.
:return: An HttpResponse object that renders the `notifications.html` template # :return: An HttpResponse object that renders the `notifications.html` template
with the fetched notifications. # with the fetched notifications.
""" # """
notifications = models.Notification.objects.filter( # notifications = models.Notification.objects.filter(
user=request.user, is_read=False # user=request.user, is_read=False
).order_by("-created") # ).order_by("-created")
return render(request, "notifications.html", {"notifications_": notifications}) # return render(request, "notifications.html", {"notifications_": notifications})
class ItemServiceCreateView( class ItemServiceCreateView(
@ -7597,3 +7645,62 @@ def task_list(request):
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
return render(request, 'tasks/task_list.html', {'page_obj': page_obj}) 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'})

View File

@ -3,39 +3,41 @@ import requests
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User 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 from plans.models import Order, PlanPricing,AbstractOrder
def run(): 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() user = User.objects.first()
order = Order.objects.create( print(Notification.get_notification_data(user))
user=user, # request = {
plan=pp.plan, # "csrfmiddlewaretoken": [
pricing=pp.pricing, # "mAnzSt7JjHkHGb27cyF1AiFvuVF7iKhONDVUzyzYuH1U0b7hxXL89D1UA4XQInuu"
amount=pp.price, # ],
currency="SAR", # "selected_plan": ["33"],
tax=15, # "first_name": ["ismail"],
status=AbstractOrder.STATUS.NEW # "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)

View File

@ -5,6 +5,62 @@
{% block content %} {% block content %}
<div class="row g-3"> <div class="row g-3">
<h2 class="mb-4">{{ _("Leads")|capfirst }}</h2> <h2 class="mb-4">{{ _("Leads")|capfirst }}</h2>
<!-- Action Tracking Modal -->
<div class="modal fade" id="actionTrackingModal" tabindex="-1" aria-labelledby="actionTrackingModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="actionTrackingModalLabel">{{ _("Update Lead Actions") }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="actionTrackingForm" method="post">
<div class="modal-body">
{% csrf_token %}
<input type="hidden" id="leadId" name="lead_id">
<div class="mb-3">
<label for="currentAction" class="form-label">{{ _("Current Action") }}</label>
<select class="form-select" id="currentAction" name="current_action" required>
<option value="">{{ _("Select Action") }}</option>
<option value="contacted">{{ _("Contacted") }}</option>
<option value="follow_up">{{ _("Follow Up") }}</option>
<option value="proposal_sent">{{ _("Proposal Sent") }}</option>
<option value="negotiation">{{ _("Negotiation") }}</option>
<option value="closed">{{ _("Closed") }}</option>
</select>
</div>
<div class="mb-3">
<label for="nextAction" class="form-label">{{ _("Next Action") }}</label>
<select class="form-select" id="nextAction" name="next_action" required>
<option value="">{{ _("Select Next Action") }}</option>
<option value="call">{{ _("Call") }}</option>
<option value="meeting">{{ _("Meeting") }}</option>
<option value="email">{{ _("Email") }}</option>
<option value="proposal">{{ _("Send Proposal") }}</option>
<option value="follow_up">{{ _("Follow Up") }}</option>
</select>
</div>
<div class="mb-3">
<label for="nextActionDate" class="form-label">{{ _("Next Action Date") }}</label>
<input type="datetime-local" class="form-control" id="nextActionDate" name="next_action_date" required>
</div>
<div class="mb-3">
<label for="actionNotes" class="form-label">{{ _("Notes") }}</label>
<textarea class="form-control" id="actionNotes" name="action_notes" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _("Close") }}</button>
<button type="submit" class="btn btn-primary">{{ _("Save Changes") }}</button>
</div>
</form>
</div>
</div>
</div>
<div class="row g-3 justify-content-between mb-4"> <div class="row g-3 justify-content-between mb-4">
<div class="col-auto"> <div class="col-auto">
<div class="d-md-flex justify-content-between"> <div class="d-md-flex justify-content-between">
@ -53,6 +109,24 @@
<span>{{ _("Schedule") }}</span> <span>{{ _("Schedule") }}</span>
</div> </div>
</th> </th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div>
<span>{{ _("Current Action")|capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div>
<span>{{ _("Next Action")|capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div>
<span>{{ _("Next Action Date")|capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;"> <th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center"> <div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div> <div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div>
@ -180,6 +254,9 @@
</div> </div>
</div> </div>
</td> </td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.get_status|upper }}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{% if lead.next_action %}{{ lead.next_action|upper }}{% endif %}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{% if lead.next_action %}{{ lead.next_action_date|upper }}{% endif %}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.staff|upper }}</td> <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.staff|upper }}</td>
<td class="align-middle white-space-nowrap fw-semibold text-body-highlight">{{ lead.source|upper }}</td> <td class="align-middle white-space-nowrap fw-semibold text-body-highlight">{{ lead.source|upper }}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.channel|upper }}</td> <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.channel|upper }}</td>
@ -219,19 +296,22 @@
<span class="fas fa-ellipsis-h fs-10"></span> <span class="fas fa-ellipsis-h fs-10"></span>
</button> </button>
<div class="dropdown-menu dropdown-menu-end py-2"> <div class="dropdown-menu dropdown-menu-end py-2">
{% if perms.inventory.change_lead %} {% if perms.inventory.change_lead %}
<a href="{% url 'lead_update' lead.id %}" class="dropdown-item text-success-dark">{% trans "Edit" %}</a> <a href="{% url 'lead_update' lead.id %}" class="dropdown-item text-success-dark">{% trans "Edit" %}</a>
{% endif %} {% endif %}
<a href="{% url 'send_lead_email' lead.id %}" class="dropdown-item text-success-dark">{% trans "Send Email" %}</a> <button class="dropdown-item text-primary" onclick="openActionModal('{{ lead.id }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')">
<a href="{% url 'schedule_lead' lead.id %}" class="dropdown-item text-success-dark">{% trans "Schedule Event" %}</a> {% trans "Update Actions" %}
{% if not lead.opportunity %} </button>
<a href="{% url 'lead_convert' lead.id %}" class="dropdown-item text-success-dark">{% trans "Convert" %}</a> <a href="{% url 'send_lead_email' lead.id %}" class="dropdown-item text-success-dark">{% trans "Send Email" %}</a>
{% endif %} <a href="{% url 'schedule_lead' lead.id %}" class="dropdown-item text-success-dark">{% trans "Schedule Event" %}</a>
<div class="dropdown-divider"></div> {% if not lead.opportunity %}
{% if perms.inventory.delete_lead %} <a href="{% url 'lead_convert' lead.id %}" class="dropdown-item text-success-dark">{% trans "Convert" %}</a>
<button class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">{% trans "Delete" %}</button> {% endif %}
{% endif %} <div class="dropdown-divider"></div>
</div> {% if perms.inventory.delete_lead %}
<button class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">{% trans "Delete" %}</button>
{% endif %}
</div>
</div> </div>
{% endif %} {% endif %}
</td> </td>
@ -287,3 +367,106 @@
{% endblock %} {% endblock %}
{% block customJS %}
<script>
// Initialize SweetAlert Toast for general messages
let Toast = Swal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
// Display Django messages
{% if messages %}
{% for message in messages %}
Toast.fire({
icon: "{{ message.tags }}",
titleText: "{{ message|safe }}"
});
{% endfor %}
{% endif %}
function openActionModal(leadId, currentAction, nextAction, nextActionDate) {
const modal = new bootstrap.Modal(document.getElementById('actionTrackingModal'));
document.getElementById('leadId').value = leadId;
document.getElementById('currentAction').value = currentAction;
document.getElementById('nextAction').value = nextAction;
document.getElementById('nextActionDate').value = nextActionDate;
modal.show();
}
document.getElementById('actionTrackingForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
// Show loading indicator
Swal.fire({
title: 'Updating Actions',
text: 'Please wait...',
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
}
});
fetch("{% url 'update_lead_actions' %}", {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token }}'
}
})
.then(response => response.json())
.then(data => {
Swal.close();
if (data.success) {
// Success notification
Swal.fire({
icon: 'success',
title: 'Success!',
text: data.message || 'Actions updated successfully',
confirmButtonText: 'OK',
timer: 3000,
timerProgressBar: true
}).then(() => {
location.reload(); // Refresh after user clicks OK
});
} else {
// Error notification
Swal.fire({
icon: 'error',
title: 'Error',
text: data.message || 'Failed to update actions',
confirmButtonText: 'OK'
});
}
})
.catch(error => {
Swal.close();
console.error('Error:', error);
Swal.fire({
icon: 'error',
title: 'Error',
text: 'An unexpected error occurred',
confirmButtonText: 'OK'
});
});
});
// Helper function for notifications
function notify(tag, msg) {
Toast.fire({
icon: tag,
titleText: msg
});
}
</script>
{% endblock customJS %}

View File

@ -0,0 +1,135 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block title %}{{ _('Leads')|capfirst }}{% endblock title %}
{% block customCSS %}
<style>
.kanban-column {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1rem;
min-height: 500px;
}
.kanban-header {
position: relative;
background-color:rgb(237, 241, 245);
font-weight: 600;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
color: #333;
clip-path: polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%);
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.kanban-header::after {
content: "";
position: absolute;
right: -20px;
top: 0;
width: 0;
height: 0;
border-top: 28px solid transparent;
border-bottom: 28px solid transparent;
border-left: 20px solid #dee2e6;
}
.lead-card {
background-color: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem;
margin-bottom: 1rem;
}
.lead-card small {
color: #6c757d;
}
</style>
{% endblock customCSS %}
{% block content %}
<div class="container-fluid my-4">
<div class="d-flex justify-content-between mb-3">
<h3>Lead Tracking</h3>
</div>
<div class="row g-3">
<!-- Column Template -->
<div class="col-md">
<div class="kanban-column">
<div class="kanban-header">New Leads ({{new|length}})</div>
{% for lead in new %}
<a href="{% url 'lead_detail' lead.id %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
<small>{{lead.phone_number}}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Follow Ups -->
<div class="col-md">
<div class="kanban-column">
<div class="kanban-header">Follow Ups ({{follow_up|length}})</div>
{% for lead in follow_up %}
<a href="{% url 'lead_detail' lead.id %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
<small>{{lead.phone_number}}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Under Review -->
<div class="col-md">
<div class="kanban-column">
<div class="kanban-header">Won ({{won|length}})</div>
{% for lead in won %}
<a href="{% url 'lead_detail' lead.id %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
<small>{{lead.phone_number}}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Demo -->
<div class="col-md">
<div class="kanban-column">
<div class="kanban-header">Lose ({{lose|length}})</div>
{% for lead in lose %}
<a href="{% url 'lead_detail' lead.id %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
<small>{{lead.phone_number}}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Negotiation -->
<div class="col-md">
<div class="kanban-column">
<div class="kanban-header">Negotiation ({{negotiation|length}})</div>
{% for lead in negotiation %}
<a href="{% url 'lead_detail' lead.id %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
<small>{{lead.phone_number}}</small>
</div>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -135,6 +135,13 @@
</div> </div>
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="{% url 'lead_tracking' %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span data-feather="users"></span></span><span class="nav-link-text">{% trans 'leads Tracking'|capfirst %}</span>
</div>
</a>
</li>
{% endif %} {% endif %}
{% if perms.django_ledger.view_customermodel %} {% if perms.django_ledger.view_customermodel %}
<li class="nav-item"> <li class="nav-item">

View File

@ -0,0 +1,50 @@
<li class="nav-item dropdown">
<div class="notification-count" hx-get="{% url 'fetch_notifications' %}" hx-trigger="every 20s" hx-swap="innerHTML" hx-select=".notification-count">
{% if notifications_ %}
<span class="badge bg-danger rounded-pill " id="notification-counter" style="position: absolute; top: 8px; right: 3px; font-size: 0.50rem;">{{ notifications_.count }}</span>
{% endif %}
</div>
<a class="nav-link" href="{% url 'fetch_notifications' %}" hx-get="{% url 'fetch_notifications' %}" hx-swap="innerHTML" hx-target=".card-body" hx-select=".card-body" style="min-width: 2.25rem" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-bs-auto-close="outside"><span class="d-block" style="height:20px;width:20px;"><span data-feather="bell" style="height:20px;width:20px;"></span></span></span>
</a>
<div class="dropdown-menu dropdown-menu-end notification-dropdown-menu py-0 shadow border navbar-dropdown-caret" id="navbarDropdownNotfication" aria-labelledby="navbarDropdownNotfication">
<div class="card position-relative border-0">
<div class="card-header p-2">
<div class="d-flex justify-content-between">
<h5 class="text-body-emphasis mb-0">{{ _("Notifications") }}</h5>
<button class="btn btn-link p-0 fs-9 fw-normal" type="button">{{ _("Mark all as read")}}</button>
</div>
</div>
<div class="card-body p-0">
<div class="scrollbar-overlay" style="height: 27rem;">
{% for notification in notifications_ %}
<div class="px-2 px-sm-3 py-3 notification-card position-relative read border-bottom">
<div class="d-flex align-items-center justify-content-between position-relative">
<div class="d-flex">
<div class="flex-1 me-sm-3">
<h4 class="fs-9 text-body-emphasis">System</h4>
<p class="fs-9 text-body-highlight mb-2 mb-sm-3 fw-normal">
<span class="me-1 fs-10">💬</span>{{notification.message|safe}}<span class="ms-2 text-body-quaternary text-opacity-75 fw-bold fs-10">10m</span>
</p>
<p class="text-body-secondary fs-9 mb-0">
<span class="me-1 fas fa-clock"></span><span class="fw-bold">10:41 AM</span>{{notification.created}}
</p>
</div>
</div>
<div class="dropdown notification-dropdown">
<button class="btn fs-10 btn-sm dropdown-toggle dropdown-caret-none transition-none" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10 text-body"></span></button>
<div class="dropdown-menu py-2">
<a class="dropdown-item" href="{% url 'mark_notification_as_read' notification.id %}">Mark as read</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="card-footer p-0 border-top border-translucent border-0">
<div class="my-2 text-center fw-bold fs-10 text-body-tertiary text-opactity-85"><a class="fw-bolder" href="{% url 'notifications_history' %}">Notification history</a></div>
</div>
</div>
</div>
</li>

View File

@ -1,50 +1,245 @@
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<div class="notification-count" hx-get="{% url 'fetch_notifications' %}" hx-trigger="every 20s" hx-swap="innerHTML" hx-select=".notification-count"> <!-- Notification counter -->
{% if notifications_ %} <div class="notification-count">
<span class="badge bg-danger rounded-pill " id="notification-counter" style="position: absolute; top: 8px; right: 3px; font-size: 0.50rem;">{{ notifications_.count }}</span> {% if notifications_ %}
{% endif %} <span class="badge bg-danger rounded-pill" id="notification-counter" style="position: absolute; top: 8px; right: 3px; font-size: 0.50rem;">{{ notifications_.count }}</span>
</div> {% else %}
<a class="nav-link" href="{% url 'fetch_notifications' %}" hx-get="{% url 'fetch_notifications' %}" hx-swap="innerHTML" hx-target=".card-body" hx-select=".card-body" style="min-width: 2.25rem" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-bs-auto-close="outside"><span class="d-block" style="height:20px;width:20px;"><span data-feather="bell" style="height:20px;width:20px;"></span></span></span> <span class="badge bg-danger rounded-pill d-none" id="notification-counter" style="position: absolute; top: 8px; right: 3px; font-size: 0.50rem;">0</span>
</a> {% endif %}
<div class="dropdown-menu dropdown-menu-end notification-dropdown-menu py-0 shadow border navbar-dropdown-caret" id="navbarDropdownNotfication" aria-labelledby="navbarDropdownNotfication"> </div>
<!-- Bell icon -->
<a class="nav-link" href="{% url 'fetch_notifications' %}" style="min-width: 2.25rem" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-bs-auto-close="outside">
<span class="d-block" style="height:20px;width:20px;">
<span data-feather="bell" style="height:20px;width:20px;"></span>
</span>
</a>
<!-- Dropdown menu -->
<div class="dropdown-menu dropdown-menu-end notification-dropdown-menu py-0 shadow border navbar-dropdown-caret" id="navbarDropdownNotfication">
<div class="card position-relative border-0"> <div class="card position-relative border-0">
<div class="card-header p-2"> <div class="card-header p-2">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<h5 class="text-body-emphasis mb-0">{{ _("Notifications") }}</h5> <h5 class="text-body-emphasis mb-0">{{ _("Notifications") }}</h5>
<button class="btn btn-link p-0 fs-9 fw-normal" type="button">{{ _("Mark all as read")}}</button> <button class="btn btn-link p-0 fs-9 fw-normal" type="button" id="mark-all-read">{{ _("Mark all as read")}}</button>
</div>
</div> </div>
</div> <div class="card-body p-0">
<div class="card-body p-0"> <div class="scrollbar-overlay" style="height: 27rem;" id="notifications-container">
<div class="scrollbar-overlay" style="height: 27rem;"> {% for notification in notifications_ %}
{% for notification in notifications_ %} <div class="px-2 px-sm-3 py-3 notification-card position-relative read border-bottom" data-notification-id="{{ notification.id }}">
<div class="px-2 px-sm-3 py-3 notification-card position-relative read border-bottom"> <!-- Notification content -->
</div>
{% endfor %}
</div>
</div>
<div class="card-footer p-0 border-top border-translucent border-0">
<div class="my-2 text-center fw-bold fs-10 text-body-tertiary text-opactity-85">
<a class="fw-bolder" href="{% url 'notifications_history' %}">Notification history</a>
</div>
</div>
</div>
</div>
</li>
<script>
document.addEventListener('DOMContentLoaded', function() {
let lastNotificationId = {{ notifications_.last.id|default:0 }};
let seenNotificationIds = new Set();
let counter = document.getElementById('notification-counter');
let notificationsContainer = document.getElementById('notifications-container');
let eventSource = null;
let initialUnreadCount = {{ notifications_.count|default:0 }};
updateCounter(initialUnreadCount);
fetchInitialNotifications();
function fetchInitialNotifications() {
fetch("{% url 'fetch_notifications' %}")
.then(response => response.json())
.then(data => {
if (data.notifications && data.notifications.length > 0) {
lastNotificationId = data.notifications[0].id;
seenNotificationIds = new Set();
let unreadCount = 0;
data.notifications.forEach(notification => {
seenNotificationIds.add(notification.id);
if (notification.unread) {
unreadCount++;
}
});
renderNotifications(data.notifications);
updateCounter(unreadCount);
connectSSE();
}
})
.catch(error => {
console.error('Error fetching initial notifications:', error);
connectSSE();
});
}
function connectSSE() {
if (eventSource) {
eventSource.close();
}
eventSource = new EventSource("{% url 'sse_stream' %}?last_id=" + lastNotificationId);
eventSource.addEventListener('notification', function(e) {
try {
const data = JSON.parse(e.data);
if (seenNotificationIds.has(data.id)) return;
seenNotificationIds.add(data.id);
if (data.id > lastNotificationId) {
lastNotificationId = data.id;
}
updateCounter('increment');
const notificationElement = createNotificationElement(data);
notificationsContainer.insertAdjacentHTML('afterbegin', notificationElement);
} catch (error) {
console.error('Error processing notification:', error);
}
});
eventSource.addEventListener('error', function(e) {
console.error('SSE connection error:', e);
setTimeout(connectSSE, 5000);
});
}
function renderNotifications(notifications) {
if (!notificationsContainer) return;
let html = '';
notifications.forEach(notification => {
html += createNotificationElement(notification);
});
notificationsContainer.innerHTML = html;
}
function createNotificationElement(data) {
const isRead = data.read ? 'read' : 'unread';
return `
<div class="px-2 px-sm-3 py-3 notification-card position-relative ${isRead} border-bottom"
data-notification-id="${data.id}">
<div class="d-flex align-items-center justify-content-between position-relative"> <div class="d-flex align-items-center justify-content-between position-relative">
<div class="d-flex"> <div class="d-flex">
<div class="flex-1 me-sm-3"> <div class="flex-1 me-sm-3">
<h4 class="fs-9 text-body-emphasis">System</h4> <h4 class="fs-9 text-body-emphasis">System</h4>
<p class="fs-9 text-body-highlight mb-2 mb-sm-3 fw-normal"> <p class="fs-9 text-body-highlight mb-2 mb-sm-3 fw-normal">
<span class="me-1 fs-10">💬</span>{{notification.message|safe}}<span class="ms-2 text-body-quaternary text-opacity-75 fw-bold fs-10">10m</span> <span class="me-1 fs-10">💬</span>${data.message}<span class="ms-2 text-body-quaternary text-opacity-75 fw-bold fs-10">Just now</span>
</p> </p>
<p class="text-body-secondary fs-9 mb-0"> <p class="text-body-secondary fs-9 mb-0">
<span class="me-1 fas fa-clock"></span><span class="fw-bold">10:41 AM</span>{{notification.created}} <span class="me-1 fas fa-clock"></span><span class="fw-bold">${new Date(data.created).toLocaleTimeString()}</span>
</p> </p>
</div> </div>
</div> </div>
<div class="dropdown notification-dropdown"> <div class="dropdown notification-dropdown">
<button class="btn fs-10 btn-sm dropdown-toggle dropdown-caret-none transition-none" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10 text-body"></span></button> <button class="btn fs-10 btn-sm dropdown-toggle dropdown-caret-none transition-none"
<div class="dropdown-menu py-2"> type="button" data-bs-toggle="dropdown">
<a class="dropdown-item" href="{% url 'mark_notification_as_read' notification.id %}">Mark as read</a> <span class="fas fa-ellipsis-h fs-10 text-body"></span>
</div> </button>
<div class="dropdown-menu py-2">
<a class="dropdown-item mark-as-read" href="#"
data-notification-id="${data.id}">Mark as read</a>
</div>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} `;
</div> }
</div>
<div class="card-footer p-0 border-top border-translucent border-0">
<div class="my-2 text-center fw-bold fs-10 text-body-tertiary text-opactity-85"><a class="fw-bolder" href="{% url 'notifications_history' %}">Notification history</a></div>
</div>
</div>
</div>
</li>
function updateCounter(action) {
if (!counter) {
counter = document.getElementById('notification-counter');
if (!counter) {
const notificationCountDiv = document.querySelector('.notification-count');
if (notificationCountDiv) {
notificationCountDiv.innerHTML = `
<span class="badge bg-danger rounded-pill" id="notification-counter"
style="position: absolute; top: 8px; right: 3px; font-size: 0.50rem;">0</span>
`;
counter = document.getElementById('notification-counter');
}
}
}
let currentCount = parseInt(counter.textContent) || 0;
if (action === 'increment') {
currentCount += 1;
} else if (action === 'decrement') {
currentCount = Math.max(0, currentCount - 1);
} else if (typeof action === 'number') {
currentCount = action;
}
counter.textContent = currentCount;
if (currentCount > 0) {
counter.classList.remove('d-none');
} else {
counter.classList.add('d-none');
}
}
document.getElementById('mark-all-read')?.addEventListener('click', function() {
fetch("{% url 'mark_all_notifications_as_read' %}", {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token }}',
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
updateCounter(0);
document.querySelectorAll('.notification-card').forEach(card => {
card.classList.remove('unread');
card.classList.add('read');
});
}
});
});
document.addEventListener('click', function(e) {
if (e.target.classList.contains('mark-as-read')) {
e.preventDefault();
const notificationId = e.target.getAttribute('data-notification-id');
fetch(`/notifications/${notificationId}/mark-read/`, {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token }}',
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
const notificationCard = document.querySelector(`[data-notification-id="${notificationId}"]`);
if (notificationCard) {
notificationCard.classList.remove('unread');
notificationCard.classList.add('read');
updateCounter('decrement');
}
}
});
}
});
});
</script>