emile 12 months ago
parent 40b1171f87
commit 0e7a153784

Binary file not shown.

Binary file not shown.

@ -67,27 +67,36 @@
</div> </div>
</div> </div>
<form> <div class="w-full flex flex-col gap-3 mt-3">
<div class="w-full flex flex-col gap-3 mt-3" id="positionsContainer"> {% for position in positions %}
{% for position in positions %} <div
<div class="w-full p-3 bg-gray-50 border border-gray-100 rounded-md flex justify-between items-center gap-3">
class="w-full p-3 bg-gray-50 border border-gray-100 rounded-md flex justify-between items-center gap-3"> <div class="flex flex-col">
<div class="flex flex-col"> <p class="text-gray-500">Position: <span class="text-secondosiblue">{{position.position}}</span>
<p class="text-gray-500">Position: <span class="text-secondosiblue">Junior Software </p>
Engineer</span></p> <p class="text-gray-500">Start Date: <span
<p class="text-gray-500">Start Date: <span class="text-secondosiblue">20-1-2020</span></p> class="text-secondosiblue">{{position.start_date}}</span></p>
<p class="text-gray-500">End Date: <span class="text-secondosiblue">20-1-2020</span></p> {% if not position.end_date %} - Present {% endif %}
</div> {% if position.end_date %}
<p class="text-gray-500">End Date: <span class="text-secondosiblue">{{position.end_date}}</span>
</p>
{% endif %}
</div>
<div class="w-[30px] h-[30px] rounded-full bg-secondosiblue border border-secondosiblue flex justify-center items-center gap-1 text-white cursor-pointer hover:bg-transparent hover:text-secondosiblue duration-300 delete" <div class="w-[30px] h-[30px] rounded-full bg-secondosiblue border border-secondosiblue flex justify-center items-center gap-1 text-white cursor-pointer hover:bg-transparent hover:text-secondosiblue duration-300 delete"
data-modal-url="{% url 'deletestaffpositionmodal' %}"> data-modal-url="{% url 'deletestaffpositionmodal' %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-5"> stroke="currentColor" class="w-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg> </svg>
</div>
</div> </div>
{% endfor %} </div>
{% endfor %}
</div>
<form method="POST">
{% csrf_token %}
<div class="w-full flex flex-col gap-3 mt-3" id="positionsContainer">
<div class="w-full bg-gray-50 border border-gray-100 rounded-md flex items-center gap-3 p-3 initialPositionContainer" <div class="w-full bg-gray-50 border border-gray-100 rounded-md flex items-center gap-3 p-3 initialPositionContainer"
style="display: none;"> style="display: none;">
<div class="w-full grid grid-cols-1 md:grid-cols-3 gap-3"> <div class="w-full grid grid-cols-1 md:grid-cols-3 gap-3">
@ -125,21 +134,21 @@
</div> </div>
</div> </div>
<div class="w-full bg-gray-50 shadow-md rounded-md p-3 flex justify-start items-center gap-1 cursor-pointer mt-3"
id="addNewPosition">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-7 text-secondosiblue">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<p class="text-secondosiblue hover:underline duration-300">Add New Position</p>
</div>
<div class="w-full flex justify-center items-center mt-3 hidden" id="submitButton"> <div class="w-full flex justify-center items-center mt-3 hidden" id="submitButton">
<button type="submit" <button type="submit"
class="w-fit py-1 px-5 bg-osiblue rounded-md outline-none text-white border border-osiblue text-xl cursor-pointer hover:bg-white hover:text-osiblue duration-300">Save</button> class="w-fit py-1 px-5 bg-osiblue rounded-md outline-none text-white border border-osiblue text-xl cursor-pointer hover:bg-white hover:text-osiblue duration-300">Save</button>
</div> </div>
</form> </form>
<div class="w-full bg-gray-50 shadow-md rounded-md p-3 flex justify-start items-center gap-1 cursor-pointer mt-3"
id="addNewPosition">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-7 text-secondosiblue">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<p class="text-secondosiblue hover:underline duration-300">Add New Position</p>
</div>
</div> </div>
</div> </div>
</div> </div>

