emile 10 months ago
parent c19a9d7bf8
commit 03f40cb8db

Binary file not shown.

@ -20,12 +20,20 @@ class TicketRoomConsumer(WebsocketConsumer):
self.ticket = get_object_or_404(Ticket, id=self.ticket_id)
self.ticket_number = self.ticket.ticket_number
existing_connection = TicketConnection.objects.filter(ticket=self.ticket, user=self.user, terminated=False).delete()
TicketConnection.objects.create(
ticket=self.ticket,
user=self.user,
type='Online',
date=datetime.now()
)
staff_profile = StaffProfile.objects.filter(user=self.user).first()
if staff_profile:
if not TicketStaff.objects.filter(staff=staff_profile, ticket=self.ticket).exists():
TicketStaff.objects.create(
staff=staff_profile,
ticket=self.ticket,
date_added=datetime.now()
)
async_to_sync(self.channel_layer.group_add)(
self.ticket_number, self.channel_name
@ -38,8 +46,7 @@ class TicketRoomConsumer(WebsocketConsumer):
TicketConnection.objects.filter(
ticket=self.ticket,
user=self.user,
type='Online'
).delete()
).update(terminated=True)
async_to_sync(self.channel_layer.group_discard)(
self.ticket_number, self.channel_name
)
@ -164,7 +171,7 @@ class TicketRoomConsumer(WebsocketConsumer):
}))
def modify_online_user(self):
connections = TicketConnection.objects.filter(ticket=self.ticket, type='Online')
connections = TicketConnection.objects.filter(ticket=self.ticket, terminated=False)
event = {
'type': 'user_connection_handler',
'user': self.user,

@ -0,0 +1,22 @@
# Generated by Django 4.2.5 on 2024-07-03 06:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('support', '0005_alter_ticketconnection_ticket'),
]
operations = [
migrations.RemoveField(
model_name='ticketconnection',
name='type',
),
migrations.AddField(
model_name='ticketconnection',
name='terminated',
field=models.BooleanField(default=False),
),
]

@ -0,0 +1,20 @@
# Generated by Django 4.2.5 on 2024-07-03 07:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('osinacore', '0085_rename_date_staffposition_start_date_and_more'),
('support', '0006_remove_ticketconnection_type_and_more'),
]
operations = [
migrations.AlterField(
model_name='ticketstaff',
name='staff',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='osinacore.staffprofile'),
),
]

@ -0,0 +1,19 @@
# Generated by Django 4.2.5 on 2024-07-03 07:50
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('support', '0007_alter_ticketstaff_staff'),
]
operations = [
migrations.AlterField(
model_name='tickettask',
name='ticket',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='support.ticket'),
),
]

