update notification + lead and add lead tracking
This commit is contained in:
parent
03dd57d4d1
commit
993d4bc712
18
inventory/migrations/0013_lead_converted_at.py
Normal file
18
inventory/migrations/0013_lead_converted_at.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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',
|
||||||
|
),
|
||||||
|
]
|
||||||
18
inventory/migrations/0017_alter_activity_activity_type.py
Normal file
18
inventory/migrations/0017_alter_activity_activity_type.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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")
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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'})
|
||||||
|
|||||||
62
scripts/r.py
62
scripts/r.py
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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 %}
|
||||||
135
templates/crm/leads/lead_tracking.html
Normal file
135
templates/crm/leads/lead_tracking.html
Normal 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 %}
|
||||||
@ -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">
|
||||||
|
|||||||
50
templates/notifications-copy.html
Normal file
50
templates/notifications-copy.html
Normal 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>
|
||||||
|
|
||||||
@ -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>
|
||||||
Loading…
x
Reference in New Issue
Block a user