@ -3,7 +3,7 @@
{% block content %} {% block content %}
<!-- LEFT SIDE --> <!-- LEFT SIDE -->
<div class="w-full xxlg1:w-[75%] flex flex-col gap-5"> <div class="w-full xxlg1:w-[75%] flex flex-col justify-center items-center gap-5">
<!-- RESPONSE MESSGAE FOR SHARING A TICKET --> <!-- RESPONSE MESSGAE FOR SHARING A TICKET -->
<div id="successMessage" <div id="successMessage"
@ -40,82 +40,9 @@
</tr> </tr>
</thead> </thead>
<tbody class="bg-white divide-y divide-gray-200"> <tbody class="bg-white divide-y divide-gray-200" id="tickets">
{% for ticket in open_tickets %} {% for ticket in open_tickets %}
<tr> {% include 'details_templates/partials/ticket-display.html' %}
<td class="px-6 py-4 text-sm border-r border-gray-300">
<div class="w-full flex {% if ticket.unread_updates_count > 0 %} justify-between {% else %} justify-center text-center {% endif %} items-center gap-3">
<p class="text-secondosiblue">{{ticket.title}}</p>
{% if ticket.unread_updates_count > 0 %}
<div>
<div
class="w-[25px] h-[25px] rounded-full bg-green-700 text-white flex justify-center items-center text-sm">
<p>{{ticket.unread_updates_count}}</p>
</div>
</div>
{% endif %}
</div>
</td>
<td class="px-6 py-4 text-center text-sm border-r border-gray-300">
<p class="text-secondosiblue">{{ticket.ticket_number}}</p>
</td>
<td class="px-6 py-4 text-center text-sm border-r border-gray-300">
<p class="text-secondosiblue">{{ticket.regarding}}</p>
</td>
<td class="px-6 py-4 text-center text-sm border-r border-gray-300">
{% if ticket.ticketupdate_set.all %}
<p class="text-secondosiblue">{{ticket.ticketupdate_set.all.last.date_added}} <br> by {{ticket.ticketupdate_set.all.last.added_by.first_name}}</p>
{% endif %}
</td>
<td class="px-6 py-4 text-center text-sm">
<div class="flex justify-center items-center gap-3">
<button class="copyButton">
<input type="text" class="hidden copyInput"
value="https://osina.ositcom.com/my-tickets/{{ticket.ticket_number}}/">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="w-[16px] text-fifthosiblue hover:scale-110 duration-500 transition-transform">
<path stroke-linecap="round" stroke-linejoin="round"
d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z" />
</svg>
</button>
<a href="{% url 'ticketroom' ticket.ticket_number %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="w-[18px] text-fifthosiblue hover:scale-110 duration-500 transition-transform">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</a>
<a href="">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="w-[18px] text-fifthosiblue hover:scale-110 duration-500 transition-transform">
<path stroke-linecap="round" stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</a>
<div class="cursor-pointer deleteTicketButton" data-modal-url="">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="w-[18px] text-red-500 hover:scale-110 duration-500 transition-transform">
<path stroke-linecap="round" stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</div>
</div>
</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@ -125,7 +52,7 @@
{% if tasks %} {% if tasks %}
<!-- TASKS ON DESKTOP --> <!-- TASKS ON DESKTOP -->
<div class="hidden md:flex flex-col gap-5"> <div class="w-full hidden md:flex flex-col gap-5">
<!-- TASKS TABLE --> <!-- TASKS TABLE -->
{% for task in tasks %} {% for task in tasks %}
<div class="w-full h-fit bg-white p-3 rounded-md shadow-md"> <div class="w-full h-fit bg-white p-3 rounded-md shadow-md">
@ -243,7 +170,7 @@
</div> </div>
<!-- TASKS ON MOBILE --> <!-- TASKS ON MOBILE -->
<div class="flex flex-col gap-5 md:hidden"> <div class="w-full flex flex-col gap-5 md:hidden">
{% for task in tasks %} {% for task in tasks %}
<div class="w-full h-fit bg-white p-3 rounded-md shadow-md"> <div class="w-full h-fit bg-white p-3 rounded-md shadow-md">
<div class="border border-gray-200 rounded-t-md"> <div class="border border-gray-200 rounded-t-md">
@ -363,6 +290,63 @@
<!---------------------- JS SCRIPTS --------------------> <!---------------------- JS SCRIPTS -------------------->
<script type="text/javascript" src='{% static "js/projects/calculate-all-pinned-projects-time.js" %}'></script> <script type="text/javascript" src='{% static "js/projects/calculate-all-pinned-projects-time.js" %}'></script>
<script type="text/javascript" src='{% static "js/tickets/share-ticket.js" %}'></script> <script type="text/javascript" src='{% static "js/tickets/share-ticket.js" %}'></script>
<script>
// WebSocket connection for new tickets
const newTicketsSocket = new WebSocket('ws://' + window.location.host + '/ws/new-tickets/');
newTicketsSocket.onopen = () => {
console.log('WebSocket connection to new tickets established');
};
newTicketsSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.event_type === 'new_ticket') {
const html = data.html;
document.getElementById('tickets').insertAdjacentHTML('afterbegin', html);
}
};
newTicketsSocket.onclose = () => {
console.log('WebSocket connection to new tickets closed');
};
newTicketsSocket.onerror = (error) => {
console.log('WebSocket error:', error);
};
// WebSocket connection for new ticket updates
const newTicketUpdatesSocket = new WebSocket('ws://' + window.location.host + '/ws/new-ticket-updates/');
newTicketUpdatesSocket.onopen = () => {
console.log('WebSocket connection to new ticket updates established');
};
newTicketUpdatesSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.event_type === 'new_ticket_update_group') {
const html = data.html;
const ticketId = data.ticket_id;
console.log(ticketId);
// Remove the existing ticket row if it exists
const existingTicketRows = document.querySelectorAll(`.ticket-${ticketId}`);
existingTicketRows.forEach(row => row.remove());
// Insert the updated ticket row at the top
document.getElementById('tickets').insertAdjacentHTML('afterbegin', html);
}
};
newTicketUpdatesSocket.onclose = () => {
console.log('WebSocket connection to new ticket updates closed');
};
newTicketUpdatesSocket.onerror = (error) => {
console.log('WebSocket error:', error);
};
</script>
{% endblock content %} {% endblock content %}