@ -9,6 +9,7 @@ from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from threading import Timer
from datetime import time
def send_ticket(ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id)
@ -74,10 +75,6 @@ def send_update(update_id):
# Create your models here.
class Ticket(models.Model):
@ -109,6 +106,30 @@ class Ticket(models.Model):
super().save(*args, **kwargs)
def get_customer_last_seen(self):
connections = TicketConnection.objects.filter(user=self.customer.user).order_by('-date')
if not connections.exists():
return "Not seen yet"
last_connection = connections.first()
if not last_connection.terminated:
return "Online"
last_seen_time = last_connection.date
now = timezone.now()
time_diff = now - last_seen_time
if time_diff < timedelta(days=1):
if last_seen_time.date() == now.date():
return f"last seen today at {last_seen_time.strftime('%I:%M %p')}"
else:
return f"last seen yesterday at {last_seen_time.strftime('%I:%M %p')}"
else:
return f"last seen on {last_seen_time.strftime('%b %d at %I:%M %p')}"
@receiver(post_save, sender=Ticket)
def send_signal_on_save(sender, instance, created, **kwargs):
@ -127,9 +148,32 @@ class TicketDepartment(models.Model):
class TicketStaff(models.Model):
staff = models.ForeignKey(StaffPosition, on_delete=models.CASCADE)
staff = models.ForeignKey(StaffProfile, on_delete=models.CASCADE)
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
date_added = models.DateTimeField()
def get_last_seen(self):
connections = TicketConnection.objects.filter(user=self.staff.user).order_by('-date')
if not connections.exists():
return "Not seen yet"
last_connection = connections.first()
if not last_connection.terminated:
return "Online"
last_seen_time = last_connection.date
now = timezone.now()
time_diff = now - last_seen_time
if time_diff < timedelta(days=1):
if last_seen_time.date() == now.date():
return f"last seen today at {last_seen_time.strftime('%I:%M %p')}"
else:
return f"last seen yesterday at {last_seen_time.strftime('%I:%M %p')}"
else:
return f"last seen on {last_seen_time.strftime('%b %d at %I:%M %p')}"
class TicketStatus(models.Model):
STATUS_CHOICES = (
@ -143,6 +187,20 @@ class TicketStatus(models.Model):
date_added = models.DateTimeField()
class TicketTask(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
task = models.ForeignKey(Task, on_delete=models.CASCADE)
class TicketConnection(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
date = models.DateTimeField()
terminated = models.BooleanField(default=False)
class TicketUpdate(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
description = models.TextField(null=True, blank=True)
@ -180,19 +238,3 @@ class TicketUpdateReaction(models.Model):
reaction = models.CharField(max_length=50, choices=REACTION_CHOICES, null=True)
ticket_update = models.ForeignKey(TicketUpdate, on_delete=models.CASCADE)
class TicketTask(models.Model):
ticket = models.ForeignKey(TicketUpdate, on_delete=models.CASCADE)
task = models.ForeignKey(Task, on_delete=models.CASCADE)
class TicketConnection(models.Model):
CONNECTION_CHOICES = (
('Online', 'Online'),
('Offline', 'Offline'),
)
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
type = models.CharField(max_length=50, choices=CONNECTION_CHOICES, null=True)
date = models.DateTimeField()

@ -26,20 +26,23 @@
<div class="w-full s:w-fit flex flex-col s:flex-row justify-end items-center gap-2">
<a href="{% url 'ticketsettings' %}" class="w-full s:w-fit">
<button
class="w-full s:w-fit px-3 py-2 bg-transparent border border-osiblue text-osiblue cursor-pointer duration-300 hover:bg-osiblue hover:text-white rounded-md flex justify-center items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-5" fill="none">
<path
d="M21.3175 7.14139L20.8239 6.28479C20.4506 5.63696 20.264 5.31305 19.9464 5.18388C19.6288 5.05472 19.2696 5.15664 18.5513 5.36048L17.3311 5.70418C16.8725 5.80994 16.3913 5.74994 15.9726 5.53479L15.6357 5.34042C15.2766 5.11043 15.0004 4.77133 14.8475 4.37274L14.5136 3.37536C14.294 2.71534 14.1842 2.38533 13.9228 2.19657C13.6615 2.00781 13.3143 2.00781 12.6199 2.00781H11.5051C10.8108 2.00781 10.4636 2.00781 10.2022 2.19657C9.94085 2.38533 9.83106 2.71534 9.61149 3.37536L9.27753 4.37274C9.12465 4.77133 8.84845 5.11043 8.48937 5.34042L8.15249 5.53479C7.73374 5.74994 7.25259 5.80994 6.79398 5.70418L5.57375 5.36048C4.85541 5.15664 4.49625 5.05472 4.17867 5.18388C3.86109 5.31305 3.67445 5.63696 3.30115 6.28479L2.80757 7.14139C2.45766 7.74864 2.2827 8.05227 2.31666 8.37549C2.35061 8.69871 2.58483 8.95918 3.05326 9.48012L4.0843 10.6328C4.3363 10.9518 4.51521 11.5078 4.51521 12.0077C4.51521 12.5078 4.33636 13.0636 4.08433 13.3827L3.05326 14.5354C2.58483 15.0564 2.35062 15.3168 2.31666 15.6401C2.2827 15.9633 2.45766 16.2669 2.80757 16.8741L3.30114 17.7307C3.67443 18.3785 3.86109 18.7025 4.17867 18.8316C4.49625 18.9608 4.85542 18.8589 5.57377 18.655L6.79394 18.3113C7.25263 18.2055 7.73387 18.2656 8.15267 18.4808L8.4895 18.6752C8.84851 18.9052 9.12464 19.2442 9.2775 19.6428L9.61149 20.6403C9.83106 21.3003 9.94085 21.6303 10.2022 21.8191C10.4636 22.0078 10.8108 22.0078 11.5051 22.0078H12.6199C13.3143 22.0078 13.6615 22.0078 13.9228 21.8191C14.1842 21.6303 14.294 21.3003 14.5136 20.6403L14.8476 19.6428C15.0004 19.2442 15.2765 18.9052 15.6356 18.6752L15.9724 18.4808C16.3912 18.2656 16.8724 18.2055 17.3311 18.3113L18.5513 18.655C19.2696 18.8589 19.6288 18.9608 19.9464 18.8316C20.264 18.7025 20.4506 18.3785 20.8239 17.7307L21.3175 16.8741C21.6674 16.2669 21.8423 15.9633 21.8084 15.6401C21.7744 15.3168 21.5402 15.0564 21.0718 14.5354L20.0407 13.3827C19.7887 13.0636 19.6098 12.5078 19.6098 12.0077C19.6098 11.5078 19.7888 10.9518 20.0407 10.6328L21.0718 9.48012C21.5402 8.95918 21.7744 8.69871 21.8084 8.37549C21.8423 8.05227 21.6674 7.74864 21.3175 7.14139Z"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path
d="M15.5195 12C15.5195 13.933 13.9525 15.5 12.0195 15.5C10.0865 15.5 8.51953 13.933 8.51953 12C8.51953 10.067 10.0865 8.5 12.0195 8.5C13.9525 8.5 15.5195 10.067 15.5195 12Z"
stroke="currentColor" stroke-width="1.5" />
</svg>
Settings
</button>
</a>
{% if request.user.staffprofile %}
<a href="{% url 'ticketsettings' ticket.ticket_number %}" class="w-full s:w-fit">
<button
class="w-full s:w-fit px-3 py-2 bg-transparent border border-osiblue text-osiblue cursor-pointer duration-300 hover:bg-osiblue hover:text-white rounded-md flex justify-center items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-5" fill="none">
<path
d="M21.3175 7.14139L20.8239 6.28479C20.4506 5.63696 20.264 5.31305 19.9464 5.18388C19.6288 5.05472 19.2696 5.15664 18.5513 5.36048L17.3311 5.70418C16.8725 5.80994 16.3913 5.74994 15.9726 5.53479L15.6357 5.34042C15.2766 5.11043 15.0004 4.77133 14.8475 4.37274L14.5136 3.37536C14.294 2.71534 14.1842 2.38533 13.9228 2.19657C13.6615 2.00781 13.3143 2.00781 12.6199 2.00781H11.5051C10.8108 2.00781 10.4636 2.00781 10.2022 2.19657C9.94085 2.38533 9.83106 2.71534 9.61149 3.37536L9.27753 4.37274C9.12465 4.77133 8.84845 5.11043 8.48937 5.34042L8.15249 5.53479C7.73374 5.74994 7.25259 5.80994 6.79398 5.70418L5.57375 5.36048C4.85541 5.15664 4.49625 5.05472 4.17867 5.18388C3.86109 5.31305 3.67445 5.63696 3.30115 6.28479L2.80757 7.14139C2.45766 7.74864 2.2827 8.05227 2.31666 8.37549C2.35061 8.69871 2.58483 8.95918 3.05326 9.48012L4.0843 10.6328C4.3363 10.9518 4.51521 11.5078 4.51521 12.0077C4.51521 12.5078 4.33636 13.0636 4.08433 13.3827L3.05326 14.5354C2.58483 15.0564 2.35062 15.3168 2.31666 15.6401C2.2827 15.9633 2.45766 16.2669 2.80757 16.8741L3.30114 17.7307C3.67443 18.3785 3.86109 18.7025 4.17867 18.8316C4.49625 18.9608 4.85542 18.8589 5.57377 18.655L6.79394 18.3113C7.25263 18.2055 7.73387 18.2656 8.15267 18.4808L8.4895 18.6752C8.84851 18.9052 9.12464 19.2442 9.2775 19.6428L9.61149 20.6403C9.83106 21.3003 9.94085 21.6303 10.2022 21.8191C10.4636 22.0078 10.8108 22.0078 11.5051 22.0078H12.6199C13.3143 22.0078 13.6615 22.0078 13.9228 21.8191C14.1842 21.6303 14.294 21.3003 14.5136 20.6403L14.8476 19.6428C15.0004 19.2442 15.2765 18.9052 15.6356 18.6752L15.9724 18.4808C16.3912 18.2656 16.8724 18.2055 17.3311 18.3113L18.5513 18.655C19.2696 18.8589 19.6288 18.9608 19.9464 18.8316C20.264 18.7025 20.4506 18.3785 20.8239 17.7307L21.3175 16.8741C21.6674 16.2669 21.8423 15.9633 21.8084 15.6401C21.7744 15.3168 21.5402 15.0564 21.0718 14.5354L20.0407 13.3827C19.7887 13.0636 19.6098 12.5078 19.6098 12.0077C19.6098 11.5078 19.7888 10.9518 20.0407 10.6328L21.0718 9.48012C21.5402 8.95918 21.7744 8.69871 21.8084 8.37549C21.8423 8.05227 21.6674 7.74864 21.3175 7.14139Z"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path
d="M15.5195 12C15.5195 13.933 13.9525 15.5 12.0195 15.5C10.0865 15.5 8.51953 13.933 8.51953 12C8.51953 10.067 10.0865 8.5 12.0195 8.5C13.9525 8.5 15.5195 10.067 15.5195 12Z"
stroke="currentColor" stroke-width="1.5" />
</svg>
Settings
</button>
</a>
{% endif %}
<button

@ -25,26 +25,14 @@
<div class="w-full flex flex-col gap-3 mt-3">
{% for department in ticket_departments %}
<div
class="w-full rounded-md py-5 px-3 bg-secondosiblue border border-secondosiblue flex justify-between items-center gap-3 text-gray-50">
<p class="text-sm sm:text-[17px]">Departnet Name</p>
class="w-full rounded-md py-5 px-3 {% if forloop.first %} bg-secondosiblue border border-secondosiblue {% else %} bg-gray-50 border border-gray-100 {% endif %} flex justify-between items-center gap-3 text-gray-50">
<p class="text-sm sm:text-[17px]">{{department.department.name}}</p>
<p class="text-sm sm:text-base">10-2-2024</p>
</div>
<div
class="w-full rounded-md py-5 px-3 bg-gray-50 border border-gray-100 flex justify-between items-center gap-3 text-secondosiblue">
<p class="text-sm sm:text-[17px]">Departnet Name</p>
<p class="text-sm sm:text-base">10-2-2024</p>
</div>
<div
class="w-full rounded-md py-5 px-3 bg-gray-50 border border-gray-100 flex justify-between items-center gap-3 text-secondosiblue">
<p class="text-sm sm:text-[17px]">Departnet Name</p>
<p class="text-sm sm:text-base">10-2-2024</p>
<p class="text-sm sm:text-base">{{department.date_added}}</p>
</div>
{% endfor %}
</div>
</div>
@ -65,39 +53,66 @@
<div class="w-full grid grid-cols-1 l:grid-cols-2 xl:grid-cols-3 gap-3 mt-3">
<!-- PROJECT MANAGER -->
<a>
<a href="{% url 'customerdetails' ticket.customer.customer_id %}">
<div
class="w-full h-full flex flex-col gap-1 px-3 py-3 bg-gray-100 rounded-md shadow-md cursor-pointer hover:scale-105 duration-500">
class="w-full h-full flex flex-col gap-1 px-3 py-3 bg-secondosiblue rounded-md shadow-md cursor-pointer hover:scale-105 duration-500">
<div class="w-full flex justify-between items-center gap-1">
<div class="flex justify-start items-center gap-2">
<div class="w-[45px] h-[45px] rounded-full bg-white">
{% if project.manager.image %}
<img src="{{member.image.url}}" class="w-full h-full rounded-full object-cover">
{% else %}
<img src="{% static 'images/default-user.png' %}"
class="w-full h-full rounded-full object-cover">
{% endif %}
<div
class="w-full h-full border border-white bg-white text-secondosiblue uppercase rounded-full flex justify-center items-center p-1 shadow-md">
{{ ticket.customer.user.first_name.0 }}{{ ticket.customer.user.last_name.0 }}
</div>
</div>
<div>
<p class="text-secondosiblue font-light">Emile Elliye</p>
<div class="text-xs bg-secondosiblue text-white shadow-md py-1 px-2 rounded-md">
<p>Project Manager</p>
<p class="text-white font-light">{{ticket.customer.user.first_name}} {{ticket.customer.user.last_name}}</p>
<div class="text-xs text-white py-1">
<p>{{customer_last_seen}}</p>
</div>
</div>
</div>
<div
class="w-[30px] h-[30px] rounded-md shadow-md text-white bg-secondosiblue border border-osiblue flex justify-center items-center cursor-pointer hover:bg-transparent hover:text-secondosiblue duration-300">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-4" fill="none">
<path d="M19.0005 4.99988L5.00045 18.9999M5.00045 4.99988L19.0005 18.9999" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
</div>
</div>
</a>
{% for member in ticket_members %}
<a href="{% url 'userdetails' member.staff.staff_id %}">
<div
class="w-full h-full flex flex-col gap-1 px-3 py-3 bg-gray-100 rounded-md shadow-md cursor-pointer hover:scale-105 duration-500">
<div class="w-full flex justify-between items-center gap-1">
<div class="flex justify-start items-center gap-2">
<div class="w-[45px] h-[45px] rounded-full bg-white">
{% if member.staff.image %}
<img src="{{member.staff.image.url}}" class="w-full h-full rounded-full object-cover">
{% else %}
<img src="{% static 'images/default-user.png' %}"
class="w-full h-full rounded-full object-cover">
{% endif %}
</div>
<div>
<p class="text-secondosiblue font-light">{{member.staff.user.first_name}} {{member.staff.user.last_name}}</p>
<div class="text-xs text-secondosiblue py-1">
<p>{{member.get_last_seen}}</p>
</div>
</div>
</div>
<div
class="w-[30px] h-[30px] rounded-md shadow-md text-white bg-secondosiblue border border-osiblue flex justify-center items-center cursor-pointer hover:bg-transparent hover:text-secondosiblue duration-300">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-4" fill="none">
<path d="M19.0005 4.99988L5.00045 18.9999M5.00045 4.99988L19.0005 18.9999" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
</div>
</div>
</a>
{% endfor %}
</div>
</div>
</div>

@ -21,7 +21,7 @@ from django.conf import settings
urlpatterns = [
path('tickets/<str:ticket_number>/', views.ticket_room, name='ticketroom'),
path('ticket-settings', views.ticket_settings, name='ticketsettings'),
path('tickets/<str:ticket_number>/settings/', views.ticket_settings, name='ticketsettings'),
path('add-ticket-department', views.add_ticket_department_modal, name='addticketdepartmentmodal'),
path('add-ticket-member', views.add_ticket_member_modal, name='addticketmembermodal'),

@ -1,10 +1,7 @@
from django.shortcuts import render, get_object_or_404
from .models import *
from .decorators import *
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
from osinacore.decorators import *
# Create your views here.
@ticket_member_required
@ -37,10 +34,44 @@ def ticket_room(request, ticket_number):
@ticket_member_required
def ticket_settings(request):
def get_last_seen_order_key(member):
last_seen_text = member.get_last_seen()
if last_seen_text == "Online":
return (0, datetime.max)
elif "last seen today" in last_seen_text:
time_part = last_seen_text.split(" at ")[-1]
last_seen_time = datetime.strptime(time_part, '%I:%M %p')
return (1, last_seen_time)
elif "last seen yesterday" in last_seen_text:
time_part = last_seen_text.split(" at ")[-1]
last_seen_time = datetime.now() - timedelta(days=1)
last_seen_time = last_seen_time.replace(hour=datetime.strptime(time_part, '%I:%M %p').hour,
minute=datetime.strptime(time_part, '%I:%M %p').minute)
return (2, last_seen_time)
elif "last seen on" in last_seen_text:
date_time_part = last_seen_text.split(" on ")[-1]
last_seen_time = datetime.strptime(date_time_part, '%b %d at %I:%M %p')
return (3, last_seen_time)
else:
return (4, datetime.min)
@staff_login_required
def ticket_settings(request, ticket_number):
ticket = get_object_or_404(Ticket, ticket_number=ticket_number)
ticket_departments = TicketDepartment.objects.filter(ticket=ticket).order_by('-id')
ticket_members = TicketStaff.objects.filter(ticket=ticket)
ticket_members = sorted(ticket_members, key=get_last_seen_order_key)
customer_last_seen = ticket.get_customer_last_seen()
context = {
'ticket': ticket,
'ticket_departments': ticket_departments,
'ticket_members': ticket_members,
'customer_last_seen': customer_last_seen,
}

Loading…
Cancel
Save