@ -511,21 +511,22 @@ def staffdetails( request, staff_id):
positions = StaffPosition.objects.filter(staff=staff).order_by('-id') positions = StaffPosition.objects.filter(staff=staff).order_by('-id')
if request.method == 'POST': if request.method == 'POST':
position_ids = request.POST.getlist('position[]') position_ids = request.POST.getlist('position[]')
print(position_ids)
start_dates = request.POST.getlist('start_date[]') start_dates = request.POST.getlist('start_date[]')
end_dates = request.POST.getlist('end_date[]') end_dates = request.POST.getlist('end_date[]')
for position_id, start_date, end_date in zip(position_ids, start_dates, end_dates): for position_id, start_date, end_date in zip(position_ids, start_dates, end_dates):
position = get_object_or_404(JobPosition, id=position_id) position = get_object_or_404(JobPosition, id=position_id)
if end_date: if end_date:
end_date = end_date end_date = end_date
else: else:
end_date = None end_date = None
StaffPosition.objects.create( StaffPosition.objects.create(
staff = staff, staff = staff,
position = position, position = position,
start_date = start_date, start_date = start_date,
end_date = end_date end_date = end_date
) )
return HttpResponse('<script>window.top.location.reload();</script>') return redirect('userdetails', staff_id=staff.staff_id)
context = { context = {

@ -30,7 +30,13 @@ document.addEventListener('DOMContentLoaded', function () {
selectElement.name = 'position[]'; selectElement.name = 'position[]';
const dateInputElements = newPositionContainer.querySelectorAll('input[type="date"]'); const dateInputElements = newPositionContainer.querySelectorAll('input[type="date"]');
dateInputElements.forEach(input => input.name = input.name.replace('date[]', '') + '[]'); dateInputElements.forEach((input, index) => {
if (index === 0) {
input.name = 'start_date[]';
} else {
input.name = 'end_date[]';
}
});
const removeButton = newPositionContainer.querySelector('.removePositionOption'); const removeButton = newPositionContainer.querySelector('.removePositionOption');
removeButton.addEventListener('click', removePositionContainer); removeButton.addEventListener('click', removePositionContainer);

@ -63,7 +63,7 @@ function app(socket) {
function initializeWebSocket() { function initializeWebSocket() {
const ticketId = document.getElementById('ticketId').textContent.trim(); const ticketId = document.getElementById('ticketId').textContent.trim();
const wsUrl = `wss://${window.location.host}/ws/ticketroom/${ticketId}/`; const wsUrl = `ws://${window.location.host}/ws/ticketroom/${ticketId}/`;
const socket = new WebSocket(wsUrl); const socket = new WebSocket(wsUrl);
socket.onopen = () => { socket.onopen = () => {

@ -12,7 +12,6 @@ from django.template.loader import render_to_string
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
class TicketRoomConsumer(WebsocketConsumer): class TicketRoomConsumer(WebsocketConsumer):
def connect(self): def connect(self):
self.user = self.scope['user'] self.user = self.scope['user']
@ -20,7 +19,7 @@ class TicketRoomConsumer(WebsocketConsumer):
self.ticket = get_object_or_404(Ticket, id=self.ticket_id) self.ticket = get_object_or_404(Ticket, id=self.ticket_id)
self.ticket_number = self.ticket.ticket_number self.ticket_number = self.ticket.ticket_number
existing_connection = TicketConnection.objects.filter(ticket=self.ticket, user=self.user, terminated=False).delete() existing_connection = TicketConnection.objects.filter(ticket=self.ticket, user=self.user, terminated_at__isnull=True).delete()
TicketConnection.objects.create( TicketConnection.objects.create(
ticket=self.ticket, ticket=self.ticket,
user=self.user, user=self.user,
@ -46,7 +45,7 @@ class TicketRoomConsumer(WebsocketConsumer):
TicketConnection.objects.filter( TicketConnection.objects.filter(
ticket=self.ticket, ticket=self.ticket,
user=self.user, user=self.user,
).update(terminated=True) ).update(terminated_at=datetime.now())
async_to_sync(self.channel_layer.group_discard)( async_to_sync(self.channel_layer.group_discard)(
self.ticket_number, self.channel_name self.ticket_number, self.channel_name
) )
@ -171,7 +170,7 @@ class TicketRoomConsumer(WebsocketConsumer):
})) }))
def modify_online_user(self): def modify_online_user(self):
connections = TicketConnection.objects.filter(ticket=self.ticket, terminated=False) connections = TicketConnection.objects.filter(ticket=self.ticket, terminated_at__isnull=True)
event = { event = {
'type': 'user_connection_handler', 'type': 'user_connection_handler',
'user': self.user, 'user': self.user,
@ -191,3 +190,80 @@ class TicketRoomConsumer(WebsocketConsumer):
'event_type': 'user_status', 'event_type': 'user_status',
'html': html 'html': html
})) }))
class NewTicketConsumer(WebsocketConsumer):
def connect(self):
self.user = self.scope['user']
async_to_sync(self.channel_layer.group_add)(
"new_ticket_group",
self.channel_name
)
self.accept()
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
"new_ticket_group",
self.channel_name
)
def new_ticket_event(self, event):
ticket_id = event['ticket_id']
ticket = Ticket.objects.get(id=ticket_id)
is_staff_or_superuser = (
hasattr(self.user, 'staffprofile') and
(self.user.staffprofile in ticket.get_all_ticket_staff() or self.user.is_superuser)
)
if is_staff_or_superuser:
context = {'ticket': ticket, 'new': True}
html = render_to_string("details_templates/partials/ticket-display.html", context)
self.send(text_data=json.dumps({
'event_type': 'new_ticket',
'html': html
}))
class NewTicketUpdateConsumer(WebsocketConsumer):
def connect(self):
self.user = self.scope['user']
async_to_sync(self.channel_layer.group_add)(
"new_ticket_update_group",
self.channel_name
)
self.accept()
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
"new_ticket_update_group",
self.channel_name
)
def new_ticket_update_event(self, event):
ticket_update_id = event['ticket_update_id']
ticket_update = TicketUpdate.objects.get(id=ticket_update_id)
ticket = ticket_update.ticket
ticket_id = ticket.id
unread_updates_count = 0
for ticket_update in ticket.ticketupdate_set.exclude(added_by=self.user):
if not TicketRead.objects.filter(ticket_update=ticket_update, user=self.user, read=True).exists():
unread_updates_count += 1
ticket.unread_updates_count = unread_updates_count
is_staff_or_superuser = (
hasattr(self.user, 'staffprofile') and
(self.user.staffprofile in ticket.get_all_ticket_staff() or self.user.is_superuser)
)
if is_staff_or_superuser:
context = {'ticket': ticket, 'new': True}
html = render_to_string("details_templates/partials/ticket-display.html", context)
self.send(text_data=json.dumps({
'event_type': 'new_ticket_update_group',
'html': html,
'ticket_id': ticket_id
}))

@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2024-07-04 06:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('support', '0008_alter_tickettask_ticket'),
]
operations = [
migrations.AddField(
model_name='ticketconnection',
name='terminated_at',
field=models.DateTimeField(blank=True, null=True),
),
]

@ -0,0 +1,17 @@
# Generated by Django 4.2.5 on 2024-07-04 06:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('support', '0009_ticketconnection_terminated_at'),
]
operations = [
migrations.RemoveField(
model_name='ticketconnection',
name='terminated',
),
]

@ -9,13 +9,13 @@ from django.conf import settings
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from threading import Timer from threading import Timer
from datetime import time from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
def send_ticket(ticket_id): def send_ticket(ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
department = TicketDepartment.objects.filter(ticket=ticket).last().department ticket_staffs = ticket.get_all_ticket_staff()
staff_profiles = department.get_staff() for staff in ticket_staffs:
for staff in staff_profiles:
subject = f"New Ticket Opened: {ticket.title}" subject = f"New Ticket Opened: {ticket.title}"
html_message = render_to_string('email_templates/new-ticket.html', { html_message = render_to_string('email_templates/new-ticket.html', {
'ticket': ticket, 'ticket': ticket,
@ -42,14 +42,9 @@ def send_ticket(ticket_id):
def send_update(update_id): def send_update(update_id):
update = get_object_or_404(TicketUpdate, id=update_id) update = get_object_or_404(TicketUpdate, id=update_id)
if TicketDepartment.objects.filter(ticket=update.ticket): staff_profiles = update.ticket.get_all_ticket_staff()
department = TicketDepartment.objects.filter(ticket=update.ticket).last().department print(staff_profiles)
else: for staff in staff_profiles:
department = Department.objects.get(name='Support')
if department:
staff_profiles = department.get_staff()
print(staff_profiles)
for staff in staff_profiles:
subject = f"New Ticket Update: {update.ticket.title}" subject = f"New Ticket Update: {update.ticket.title}"
html_message = render_to_string('email_templates/new-ticket-update.html', { html_message = render_to_string('email_templates/new-ticket-update.html', {
'update': update, 'update': update,
@ -113,10 +108,10 @@ class Ticket(models.Model):
last_connection = connections.first() last_connection = connections.first()
if not last_connection.terminated: if not last_connection.terminated_at:
return "Online" return "Online"
last_seen_time = last_connection.date last_seen_time = last_connection.terminated_at
now = timezone.now() now = timezone.now()
time_diff = now - last_seen_time time_diff = now - last_seen_time
@ -155,6 +150,18 @@ def send_signal_on_save(sender, instance, created, **kwargs):
@receiver(post_save, sender=Ticket)
def new_ticket_handler(sender, instance, created, **kwargs):
if created:
channel_layer = get_channel_layer()
event = {
'type': 'new_ticket_event',
'ticket_id': instance.id,
}
async_to_sync(channel_layer.group_send)("new_ticket_group", event)
class TicketDepartment(models.Model): class TicketDepartment(models.Model):
department = models.ForeignKey(Department, on_delete=models.CASCADE) department = models.ForeignKey(Department, on_delete=models.CASCADE)
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE) ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
@ -172,10 +179,10 @@ class TicketStaff(models.Model):
last_connection = connections.first() last_connection = connections.first()
if not last_connection.terminated: if not last_connection.terminated_at:
return "Online" return "Online"
last_seen_time = last_connection.date last_seen_time = last_connection.terminated_at
now = timezone.now() now = timezone.now()
time_diff = now - last_seen_time time_diff = now - last_seen_time
@ -211,7 +218,7 @@ class TicketConnection(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE) ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
date = models.DateTimeField() date = models.DateTimeField()
terminated = models.BooleanField(default=False) terminated_at = models.DateTimeField(null=True, blank=True)
@ -230,6 +237,18 @@ def send_signal_on_save(sender, instance, created, **kwargs):
timer.start() timer.start()
@receiver(post_save, sender=TicketUpdate)
def new_ticket_update_handler(sender, instance, created, **kwargs):
if created:
channel_layer = get_channel_layer()
event = {
'type': 'new_ticket_update_event',
'ticket_update_id': instance.id,
}
async_to_sync(channel_layer.group_send)("new_ticket_update_group", event)
class TicketRead(models.Model): class TicketRead(models.Model):
ticket_update = models.ForeignKey(TicketUpdate, on_delete=models.CASCADE) ticket_update = models.ForeignKey(TicketUpdate, on_delete=models.CASCADE)

@ -3,6 +3,8 @@ from .consumers import *
websocket_urlpatterns = [ websocket_urlpatterns = [
path("ws/ticketroom/<int:ticket_id>/", TicketRoomConsumer.as_asgi()), path("ws/ticketroom/<int:ticket_id>/", TicketRoomConsumer.as_asgi()),
path('ws/new-tickets/', NewTicketConsumer.as_asgi()),
path('ws/new-ticket-updates/', NewTicketUpdateConsumer.as_asgi()),
] ]

@ -19,7 +19,7 @@
<h1 class="text-secondosiblue text-2xl font-semibold text-center">Add New Member</h1> <h1 class="text-secondosiblue text-2xl font-semibold text-center">Add New Member</h1>
<select name="staff" class="w-full h-[150px] py-1 px-3 border border-gray-300 outline-none rounded-md mt-4" multiple> <select name="staff" class="w-full h-[50px] py-1 px-3 border border-gray-300 outline-none rounded-md mt-4">
<option value="" selected disabled>Select Staff</option> <option value="" selected disabled>Select Staff</option>
{% for staff in staffprofiles %} {% for staff in staffprofiles %}
<option value="{{staff.id}}">{{staff.user.first_name}} {{staff.user.last_name}}</option> <option value="{{staff.id}}">{{staff.user.first_name}} {{staff.user.last_name}}</option>

@ -0,0 +1,84 @@
<tr class="ticket-{{ticket.id}} {% if new %}bg-gray-200 fade-in-up{% endif %}">
<td class="px-6 py-4 text-sm border-r border-gray-300">
<div class="w-full flex {% if ticket.unread_updates_count > 0 %} justify-between {% else %} justify-center text-center {% endif %} items-center gap-3">
<p class="text-secondosiblue">{{ticket.title}}</p>
{% if ticket.unread_updates_count > 0 %}
<div>
<div
class="w-[25px] h-[25px] rounded-full bg-green-700 text-white flex justify-center items-center text-sm">
<p>{{ticket.unread_updates_count}}</p>
</div>
</div>
{% endif %}
</div>
</td>
<td class="px-6 py-4 text-center text-sm border-r border-gray-300">
<p class="text-secondosiblue">{{ticket.ticket_number}}</p>
</td>
<td class="px-6 py-4 text-center text-sm border-r border-gray-300">
<p class="text-secondosiblue">{{ticket.regarding}}</p>
</td>
<td class="px-6 py-4 text-center text-sm border-r border-gray-300">
{% if ticket.ticketupdate_set.all %}
<p class="text-secondosiblue">{{ticket.ticketupdate_set.all.last.date_added}} <br> by {{ticket.ticketupdate_set.all.last.added_by.first_name}}</p>
{% endif %}
</td>
<td class="px-6 py-4 text-center text-sm">
<div class="flex justify-center items-center gap-3">
<button class="copyButton">
<input type="text" class="hidden copyInput"
value="https://osina.ositcom.com/my-tickets/{{ticket.ticket_number}}/">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="w-[16px] text-fifthosiblue hover:scale-110 duration-500 transition-transform">
<path stroke-linecap="round" stroke-linejoin="round"
d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z" />
</svg>
</button>
<a href="{% url 'ticketroom' ticket.ticket_number %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="w-[18px] text-fifthosiblue hover:scale-110 duration-500 transition-transform">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</a>
<a href="">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="w-[18px] text-fifthosiblue hover:scale-110 duration-500 transition-transform">
<path stroke-linecap="round" stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</a>
<div class="cursor-pointer deleteTicketButton" data-modal-url="">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="w-[18px] text-red-500 hover:scale-110 duration-500 transition-transform">
<path stroke-linecap="round" stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</div>
</div>
</td>
</tr>
<style>
@keyframes fadeInAndUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0px); }
}
.fade-in-up {
animation: fadeInAndUp 0.6s ease;
}
</style>

@ -45,7 +45,7 @@
</div> </div>
<button <button
class="h-full rounded-tr-md px-4 bg-secondosiblue text-gray-200 text-[18px] outline-none border-none cursor-pointer flex justify-center items-center smallPopupButton" class="h-full rounded-tr-md px-4 bg-secondosiblue text-gray-200 text-[18px] outline-none border-none cursor-pointer flex justify-center items-center xsmallPopupButton"
data-modal-url="{% url 'addticketmembermodal' ticket.ticket_number %}"> data-modal-url="{% url 'addticketmembermodal' ticket.ticket_number %}">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
</button> </button>

@ -24,7 +24,7 @@ def ticket_room(request, ticket_number):
last_ticket_status = TicketStatus.objects.filter(ticket=ticket).last() last_ticket_status = TicketStatus.objects.filter(ticket=ticket).last()
connections = TicketConnection.objects.filter(terminated=False).order_by('-id') connections = TicketConnection.objects.filter(terminated_at__isnull=True).order_by('-id')
context = { context = {
'base_template': base_template, 'base_template': base_template,

Loading…
Cancel
Save