emile 10 months ago
parent e77014d6eb
commit fc986afd16

BIN
.DS_Store vendored

Binary file not shown.

BIN
osinaweb/.DS_Store vendored

Binary file not shown.

@ -5,6 +5,7 @@ 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 support .models import *
@customer_login_required

@ -1,10 +1 @@
from django.contrib import admin
from .models import *
# Register your models here.
admin.site.register(Ticket)
admin.site.register(TicketStatus)
admin.site.register(TicketRead)
admin.site.register(TicketUpdate)
admin.site.register(TicketAttachment)
admin.site.register(TicketUpdateReaction)

@ -1,6 +1,7 @@
from billing.models import *
from osinacore.models import *
from customercore.models import *
from support.models import *
from django.db.models import Count, Q
def utilities(request):

@ -0,0 +1,82 @@
# Generated by Django 4.2.5 on 2024-06-27 19:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('customercore', '0023_delete_file_remove_ticketattachment_file_and_more'),
]
operations = [
migrations.RemoveField(
model_name='ticketattachment',
name='ticket',
),
migrations.RemoveField(
model_name='ticketattachment',
name='ticket_update',
),
migrations.RemoveField(
model_name='ticketread',
name='ticket_update',
),
migrations.RemoveField(
model_name='ticketread',
name='user',
),
migrations.RemoveField(
model_name='ticketstatus',
name='added_by',
),
migrations.RemoveField(
model_name='ticketstatus',
name='ticket',
),
migrations.RemoveField(
model_name='tickettask',
name='task',
),
migrations.RemoveField(
model_name='tickettask',
name='ticket',
),
migrations.RemoveField(
model_name='ticketupdate',
name='added_by',
),
migrations.RemoveField(
model_name='ticketupdate',
name='ticket',
),
migrations.RemoveField(
model_name='ticketupdatereaction',
name='customer',
),
migrations.RemoveField(
model_name='ticketupdatereaction',
name='ticket_update',
),
migrations.DeleteModel(
name='Ticket',
),
migrations.DeleteModel(
name='TicketAttachment',
),
migrations.DeleteModel(
name='TicketRead',
),
migrations.DeleteModel(
name='TicketStatus',
),
migrations.DeleteModel(
name='TicketTask',
),
migrations.DeleteModel(
name='TicketUpdate',
),
migrations.DeleteModel(
name='TicketUpdateReaction',
),
]

@ -1,90 +0,0 @@
from django.db import models
from billing.models import *
from django.utils import timezone
# Create your models here.
class Ticket(models.Model):
REGARDING_CHOICES = (
('General/Account/Billing', 'General/Account/Billing'),
('Project/Product', 'Project/Product'),
)
STATUS_CHOICES = (
('Open', 'Open'),
('Working On', 'Working On'),
('Closed', 'Closed'),
)
ticket_number = models.CharField(max_length=400, blank=True)
title = models.CharField(max_length=400)
description = models.TextField(null=True, blank=True)
status = models.CharField(max_length=50, choices=STATUS_CHOICES, null=True)
regarding = models.CharField(max_length=50, choices=REGARDING_CHOICES, null=True)
project = models.ForeignKey(Project, on_delete=models.SET_NULL, blank=True, null=True)
product = models.ForeignKey(Item, on_delete=models.SET_NULL, blank=True, null=True)
departments = models.ManyToManyField(Department, null=True, blank=True)
opened_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
opened_date = models.DateTimeField()
customer = models.ForeignKey(CustomerProfile, on_delete=models.SET_NULL, null=True)
ticket_members = models.ManyToManyField(StaffProfile, null=True, blank=True, related_name='ticket_members')
def save(self, *args, **kwargs):
if not self.ticket_number:
last_ticket = Ticket.objects.filter(opened_date__year=timezone.now().year).order_by('-ticket_number').first()
if last_ticket and last_ticket.ticket_number:
last_ticket_number = int(last_ticket.ticket_number[-4:])
new_ticket_number = last_ticket_number + 1
else:
new_ticket_number = 1
current_year_last_two_digits = str(timezone.now().year)[-2:]
self.ticket_number = f"{current_year_last_two_digits}{new_ticket_number:04}"
super().save(*args, **kwargs)
class TicketStatus(models.Model):
STATUS_CHOICES = (
('Open', 'Open'),
('Working On', 'Working On'),
('Closed', 'Closed'),
)
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
status = models.CharField(max_length=50, choices=STATUS_CHOICES, null=True)
added_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
date_added = models.DateTimeField()
class TicketUpdate(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
description = models.TextField(null=True, blank=True)
added_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
date_added = models.DateTimeField()
class TicketRead(models.Model):
ticket_update = models.ForeignKey(TicketUpdate, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
read = models.BooleanField(default=False)
class TicketAttachment(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, null=True, blank=True)
ticket_update = models.ForeignKey(TicketUpdate, on_delete=models.CASCADE, null=True, blank=True)
file_path = models.CharField(max_length=255, null=True)
class TicketUpdateReaction(models.Model):
REACTION_CHOICES = (
('Happy', 'Happy'),
('Indifferent', 'Indifferent'),
('Sad', 'Sad'),
)
customer = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
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)

@ -3,7 +3,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-3">
<div class="w-full flex flex-col gap-3">
<div class="w-full bg-white rounded-md h-fit shadow-md p-5">
<h1 class="text-3xl text-secondosiblue text-center font-semibold">
Create Ticket

@ -2,7 +2,7 @@
{%load static%}
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-5">
<div class="w-full flex flex-col gap-5">
<!--<div class="w-full bg-white rounded-md h-fit shadow-md p-5 flex justify-center items-center text-center">

@ -1,106 +0,0 @@
{% load static %}
<div class="flex gap-3 mb-10">
<div>
<div class="w-[45px] s:w-[60px] h-[45px] s:h-[60px] rounded-full shadow-md border border-gray-100">
{% if update.added_by.customerprofile %}
{% if update.added_by.customerprofile.image %}
<img src="{{update.added_by.customerprofile.image.url}}"
class="w-full h-full rounded-full object-cover">
{% else %}
<div
class="w-full h-full border border-secondosiblue bg-secondosiblue text-white uppercase rounded-full flex justify-center items-center p-1 shadow-md">
{{ update.added_by.first_name.0 }}{{ update.added_by.last_name.0 }}
</div>
{% endif %}
{% elif update.added_by.staffprofile %}
{% if update.added_by.staffprofile.image %}
<img src="{{update.added_by.staffprofile.image.url}}"
class="w-full h-full rounded-full object-cover">
{% else %}
<div
class="w-full h-full border border-osiblue bg-osiblue text-white uppercase rounded-full flex justify-center items-center p-1 shadow-md">
{{ update.added_by.first_name.0 }}{{ update.added_by.last_name.0 }}
</div>
{% endif %}
{% endif %}
</div>
</div>
<div class="w-full replyContainer shadow-md">
<div
class="w-full bg-gray-100 flex justify-between items-center gap-3 px-3 py-3 cursor-pointer rounded-t-md toggleReply">
<p class="text-secondosiblue font-light text-sm s:text-base"><span
class="font-semibold">{{update.added_by.first_name}}</span>
replied {{update.date_added}}</p>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4 text-secondosiblue arrowUp">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4 text-secondosiblue arrowDown hidden">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</div>
<div class="w-full bg-white p-5 flex flex-col gap-3 reply default-css">
{{update.description | safe }}
{% if update.ticketattachment_set.all %}
<div class="w-full flex flex-wrap justify-end items-center gap-3">
{% for file in update.ticketattachment_set.all %}
<div
class="flex items-center gap-1 text-secondosiblue hover:text-gray-500 duration-300 cursor-pointer text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-4 h-4 text-secondosiblue">
<path stroke-linecap="round" stroke-linejoin="round"
d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
</svg>
<a href="https://osina.ositcom.com/{{file.file_path}}" target="_blank">
{{ file.file_path | cut:"static/images/uploaded_ticket_files/" }}{% if not forloop.last %}, {% endif %}
</a>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% if update.added_by.staffprofile %}
<div class="w-full border-t border-gray-200 pt-5 flex justify-start items-center gap-3 p-5">
<a class="text-secondosiblue font-light cursor-pointer hover:text-gray-500 duration-300">How
did I do?</a>
<div class="flex justify-start items-center gap-2">
<a href="{% url 'customeraddticketupdatereaction' 'Happy' update.id %}">
<div
class="w-fit h-fit rounded-full {% if update.last_customer_reaction == 'Happy' %} border-2 border-secondosiblue {% endif %}">
<img src="{% static 'images/icons/happy-icon.png' %}"
class="w-[30px] h-[30px] rounded-full cursor-pointer hover:scale-105 duration-300 transition-transform">
</div>
</a>
<a href="{% url 'customeraddticketupdatereaction' 'Indifferent' update.id %}">
<div
class="w-fit h-fit rounded-full {% if update.last_customer_reaction == 'Indifferent' %} border-2 border-secondosiblue {% endif %}">
<img src="{% static 'images/icons/neutral-icon.png' %}"
class="w-[30px] h-[30px] rounded-full cursor-pointer hover:scale-105 duration-300 transition-transform">
</div>
</a>
<a href="{% url 'customeraddticketupdatereaction' 'Sad' update.id %}">
<div
class="w-fit h-fit rounded-full {% if update.last_customer_reaction == 'Sad' %} border-2 border-secondosiblue {% endif %}">
<img src="{% static 'images/icons/unhappy-icon.png' %}"
class="w-[30px] h-[30px] rounded-full cursor-pointer hover:scale-105 duration-300 transition-transform">
</div>
</a>
</div>
</div>
{% endif %}
</div>
</div>

@ -3,7 +3,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-3">
<div class="w-full flex flex-col gap-3">
<div class="w-full bg-white rounded-md h-fit shadow-md p-5">
<div
class="w-full bg-gray-50 px-3 py-3 border border-gray-100 shadow-md rounded-md flex flex-col md:flex-row justify-between items-center gap-3">

@ -3,7 +3,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-3">
<div class="w-full flex flex-col gap-3">
<div class="w-full bg-white rounded-md h-fit shadow-md p-5">
<div
class="w-full rounded-md flex flex-col justify-center items-center py-2 gap-2 mt-[50px] {% if project.projectstatus_set.all.last.status == 'In Progress' %} bg-orange-500 {% elif project.projectstatus_set.all.last.status == 'Completed' %} bg-green-700 {% elif project.projectstatus_set.all.last.status == 'Cancelled' %} bg-red-500 {% endif %}">

@ -1,237 +0,0 @@
{% extends "customer_main.html" %}
{%load static%}
{% block content %}
<!-- TEXT EDITOR -->
<style>
@import url(https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/5.3.45/css/materialdesignicons.min.css);
</style>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.js" defer></script>
<div class="w-full px-5 s:px-9 flex flex-col gap-3">
<div class="w-full bg-white rounded-md h-fit shadow-md p-5">
<div class="w-full h-fit flex flex-col gap-2 bg-gray-100 shadow-md rounded-md px-3 py-3">
<div class="w-full flex flex-col s:flex-row justify-between items-start s:items-center gap-3 mb-4 s:mb-0">
<p class="text-secondosiblue text-[20px]">Ticket <span
class="font-semibold">#{{ticket.ticket_number}}</span></p>
<button
class="w-full s:w-fit px-3 py-2 bg-red-500 border border-red-500 text-white cursor-pointer duration-300 hover:bg-white hover:text-red-500 rounded-md closeTicketButton" data-modal-url="{% url 'closeticketstatusmodal' %}">
Close Ticket
</button>
</div>
{% if last_ticket_status.status == 'Open' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-green-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Opened by {{last_ticket_status.added_by.first_name}} at
{{last_ticket_status.date_added}}</p>
</div>
{% elif last_ticket_status.status == 'Working On' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-yellow-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Updated to 'Working On' by
{{last_ticket_status.added_by.first_name}} at {{last_ticket_status.date_added}}</p>
</div>
{% elif last_ticket_status.status == 'Closed' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-red-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Closed by {{last_ticket_status.added_by.first_name}} at
{{last_ticket_status.date_added}}</p>
</div>
{% endif %}
<div class="w-full mt-3">
<p class="text-gray-500 font-light text-sm leading-7">{{ticket.description}}
</p>
</div>
{% if ticket.ticketattachment_set.all %}
<div class="w-full flex flex-wrap justify-end items-center gap-3">
{% for file in ticket.ticketattachment_set.all %}
<div class="flex items-center gap-1 text-secondosiblue hover:text-gray-500 duration-300 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4 text-secondosiblue">
<path stroke-linecap="round" stroke-linejoin="round"
d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
</svg>
<a href="https://osina.ositcom.com/{{file.file_path}}" target="_blank" class="text-sm">
{{ file.file_path | cut:"static/images/uploaded_ticket_files/" }}{% if not forloop.last %}, {% endif %}</a>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<div class="w-full flex flex-col mt-5 s:mt-10">
<div id="messages">
{% for update in ticket_updates %}
{% include 'details_templates/customer-ticket-message.html' %}
{% endfor %}
</div>
<div id="typing-notification"></div>
<!-- REPLYING SECTION -->
<form id="ticketForm" class="flex gap-3"
enctype="multipart/form-data">
{% csrf_token %}
<div>
<div class="w-[45px] s:w-[60px] h-[45px] s:h-[60px] rounded-full shadow-md border border-gray-100">
{% if request.user.customerprofile %}
{% if request.user.customerprofile.image %}
<img src="{{request.user.customerprofile.image.url}}"
class="w-full h-full rounded-full object-cover">
{% else %}
<div
class="w-full h-full border border-secondosiblue bg-secondosiblue text-white uppercase rounded-full flex justify-center items-center p-1 shadow-md">
{{ request.user.first_name.0 }}{{ request.user.last_name.0 }}
</div>
{% endif %}
{% elif request.user.staffprofile %}
{% if request.user.staffprofile.image %}
<img src="{{request.user.staffprofile.image.url}}"
class="w-full h-full rounded-full object-cover">
{% else %}
<div
class="w-full h-full border border-osiblue bg-osiblue text-white uppercase rounded-full flex justify-center items-center p-1 shadow-md">
{{ request.user.first_name.0 }}{{ request.user.last_name.0 }}
</div>
{% endif %}
{% endif %}
</div>
</div>
<div class="w-full flex flex-col gap-3">
<textarea name="description" rows="8" id="textEditor"
class="w-full bg-white px-3 py-3 border border-gray-200 rounded-b-md outline-none text-gray-500 resize-none"
placeholder="Add Comment..." required></textarea>
<div class="w-full flex flex-col items-end gap-3">
<div class="w-full s:w-fit flex flex-col s:flex-row justify-end items-center gap-3">
<div
class="w-full s:w-[50px] h-[50px] rounded-md bg-gray-50 shadow-md border border-gray-100 flex justify-center items-center p-2 cursor-pointer relative hover:scale-105 duration-300 transition-transform">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="w-5 h-5 text-secondosiblue z-10 absolute pointer-events-none">
<path stroke-linecap="round" stroke-linejoin="round"
d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
</svg>
<input type="file" id="fileupload" name="" placeholder="Select file" multiple
class="opacity-0 absolute top-1/2 left-1/2 w-[25px] -translate-x-1/2 -translate-y-1/2 z-10">
<select id="filePathInput" name="filePath" multiple hidden></select>
</div>
<button
class="w-full s:w-fit bg-secondosiblue border border-secondosiblue text-white rounded-md cursor-pointer hover:bg-white hover:text-secondosiblue duration-300 px-9 py-3">
Send
</button>
</div>
<div id="uploaded_files" class="w-full flex flex-col gap-3"></div>
</div>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', () => {
const ticketId = '{{ticket.id}}';
const wsUrl = `ws://${window.location.host}/ws/ticketroom/${ticketId}/`;
const socket = new WebSocket(wsUrl);
let typingTimeout = null;
socket.onopen = () => {
console.log('WebSocket connection established');
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event_type === 'typing') {
const typingDiv = document.getElementById('typing-notification');
typingDiv.innerHTML = data.html;
} else if (data.event_type === 'stop_typing') {
const typingDiv = document.getElementById('typing-notification');
typingDiv.innerHTML = '';
} else {
const typingDiv = document.getElementById('typing-notification');
const messagesDiv = document.getElementById('messages');
messagesDiv.insertAdjacentHTML('beforeend', data.html);
typingDiv.innerHTML = '';
}
};
socket.onclose = () => {
console.log('WebSocket connection closed');
};
socket.onerror = (error) => {
console.log('WebSocket error:', error);
};
const form = document.getElementById('ticketForm');
const textEditor = document.getElementById('textEditor');
textEditor.addEventListener('input', () => {
clearTimeout(typingTimeout);
const data = {
event_type: 'typing'
};
socket.send(JSON.stringify(data));
typingTimeout = setTimeout(() => {
const stopTypingData = {
event_type: 'stop_typing'
};
socket.send(JSON.stringify(stopTypingData));
}, 2000);
});
form.addEventListener('submit', (event) => {
event.preventDefault();
const formData = new FormData(form);
const description = formData.get('description');
const files = formData.getAll('filePath');
const filePaths = [];
for (const file of files) {
const filePath = `/path/to/uploaded/files/${file.name}`;
filePaths.push(filePath);
}
const data = {
event_type: 'update',
description: description,
filePath: filePaths
};
socket.send(JSON.stringify(data));
form.reset();
});
});
</script>
</div>
</div>
</div>
<!---------------------- JS SCRIPTS -------------------->
<script type="text/javascript" src="{% static 'js/tickets/ticket-details.js' %}"></script>
<script type="text/javascript" src="{% static 'js/inputs/text-editor.js' %}"></script>
<script type="text/javascript" src='{% static "js/inputs/file-uploader.js" %}'></script>
{% endblock %}

@ -2,7 +2,7 @@
{%load static%}
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-3">
<div class="w-full flex flex-col gap-3">
<!-- INVOICES -->
<div class="w-full bg-white rounded-md h-fit shadow-md p-5">
<h1 class="text-secondosiblue text-[25px]">My Invoices</h1>

@ -3,7 +3,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-3">
<div class="w-full flex flex-col gap-3">
<div class="w-full bg-white rounded-md h-fit shadow-md p-5">
<h1 class="text-secondosiblue text-[25px]">My Orders</h1>

@ -3,7 +3,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-3">
<div class="w-full flex flex-col gap-3">
<div class="w-full bg-white rounded-md h-fit shadow-md p-5">
<h1 class="text-secondosiblue text-[25px]">My Projects</h1>

@ -3,7 +3,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-3">
<div class="w-full flex flex-col gap-3">
<div class="w-full bg-white rounded-md h-fit shadow-md p-5">
<h1 class="text-secondosiblue text-[25px]">My Tickets</h1>
@ -61,10 +61,10 @@
<tbody class="bg-white divide-y divide-gray-200">
{% for ticket in open_tickets %}
<tr data-href="{% url 'customerticketdetails' ticket.ticket_number %}" class="hover:bg-gray-100 duration-300 cursor-pointer">
<tr data-href="{% url 'ticketroom' ticket.ticket_number %}" class="hover:bg-gray-100 duration-300 cursor-pointer">
<td class="min-w-[250px] max-w-[250px] px-6 py-4 text-center text-sm border-r border-gray-300">
<div class="w-full flex {% if ticket.unread_updates_count > 0 %} justify-between {% else %} justify-center {% endif %} items-center gap-3">
<a href="{% url 'customerticketdetails' ticket.ticket_number %}">
<a href="{% url 'ticketroom' ticket.ticket_number %}">
<p class="text-secondosiblue">{{ticket.title }}</p>
</a>
@ -139,9 +139,9 @@
<tbody class="bg-white divide-y divide-gray-200">
{% for ticket in closed_tickets %}
<tr data-href="{% url 'customerticketdetails' ticket.ticket_number %}" class="hover:bg-gray-100 duration-300 cursor-pointer">
<tr data-href="{% url 'ticketroom' ticket.ticket_number %}" class="hover:bg-gray-100 duration-300 cursor-pointer">
<td class="px-6 py-4 text-center text-sm border-r border-gray-300">
<a href="{% url 'customerticketdetails' ticket.ticket_number %}">
<a href="{% url 'ticketroom' ticket.ticket_number %}">
<p class="text-secondosiblue cursor-pointer">{{ticket.title }}</p>
</a>
</td>

@ -3,7 +3,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-3">
<div class="w-full flex flex-col gap-3">
<div class="w-full bg-white rounded-md h-fit shadow-md p-5">
<div
class="w-full bg-secondosiblue shadow-md rounded-md px-5 py-5 flex flex-wrap items-center gap-8 text-fifthosiblue text-base xl:text-xl uppercase">

@ -12,7 +12,7 @@
<p id="itemId" class="hidden">{{item.id}}</p>
<div class="w-full px-5 s:px-9 flex flex-col gap-5 mt-5 font-poppinsLight">
<div class="w-full flex flex-col gap-5 mt-5 font-poppinsLight">
<div class="w-full h-fit bg-white rounded-md shadow-md">
<div class="grid grid-cols-1 xxlg1:grid-cols-2">

@ -12,7 +12,7 @@
<p id="paymentId" class="hidden">{{payment.id}}</p>
<div class="w-full px-5 s:px-9 flex flex-col gap-5 mt-5 font-poppinsLight">
<div class="w-full flex flex-col gap-5 mt-5 font-poppinsLight">
<div class="w-full h-fit bg-white rounded-md shadow-md">
<div class="grid grid-cols-1 xxlg1:grid-cols-2">

@ -9,7 +9,7 @@
<div class="w-full px-5 s:px-9 flex flex-col gap-5 mt-5 font-poppinsLight">
<div class="w-full flex flex-col gap-5 mt-5 font-poppinsLight">
<div class="w-full h-fit bg-white rounded-md shadow-md">
<div class="grid grid-cols-1 xxlg1:grid-cols-2">
<div

@ -9,7 +9,7 @@
<div class="w-full px-5 s:px-9 flex flex-col gap-5 mt-5 font-poppinsLight">
<div class="w-full flex flex-col gap-5 mt-5 font-poppinsLight">
<div class="w-full h-fit bg-white rounded-md shadow-md">
<div class="grid grid-cols-1 xxlg1:grid-cols-2">
<div

@ -8,7 +8,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-5 mt-5 mb-5">
<div class="w-full flex flex-col gap-5 mt-5 mb-5">
<div class="w-full h-fit bg-white rounded-md shadow-md px-5 py-9 flex flex-col justify-center items-center">
<p class="text-osiblue uppercase text-xl s:text-3xl font-poppinsBold text-center">Cloud VPS Hosting</p>

@ -8,7 +8,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-5 mt-5 mb-5">
<div class="w-full flex flex-col gap-5 mt-5 mb-5">
<div class="w-full h-fit bg-white rounded-md shadow-md px-5 py-9 flex flex-col justify-center items-center">
<img src="{% static 'images/cpanellogo.png' %}" class="w-[180px] s:w-[250px]">

@ -8,7 +8,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-5 mt-5 mb-5">
<div class="w-full flex flex-col gap-5 mt-5 mb-5">
<div class="w-full h-fit bg-white rounded-md shadow-md px-5 py-9 flex flex-col justify-center items-center">
<p class="text-osiblue uppercase text-xl s:text-3xl font-poppinsBold text-center">Dedicated CPU Servers</p>

@ -8,7 +8,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-5 mt-5 mb-5">
<div class="w-full flex flex-col gap-5 mt-5 mb-5">
<div class="w-full h-fit bg-white rounded-md shadow-md px-5 py-9 flex flex-col justify-center items-center">
<img src="{% static 'images/ositcom_logos/osicardblue.png' %}" class="w-[180px] s:w-[250px]">

@ -7,7 +7,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-5 mt-5 mb-5">
<div class="w-full flex flex-col gap-5 mt-5 mb-5">
<div class="w-full h-fit bg-white rounded-md shadow-md px-5 py-9 flex flex-col justify-center items-center">
<img src="{% static 'images/ositcom_logos/osimenublue.png' %}" class="w-[180px] s:w-[250px]">

@ -8,7 +8,7 @@
{% block content %}
<div class="w-full px-5 s:px-9 flex flex-col gap-5 mt-5 mb-5">
<div class="w-full flex flex-col gap-5 mt-5 mb-5">
<div class="w-full h-fit bg-white rounded-md shadow-md px-5 py-9 flex flex-col justify-center items-center">
<p class="text-osiblue uppercase text-xl s:text-3xl font-poppinsBold text-center">Shared Hosting</p>

@ -20,7 +20,6 @@ urlpatterns = [
# DETAILS
path('my-tickets/<int:ticket_number>/', views.customer_ticket_details, name='customerticketdetails'),
path('my-orders/<str:order_id>/', views.customer_order_details, name='customerorderdetails'),
path('my-projects/<str:project_id>/', views.customer_project_details, name='customerprojectdetails'),

@ -10,6 +10,7 @@ from customercore.views import *
from django.contrib.auth.hashers import check_password
from django.contrib.auth import update_session_auth_hash, logout
import json
from support .models import *
@ -166,44 +167,6 @@ def customer_tickets(request, *args, **kwargs):
# DETAILS
@customer_login_required
def customer_ticket_details(request, ticket_number):
ticket = get_object_or_404(Ticket, ticket_number=ticket_number)
# Check if the logged-in user is the customer associated with the ticket
if request.user.is_authenticated:
if ticket.customer != request.user.customerprofile:
raise Http404("Page not found.")
# Subquery to get the last reaction added by the logged-in customer for each ticket update
last_reaction_subquery = TicketUpdateReaction.objects.filter(
ticket_update=OuterRef('pk'),
customer=request.user
).order_by('-id').values('reaction')[:1]
ticket_updates = TicketUpdate.objects.filter(ticket=ticket).annotate(
last_customer_reaction=Subquery(last_reaction_subquery)
).order_by('id')
# Mark updates as read for the current user
for update in TicketUpdate.objects.filter(ticket=ticket).exclude(added_by=request.user).order_by('id'):
if not TicketRead.objects.filter(ticket_update=update, user=request.user).exists():
TicketRead.objects.create(ticket_update=update, user=request.user, read=True)
last_ticket_status = TicketStatus.objects.filter(ticket=ticket).last()
else:
ticket_updates = None
last_ticket_status = None
context = {
'ticket': ticket,
'ticket_updates': ticket_updates,
'last_ticket_status': last_ticket_status,
}
return render(request, 'details_templates/inner-customer-ticket.html', context)
# PRODUCTS

Binary file not shown.

@ -8,7 +8,7 @@ from datetime import date
from django.http import JsonResponse
from osinacore.decorators import *
from billing.models import *
from customercore.models import *
from support.models import *
import os
from django.conf import settings
from django.core.files import File

Binary file not shown.

@ -2,7 +2,7 @@
{%load static%}
{% block content %}
<div class="w-full px-5 s:px-9 mb-5">
<div class="w-full">
<div class="w-full h-full shadow-md rounded-md py-5 px-3 bg-white">
<h1 class="text-3xl text-secondosiblue text-center font-semibold">
Add Business

@ -155,11 +155,11 @@
<!-- TABLE BODY -->
{% for ticket in customer_open_tickets %}
<tbody class="bg-white divide-y divide-gray-200">
<tr data-href="{% url 'customerticketdetails' ticket.ticket_number %}"
<tr data-href="{% url 'ticketroom' ticket.ticket_number %}"
class="hover:bg-gray-100 duration-300 cursor-pointer">
<td class="min-w-[250px] max-w-[250px] px-6 py-4 text-center text-sm border-r border-gray-300">
<div class="w-full flex {% if ticket.unread_updates_count > 0 %} justify-between {% else %} justify-center {% endif %} items-center gap-3">
<a href="{% url 'customerticketdetails' ticket.ticket_number %}">
<a href="{% url 'ticketroom' ticket.ticket_number %}">
<p class="text-secondosiblue">{{ticket.title }}</p>
</a>

@ -562,7 +562,7 @@
<!-- BODY -->
<div class="w-full mb-5">
<div class="w-full px-5 s:px-9 mb-5">
{% block content %}
replace me
{% endblock content %}

@ -1,368 +0,0 @@
{% extends "main.html" %}
{%load static%}
{% block content %}
<!-- TEXT EDITOR -->
<style>
@import url(https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/5.3.45/css/materialdesignicons.min.css);
</style>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.js" defer></script>
<script src="https://unpkg.com/htmx.org/dist/htmx.js"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
<div class="w-full xxlg1:w-[75%] bg-white h-fit rounded-md shadow-md p-5">
<div class="w-full h-fit flex flex-col gap-2 bg-gray-100 shadow-md rounded-md px-3 py-3">
<div class="w-full flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-5 md:mb-0">
<p class="text-secondosiblue text-[20px]">Ticket <span class="font-semibold">#{{ticket.ticket_number}}</span></p>
<div class="w-full s:w-fit flex flex-col s:flex-row justify-end items-center gap-2">
<button class="w-full s:w-fit px-3 py-2 bg-osiblue border border-osiblue text-white cursor-pointer duration-300 hover:bg-white hover:text-osiblue rounded-md updateTicketStatusButton" data-modal-url="{% url 'edit-ticket-status-modal' ticket.id %}">
Update Status
</button>
<button class="w-full s:w-fit px-3 py-2 bg-osiblue border border-osiblue text-white cursor-pointer duration-300 hover:bg-white hover:text-osiblue rounded-md">
Add Task
</button>
</div>
</div>
{% if last_ticket_status.status == 'Open' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-green-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Opened by {{last_ticket_status.added_by.first_name}} on
{{ last_ticket_status.date_added|date:"d F Y, h:i A" }}</p>
</div>
{% elif last_ticket_status.status == 'Working On' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-yellow-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Updated to 'Working On' by
{{last_ticket_status.added_by.first_name}} on {{ last_ticket_status.date_added|date:"d F Y, h:i A" }}</p>
</div>
{% elif last_ticket_status.status == 'Closed' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-red-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Closed by {{last_ticket_status.added_by.first_name}} on
{{ last_ticket_status.date_added|date:"d F Y, h:i A" }}</p>
</div>
{% endif %}
<div class="w-full mt-3">
<p class="text-gray-500 font-light text-sm leading-7">{{ticket.description}}
</p>
</div>
{% if ticket.ticketattachment_set.all %}
<div class="w-full flex flex-wrap justify-end items-center gap-3">
{% for file in ticket.ticketattachment_set.all %}
<div class="flex items-center gap-1 text-secondosiblue hover:text-gray-500 duration-300 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4 text-secondosiblue">
<path stroke-linecap="round" stroke-linejoin="round"
d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
</svg>
<a href="https://osina.ositcom.com/{{file.file_path}}" target="_blank" class="text-sm">{{ file.file_path | cut:"static/images/uploaded_ticket_files/" }}{% if not forloop.last %}, {% endif %}</a>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<div class="w-full flex flex-col mt-5 s:mt-10">
<!-- REPLY 1 -->
<div id="messages">
{% for update in ticket_updates %}
{% include 'details_templates/ticket-message.html' %}
{% endfor %}
</div>
<div id="typing-notification"></div>
<!-- REPLYING SECTION -->
<form id="ticketForm" class="flex gap-3"
enctype="multipart/form-data">
{% csrf_token %}
<div>
<div class="w-[45px] s:w-[60px] h-[45px] s:h-[60px] rounded-full shadow-md border border-gray-100">
{% if request.user.customerprofile %}
{% if request.user.customerprofile.image %}
<img src="{{request.user.customerprofile.image.url}}"
class="w-full h-full rounded-full object-cover">
{% else %}
<div
class="w-full h-full border border-secondosiblue bg-secondosiblue text-white uppercase rounded-full flex justify-center items-center p-1 shadow-md">
{{ request.user.first_name.0 }}{{ request.user.last_name.0 }}
</div>
{% endif %}
{% elif request.user.staffprofile %}
{% if request.user.staffprofile.image %}
<img src="{{request.user.staffprofile.image.url}}" class="w-full h-full rounded-full object-cover">
{% else %}
<div
class="w-full h-full border border-osiblue bg-osiblue text-white uppercase rounded-full flex justify-center items-center p-1 shadow-md">
{{ request.user.first_name.0 }}{{ request.user.last_name.0 }}
</div>
{% endif %}
{% endif %}
</div>
</div>
<div class="w-full flex flex-col gap-3">
<textarea name="description" rows="8" id="textEditor"
class="w-full bg-white px-3 py-3 border border-gray-200 rounded-b-md outline-none text-gray-500 resize-none"
placeholder="Add Comment..." required></textarea>
<div class="w-full flex flex-col items-end gap-3">
<div class="w-full s:w-fit flex flex-col s:flex-row justify-end items-center gap-3">
<div
class="w-full s:w-[50px] h-[50px] rounded-md bg-gray-50 shadow-md border border-gray-100 flex justify-center items-center p-2 cursor-pointer relative hover:scale-105 duration-300 transition-transform">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="w-5 h-5 text-secondosiblue z-10 absolute pointer-events-none">
<path stroke-linecap="round" stroke-linejoin="round"
d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
</svg>
<input type="file" id="fileupload" name="" placeholder="Select file" multiple
class="opacity-0 absolute top-1/2 left-1/2 w-[25px] -translate-x-1/2 -translate-y-1/2 z-10">
<select id="filePathInput" name="filePath" multiple hidden></select>
</div>
<button
class="w-full s:w-fit bg-secondosiblue border border-secondosiblue text-white rounded-md cursor-pointer hover:bg-white hover:text-secondosiblue duration-300 px-9 py-3">
Send
</button>
<button
class="w-full s:w-fit bg-white border border-secondosiblue text-secondosiblue rounded-md cursor-pointer hover:bg-secondosiblue hover:text-white duration-300 px-9 py-3">
Send as Note
</button>
</div>
<div id="uploaded_files" class="w-full flex flex-col gap-3"></div>
</div>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const ticketId = '{{ticket.id}}';
const wsUrl = `ws://${window.location.host}/ws/ticketroom/${ticketId}/`;
const socket = new WebSocket(wsUrl);
let typingTimeout = null;
socket.onopen = () => {
console.log('WebSocket connection established');
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event_type === 'typing') {
const typingDiv = document.getElementById('typing-notification');
typingDiv.innerHTML = data.html;
} else if (data.event_type === 'stop_typing') {
const typingDiv = document.getElementById('typing-notification');
typingDiv.innerHTML = '';
} else {
const typingDiv = document.getElementById('typing-notification');
const messagesDiv = document.getElementById('messages');
messagesDiv.insertAdjacentHTML('beforeend', data.html);
typingDiv.innerHTML = '';
}
};
socket.onclose = () => {
console.log('WebSocket connection closed');
};
socket.onerror = (error) => {
console.log('WebSocket error:', error);
};
const form = document.getElementById('ticketForm');
const textEditor = document.getElementById('textEditor');
textEditor.addEventListener('input', () => {
clearTimeout(typingTimeout);
const data = {
event_type: 'typing'
};
socket.send(JSON.stringify(data));
typingTimeout = setTimeout(() => {
const stopTypingData = {
event_type: 'stop_typing'
};
socket.send(JSON.stringify(stopTypingData));
}, 2000);
});
form.addEventListener('submit', (event) => {
event.preventDefault();
const formData = new FormData(form);
const description = formData.get('description');
const files = formData.getAll('filePath');
const filePaths = [];
for (const file of files) {
const filePath = `/path/to/uploaded/files/${file.name}`;
filePaths.push(filePath);
}
const data = {
event_type: 'update',
description: description,
filePath: filePaths
};
socket.send(JSON.stringify(data));
form.reset();
});
});
</script>
<div class="w-full mt-5">
<div
class=" bg-gray-200 rounded-t-md flex justify-between items-center text-white text-xl font-bold h-[50px]">
<div class="px-3">
<p class="text-secondosiblue uppercase font-bold">Task</p>
</div>
</div>
<div class="w-full flex flex-col gap-3">
{% if points %}
{% for point in points %}
<p class="pointId" data-point-id="{{ point.id }}" style="display: none;">{{ point.id }}</p>
<div class="w-full flex flex-col gap-1
{% if point.status == 'Completed' %}
bg-green-700
{% elif point.status == 'Working On' %}
bg-orange-500
{% elif point.status == 'Paused' %}
bg-red-500
{% else %}
bg-slate-700
{% endif %}
bg-opacity-50 rounded-md shadow-md p-3 mt-4">
<div class="w-full flex justify-between items-end pb-2 border-b border-gray-200">
<div class="w-[380px]">
{% if point.status == 'Completed' %}
<p class="text-white line-through">{{point.text}}</p>
{% else %}
<p class="text-white">{{point.text}}</p>
{% endif %}
</div>
<div class="flex justify-end items-center gap-2">
{% if point.status == 'Not Completed' or point.status == 'Paused' and not point.status == 'Completed' %}
<a href="{% url 'mark_point_working_on_task_page' point.id task.id %}">
<button
class="w-[40px] h-[40px] rounded-full bg-transparent shadow-md text-white border border-white flex justify-center items-center hover:scale-105 transition-transform duration-300"
id="startPointButton">
<i class="fa fa-play"></i>
</button>
</a>
{% endif %}
{% if point.status == 'Working On' and not point.status == 'Completed' %}
<a href="{% url 'mark_point_paused_task_page' point.id task.id %}">
<button
class="w-[40px] h-[40px] rounded-full bg-transparent shadow-md text-white border border-white justify-center items-center hover:scale-105 transition-transform duration-300"
id="pausePointButton">
<i class="fa fa-pause"></i>
</button>
</a>
{% endif %}
{% if not point.status == 'Completed' and not point.status == 'Paused' %}
<a href="{% url 'mark_point_completed_task_page' point.id task.id %}">
<button
class="w-[40px] h-[40px] rounded-full bg-transparent shadow-md text-white border border-white flex justify-center items-center hover:scale-105 transition-transform duration-300">
<i class="fa fa-check"></i>
</button>
</a>
{% endif %}
{% if point.status == 'Completed' %}
<button
class="w-[40px] h-[40px] rounded-full bg-transparent shadow-md text-white border border-white flex justify-center items-center opacity-30 cursor-default">
<i class="fa fa-check"></i>
</button>
{% endif %}
{% if not point.status == 'Completed' %}
<form method="post" action="{% url 'deletepointmodal' point.id task.id %}">
{% csrf_token %}
<button type="submit"
class="w-[40px] h-[40px] bg-transparent border border-white rounded-full text-white flex justify-center items-center shadow-md hover:scale-105 transition-transform duration-300">
<i class="fa fa-trash"></i>
</button>
</form>
{% endif %}
</div>
</div>
<div class="flex justify-between items-center gap-3 pt-2">
<div class="text-white text-sm">
<p>Total Time:
<span class="font-semibold pointTotalTime">
<span class="hours">{{ point.total_activity_time.0 }}</span> hours,
<span class="minutes">{{ point.total_activity_time.1 }}</span> minutes,
<span class="seconds">{{ point.total_activity_time.2 }}</span> seconds
</span>
</p>
</div>
<div class="w-fit py-1 px-2 bg-white text-secondosiblue text-xs rounded-md shadow-md">
{% if point.status == 'Completed' %}
<p class="text-green-700 opacity-50">Completed</p>
{% elif point.status == 'Working On' %}
<p class="text-orange-500 opacity-50">Working On</p>
{% elif point.status == 'Paused' %}
<p class="text-red-500 opacity-50">Paused</p>
{% else %}
<p class="text-secondosiblue opacity-50">Created</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="w-full flex justify-center items-center p-5 text-secondosiblue text-center">
<p>No Available Points</p>
</div>
{% endif %}
</div>
</div>
</div>
<!---------------------- JS SCRIPTS -------------------->
<script type="text/javascript" src="{% static 'js/tickets/ticket-details.js' %}"></script>
<script type="text/javascript" src="{% static 'js/inputs/text-editor.js' %}"></script>
<script type="text/javascript" src='{% static "js/inputs/file-uploader.js" %}'></script>
{% endblock %}

@ -122,7 +122,7 @@
</button>
<a href="{% url 'ticketdetails' ticket.ticket_number %}">
<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">
@ -231,7 +231,7 @@
</svg>
</button>
<a href="{% url 'ticketdetails' ticket.ticket_number %}">
<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">

@ -57,7 +57,6 @@ urlpatterns = [
#Details Templates
path('customers/<str:customer_id>/', views.customerdetails, name='customerdetails'),
path('tickets/<str:ticket_number>/', views.ticket_details, name='ticketdetails'),
path('businesses/<str:business_id>/', views.businessdetails, name='businessdetails'),
path('staffs/<str:staff_id>/', views.staffdetails, name='userdetails'),
path('projectdetails/<str:project_id>/', views.projectdetails, name='detailed-project'),

@ -19,7 +19,7 @@ from django.core.mail import send_mail
from django.conf import settings
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from customercore .models import *
from support .models import *
from django.db.models import Max
from django.core.paginator import Paginator
@ -544,26 +544,6 @@ def customerdetails(request, customer_id):
return render(request, 'details_templates/customer-details.html', context)
@staff_login_required
def ticket_details(request, ticket_number):
ticket = get_object_or_404(Ticket, ticket_number=ticket_number)
ticket_updates = TicketUpdate.objects.filter(ticket=ticket).order_by('id')
for update in TicketUpdate.objects.filter(ticket=ticket).exclude(added_by=request.user).order_by('id'):
if not TicketRead.objects.filter(ticket_update=update, user=request.user).exists():
TicketRead.objects.create(ticket_update=update, user=request.user, read=True)
last_ticket_status = TicketStatus.objects.filter(ticket=ticket).last()
context = {
'ticket' : ticket,
'ticket_updates': ticket_updates,
'last_ticket_status': last_ticket_status
}
return render(request, 'details_templates/ticket-details.html', context)
@staff_login_required
def businessdetails(request, business_id):
business = get_object_or_404(Business, business_id=business_id)

@ -13,7 +13,7 @@ from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from channels.auth import AuthMiddlewareStack
from osinacore import routing
from support import routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'osinaweb.settings')

@ -41,6 +41,7 @@ LOGIN_URL = 'signin'
INSTALLED_APPS = [
'daphne',
'support',
'rest_framework',
'osinacore',
'customercore',

@ -23,6 +23,7 @@ urlpatterns = [
path('', include('osinacore.urls')),
path('', include('customercore.urls')),
path('', include('billing.urls')),
path('', include('support.urls')),
path('admin/', admin.site.urls),
]

@ -1197,14 +1197,6 @@ video {
height: 150px;
}
.h-\[16px\] {
height: 16px;
}
.h-\[18px\] {
height: 18px;
}
.h-\[20px\] {
height: 20px;
}
@ -1229,10 +1221,6 @@ video {
height: 2px;
}
.h-\[300px\] {
height: 300px;
}
.h-\[305px\] {
height: 305px;
}
@ -3799,14 +3787,6 @@ video {
}
@media (min-width: 650px) {
.s\:mb-0 {
margin-bottom: 0px;
}
.s\:mt-10 {
margin-top: 2.5rem;
}
.s\:mt-5 {
margin-top: 1.25rem;
}
@ -3839,10 +3819,6 @@ video {
height: 55px;
}
.s\:h-\[60px\] {
height: 60px;
}
.s\:h-\[90px\] {
height: 90px;
}
@ -3879,18 +3855,10 @@ video {
width: 500px;
}
.s\:w-\[50px\] {
width: 50px;
}
.s\:w-\[550px\] {
width: 550px;
}
.s\:w-\[60px\] {
width: 60px;
}
.s\:w-\[85\%\] {
width: 85%;
}
@ -3912,10 +3880,6 @@ video {
flex-direction: row;
}
.s\:items-center {
align-items: center;
}
.s\:justify-end {
justify-content: flex-end;
}
@ -3992,10 +3956,6 @@ video {
grid-column: span 2 / span 2;
}
.md\:mb-0 {
margin-bottom: 0px;
}
.md\:block {
display: block;
}
@ -4033,10 +3993,6 @@ video {
flex-direction: row;
}
.md\:items-center {
align-items: center;
}
.md\:text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

@ -0,0 +1,10 @@
from django.contrib import admin
from .models import *
# Register your models here.
admin.site.register(Ticket)
admin.site.register(TicketStatus)
admin.site.register(TicketRead)
admin.site.register(TicketUpdate)
admin.site.register(TicketAttachment)
admin.site.register(TicketUpdateReaction)

@ -0,0 +1,6 @@
from django.apps import AppConfig
class SupportConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'support'

@ -1,6 +1,6 @@
from channels.generic.websocket import WebsocketConsumer
from django.shortcuts import get_object_or_404
from customercore.models import *
from .models import *
import json
from django.template.loader import render_to_string
from asgiref.sync import async_to_sync
@ -47,6 +47,18 @@ class TicketRoomConsumer(WebsocketConsumer):
async_to_sync(self.channel_layer.group_send)(
self.ticket_number, event
)
elif event_type == 'update_reaction':
reaction = text_data_json['reaction']
update_id = text_data_json['update_id']
event = {
'type': 'reaction_handler',
'update_id': update_id,
'reaction': reaction,
'user': self.scope['user']
}
async_to_sync(self.channel_layer.group_send)(
self.ticket_number, event
)
else:
body = text_data_json['description']
file_paths = text_data_json['filePath']
@ -79,7 +91,7 @@ class TicketRoomConsumer(WebsocketConsumer):
}
html = render_to_string("details_templates/new-ticket-message.html", context=context)
html = render_to_string("details_templates/partials/new-ticket-message.html", context=context)
self.send(text_data=json.dumps({
'event_type': 'update',
'html': html
@ -89,7 +101,7 @@ class TicketRoomConsumer(WebsocketConsumer):
context = {
'user': event['user']
}
html = render_to_string("details_templates/typing-message.html", context=context)
html = render_to_string("details_templates/partials/typing-message.html", context=context)
self.send(text_data=json.dumps({
'event_type': 'typing',
'html': html
@ -99,3 +111,37 @@ class TicketRoomConsumer(WebsocketConsumer):
self.send(text_data=json.dumps({
'event_type': 'stop_typing'
}))
def reaction_handler(self, event):
update_id = event['update_id']
reaction = event['reaction']
user = self.user
update = TicketUpdate.objects.get(id=update_id)
existing_reaction = TicketUpdateReaction.objects.filter(ticket_update=update, customer=user).first()
new_reaction = None
if existing_reaction:
# If the existing reaction type is equal to the new reaction, delete it
if existing_reaction.reaction == reaction:
existing_reaction.delete()
else:
# If not, delete all previous reactions and add a new one
TicketUpdateReaction.objects.filter(ticket_update=update, customer=user).delete()
new_reaction = TicketUpdateReaction.objects.create(
ticket_update=update,
reaction=reaction,
customer=user
)
else:
# If there's no existing reaction, simply add the new one
new_reaction = TicketUpdateReaction.objects.create(
ticket_update=update,
reaction=reaction,
customer=user
)
self.send(text_data=json.dumps({
'event_type': 'reaction',
'update_id': update_id,
'reaction': new_reaction.reaction if new_reaction else None
}))

@ -0,0 +1,22 @@
from functools import wraps
from django.shortcuts import redirect
from osinacore.models import *
from django.http import QueryDict
def ticket_member_required(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
# Check if the user is logged in and is either a customer or a staff member
if not request.user.is_authenticated or (
not CustomerProfile.objects.filter(user=request.user) and
not StaffProfile.objects.filter(user=request.user)):
# Capture the 'next' page
next_page = request.build_absolute_uri()
query_params = QueryDict(mutable=True)
query_params['next'] = next_page
query_string = query_params.urlencode()
login_url = f"/login/?{query_string}" # Change the login URL as per your project setup
return redirect(login_url)
return view_func(request, *args, **kwargs)
return _wrapped_view

@ -0,0 +1,92 @@
# Generated by Django 4.2.5 on 2024-06-27 19:02
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('billing', '0052_alter_orderstatus_status'),
('osinacore', '0085_rename_date_staffposition_start_date_and_more'),
]
operations = [
migrations.CreateModel(
name='Ticket',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ticket_number', models.CharField(blank=True, max_length=400)),
('title', models.CharField(max_length=400)),
('description', models.TextField(blank=True, null=True)),
('status', models.CharField(choices=[('Open', 'Open'), ('Working On', 'Working On'), ('Closed', 'Closed')], max_length=50, null=True)),
('regarding', models.CharField(choices=[('General/Account/Billing', 'General/Account/Billing'), ('Project/Product', 'Project/Product')], max_length=50, null=True)),
('opened_date', models.DateTimeField()),
('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='osinacore.customerprofile')),
('departments', models.ManyToManyField(blank=True, null=True, to='osinacore.department')),
('opened_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='billing.item')),
('project', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='osinacore.project')),
('ticket_members', models.ManyToManyField(blank=True, null=True, related_name='ticket_members', to='osinacore.staffprofile')),
],
),
migrations.CreateModel(
name='TicketUpdate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField(blank=True, null=True)),
('date_added', models.DateTimeField()),
('added_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='support.ticket')),
],
),
migrations.CreateModel(
name='TicketUpdateReaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reaction', models.CharField(choices=[('Happy', 'Happy'), ('Indifferent', 'Indifferent'), ('Sad', 'Sad')], max_length=50, null=True)),
('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('ticket_update', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='support.ticketupdate')),
],
),
migrations.CreateModel(
name='TicketTask',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='osinacore.task')),
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='support.ticketupdate')),
],
),
migrations.CreateModel(
name='TicketStatus',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('Open', 'Open'), ('Working On', 'Working On'), ('Closed', 'Closed')], max_length=50, null=True)),
('date_added', models.DateTimeField()),
('added_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='support.ticket')),
],
),
migrations.CreateModel(
name='TicketRead',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('read', models.BooleanField(default=False)),
('ticket_update', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='support.ticketupdate')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='TicketAttachment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file_path', models.CharField(max_length=255, null=True)),
('ticket', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='support.ticket')),
('ticket_update', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='support.ticketupdate')),
],
),
]

@ -0,0 +1,93 @@
from django.db import models
# Create your models here.
from django.db import models
from billing.models import *
from django.utils import timezone
# Create your models here.
class Ticket(models.Model):
REGARDING_CHOICES = (
('General/Account/Billing', 'General/Account/Billing'),
('Project/Product', 'Project/Product'),
)
STATUS_CHOICES = (
('Open', 'Open'),
('Working On', 'Working On'),
('Closed', 'Closed'),
)
ticket_number = models.CharField(max_length=400, blank=True)
title = models.CharField(max_length=400)
description = models.TextField(null=True, blank=True)
status = models.CharField(max_length=50, choices=STATUS_CHOICES, null=True)
regarding = models.CharField(max_length=50, choices=REGARDING_CHOICES, null=True)
project = models.ForeignKey(Project, on_delete=models.SET_NULL, blank=True, null=True)
product = models.ForeignKey(Item, on_delete=models.SET_NULL, blank=True, null=True)
departments = models.ManyToManyField(Department, null=True, blank=True)
opened_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
opened_date = models.DateTimeField()
customer = models.ForeignKey(CustomerProfile, on_delete=models.SET_NULL, null=True)
ticket_members = models.ManyToManyField(StaffProfile, null=True, blank=True, related_name='ticket_members')
def save(self, *args, **kwargs):
if not self.ticket_number:
last_ticket = Ticket.objects.filter(opened_date__year=timezone.now().year).order_by('-ticket_number').first()
if last_ticket and last_ticket.ticket_number:
last_ticket_number = int(last_ticket.ticket_number[-4:])
new_ticket_number = last_ticket_number + 1
else:
new_ticket_number = 1
current_year_last_two_digits = str(timezone.now().year)[-2:]
self.ticket_number = f"{current_year_last_two_digits}{new_ticket_number:04}"
super().save(*args, **kwargs)
class TicketStatus(models.Model):
STATUS_CHOICES = (
('Open', 'Open'),
('Working On', 'Working On'),
('Closed', 'Closed'),
)
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
status = models.CharField(max_length=50, choices=STATUS_CHOICES, null=True)
added_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
date_added = models.DateTimeField()
class TicketUpdate(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
description = models.TextField(null=True, blank=True)
added_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
date_added = models.DateTimeField()
class TicketRead(models.Model):
ticket_update = models.ForeignKey(TicketUpdate, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
read = models.BooleanField(default=False)
class TicketAttachment(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, null=True, blank=True)
ticket_update = models.ForeignKey(TicketUpdate, on_delete=models.CASCADE, null=True, blank=True)
file_path = models.CharField(max_length=255, null=True)
class TicketUpdateReaction(models.Model):
REACTION_CHOICES = (
('Happy', 'Happy'),
('Indifferent', 'Indifferent'),
('Sad', 'Sad'),
)
customer = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
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)

Binary file not shown.

@ -1,7 +1,7 @@
<div id="messages" hx-swap-oob="beforeend">
<div class="fade-in-up">
{% include 'details_templates/ticket-message.html' %}
{% include 'details_templates/partials/ticket-message.html' %}
</div>
<style>

@ -0,0 +1,50 @@
{% load static %}
{% if update.added_by.staffprofile %}
<div class="w-full border-t border-gray-200 pt-5 flex justify-start items-center gap-3 p-5 {% if not user.customerprofile %} hidden {% endif %}">
<a class="text-secondosiblue font-light cursor-pointer hover:text-gray-500 duration-300">How
did I do?</a>
<div class="flex justify-start items-center gap-2">
<button data-update-id="{{ update.id }}" data-reaction="Happy"
class="reaction-button w-fit h-fit rounded-full {% if update.ticketupdatereaction_set.all.last.reaction == 'Happy' %} border-2 border-secondosiblue {% endif %}">
<img src="{% static 'images/icons/happy-icon.png' %}"
class="w-[30px] h-[30px] rounded-full cursor-pointer hover:scale-105 duration-300 transition-transform pointer-events-none">
</button>
<button data-update-id="{{ update.id }}" data-reaction="Indifferent"
class="reaction-button w-fit h-fit rounded-full {% if update.ticketupdatereaction_set.all.last.reaction == 'Indifferent' %} border-2 border-secondosiblue {% endif %}">
<img src="{% static 'images/icons/neutral-icon.png' %}"
class="w-[30px] h-[30px] rounded-full cursor-pointer hover:scale-105 duration-300 transition-transform pointer-events-none">
</button>
<button data-update-id="{{ update.id }}" data-reaction="Sad"
class="reaction-button w-fit h-fit rounded-full {% if update.ticketupdatereaction_set.all.last.reaction == 'Sad' %} border-2 border-secondosiblue {% endif %}">
<img src="{% static 'images/icons/unhappy-icon.png' %}"
class="w-[30px] h-[30px] rounded-full cursor-pointer hover:scale-105 duration-300 transition-transform pointer-events-none">
</button>
</div>
</div>
<div id="submitted-reactions" class="w-full border-t border-gray-200 pt-5 flex justify-start items-center gap-3 p-5 {% if user.customerprofile %} hidden {% endif %}">
<div class="flex justify-start items-center gap-2">
<button data-reaction="Happy"
class="submittedreaction-button w-fit h-fit rounded-full {% if not update.ticketupdatereaction_set.all.last.reaction == 'Happy' %} hidden {% endif %} ">
<img src="{% static 'images/icons/happy-icon.png' %}"
class="w-[30px] h-[30px] rounded-full cursor-pointer hover:scale-105 duration-300 transition-transform pointer-events-none">
</button>
<button data-reaction="Indifferent"
class="submittedreaction-button w-fit h-fit rounded-full {% if not update.ticketupdatereaction_set.all.last.reaction == 'Indifferent' %} hidden {% endif %}">
<img src="{% static 'images/icons/neutral-icon.png' %}"
class=" w-[30px] h-[30px] rounded-full cursor-pointer hover:scale-105 duration-300 transition-transform pointer-events-none">
</button>
<button data-reaction="Sad"
class="submittedreaction-button w-fit h-fit rounded-full {% if not update.ticketupdatereaction_set.all.last.reaction == 'Sad' %} hidden {% endif %} ">
<img src="{% static 'images/icons/unhappy-icon.png' %}"
class="w-[30px] h-[30px] rounded-full cursor-pointer hover:scale-105 duration-300 transition-transform pointer-events-none">
</button>
</div>
</div>
{% endif %}

@ -1,4 +1,4 @@
<div class="flex gap-3 mb-10">
<div class="flex gap-3 mb-10" id="update-{{ update.id }}">
<div>
<div class="w-[45px] s:w-[60px] h-[45px] s:h-[60px] rounded-full shadow-md border border-gray-100">
{% if update.added_by.customerprofile %}
@ -65,5 +65,6 @@
{% endif %}
</div>
{% include 'details_templates/partials/ticket-message-reactions.html' %}
</div>
</div>

@ -0,0 +1,400 @@
{% extends base_template %}
{%load static%}
{% block content %}
<!-- TEXT EDITOR -->
<style>
@import url(https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/5.3.45/css/materialdesignicons.min.css);
</style>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.js" defer></script>
<script src="https://unpkg.com/htmx.org/dist/htmx.js"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
<div class="{% if request.user.customerprofile %} w-full {% else %}w-full xxlg1:w-[75%] {% endif %} bg-white h-fit rounded-md shadow-md p-5">
<div class="w-full h-fit flex flex-col gap-2 bg-gray-100 shadow-md rounded-md px-3 py-3">
<div class="w-full flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-5 md:mb-0">
<p class="text-secondosiblue text-[20px]">Ticket <span class="font-semibold">#{{ticket.ticket_number}}</span></p>
<div class="w-full s:w-fit flex flex-col s:flex-row justify-end items-center gap-2">
<button class="w-full s:w-fit px-3 py-2 bg-osiblue border border-osiblue text-white cursor-pointer duration-300 hover:bg-white hover:text-osiblue rounded-md updateTicketStatusButton" data-modal-url="{% url 'edit-ticket-status-modal' ticket.id %}">
Update Status
</button>
<button class="w-full s:w-fit px-3 py-2 bg-osiblue border border-osiblue text-white cursor-pointer duration-300 hover:bg-white hover:text-osiblue rounded-md">
Add Task
</button>
</div>
</div>
{% if last_ticket_status.status == 'Open' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-green-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Opened by {{last_ticket_status.added_by.first_name}} on
{{ last_ticket_status.date_added|date:"d F Y, h:i A" }}</p>
</div>
{% elif last_ticket_status.status == 'Working On' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-yellow-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Updated to 'Working On' by
{{last_ticket_status.added_by.first_name}} on {{ last_ticket_status.date_added|date:"d F Y, h:i A" }}</p>
</div>
{% elif last_ticket_status.status == 'Closed' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-red-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Closed by {{last_ticket_status.added_by.first_name}} on
{{ last_ticket_status.date_added|date:"d F Y, h:i A" }}</p>
</div>
{% endif %}
<div class="w-full mt-3">
<p class="text-gray-500 font-light text-sm leading-7">{{ticket.description}}
</p>
</div>
{% if ticket.ticketattachment_set.all %}
<div class="w-full flex flex-wrap justify-end items-center gap-3">
{% for file in ticket.ticketattachment_set.all %}
<div class="flex items-center gap-1 text-secondosiblue hover:text-gray-500 duration-300 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4 text-secondosiblue">
<path stroke-linecap="round" stroke-linejoin="round"
d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
</svg>
<a href="https://osina.ositcom.com/{{file.file_path}}" target="_blank" class="text-sm">{{ file.file_path | cut:"static/images/uploaded_ticket_files/" }}{% if not forloop.last %}, {% endif %}</a>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<div class="w-full flex flex-col mt-5 s:mt-10">
<!-- REPLY 1 -->
<div id="messages">
{% for update in ticket_updates %}
{% include 'details_templates/partials/ticket-message.html' %}
{% endfor %}
</div>
<div id="typing-notification"></div>
<!-- REPLYING SECTION -->
<form id="ticketForm" class="flex gap-3"
enctype="multipart/form-data">
{% csrf_token %}
<div>
<div class="w-[45px] s:w-[60px] h-[45px] s:h-[60px] rounded-full shadow-md border border-gray-100">
{% if request.user.customerprofile %}
{% if request.user.customerprofile.image %}
<img src="{{request.user.customerprofile.image.url}}"
class="w-full h-full rounded-full object-cover">
{% else %}
<div
class="w-full h-full border border-secondosiblue bg-secondosiblue text-white uppercase rounded-full flex justify-center items-center p-1 shadow-md">
{{ request.user.first_name.0 }}{{ request.user.last_name.0 }}
</div>
{% endif %}
{% elif request.user.staffprofile %}
{% if request.user.staffprofile.image %}
<img src="{{request.user.staffprofile.image.url}}" class="w-full h-full rounded-full object-cover">
{% else %}
<div
class="w-full h-full border border-osiblue bg-osiblue text-white uppercase rounded-full flex justify-center items-center p-1 shadow-md">
{{ request.user.first_name.0 }}{{ request.user.last_name.0 }}
</div>
{% endif %}
{% endif %}
</div>
</div>
<div class="w-full flex flex-col gap-3">
<textarea name="description" rows="8" id="textEditor"
class="w-full bg-white px-3 py-3 border border-gray-200 rounded-b-md outline-none text-gray-500 resize-none"
placeholder="Add Comment..." required></textarea>
<div class="w-full flex flex-col items-end gap-3">
<div class="w-full s:w-fit flex flex-col s:flex-row justify-end items-center gap-3">
<div
class="w-full s:w-[50px] h-[50px] rounded-md bg-gray-50 shadow-md border border-gray-100 flex justify-center items-center p-2 cursor-pointer relative hover:scale-105 duration-300 transition-transform">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="w-5 h-5 text-secondosiblue z-10 absolute pointer-events-none">
<path stroke-linecap="round" stroke-linejoin="round"
d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
</svg>
<input type="file" id="fileupload" name="" placeholder="Select file" multiple
class="opacity-0 absolute top-1/2 left-1/2 w-[25px] -translate-x-1/2 -translate-y-1/2 z-10">
<select id="filePathInput" name="filePath" multiple hidden></select>
</div>
<button
class="w-full s:w-fit bg-secondosiblue border border-secondosiblue text-white rounded-md cursor-pointer hover:bg-white hover:text-secondosiblue duration-300 px-9 py-3">
Send
</button>
<button
class="w-full s:w-fit bg-white border border-secondosiblue text-secondosiblue rounded-md cursor-pointer hover:bg-secondosiblue hover:text-white duration-300 px-9 py-3">
Send as Note
</button>
</div>
<div id="uploaded_files" class="w-full flex flex-col gap-3"></div>
</div>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const ticketId = '{{ticket.id}}';
const wsUrl = `ws://${window.location.host}/ws/ticketroom/${ticketId}/`;
const socket = new WebSocket(wsUrl);
let typingTimeout = null;
socket.onopen = () => {
console.log('WebSocket connection established');
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event_type === 'typing') {
const typingDiv = document.getElementById('typing-notification');
typingDiv.innerHTML = data.html;
} else if (data.event_type === 'stop_typing') {
const typingDiv = document.getElementById('typing-notification');
typingDiv.innerHTML = '';
} else if (data.event_type === 'reaction') {
console.log(data.reaction);
const updateElement = document.getElementById(`update-${data.update_id}`);
updateElement.querySelectorAll('.reaction-button').forEach(button => {
button.classList.remove('border-2', 'border-secondosiblue');
if (button.dataset.reaction === data.reaction) {
button.classList.add('border-2', 'border-secondosiblue');
}
});
updateElement.querySelectorAll('.submittedreaction-button').forEach(button => {
button.classList.add('hidden');
if (button.dataset.reaction === data.reaction) {
button.classList.remove('hidden');
}
});
} else {
const typingDiv = document.getElementById('typing-notification');
const messagesDiv = document.getElementById('messages');
messagesDiv.insertAdjacentHTML('beforeend', data.html);
typingDiv.innerHTML = '';
}
};
socket.onclose = () => {
console.log('WebSocket connection closed');
};
socket.onerror = (error) => {
console.log('WebSocket error:', error);
};
const form = document.getElementById('ticketForm');
const textEditor = document.getElementById('textEditor');
textEditor.addEventListener('input', () => {
clearTimeout(typingTimeout);
const data = {
event_type: 'typing'
};
socket.send(JSON.stringify(data));
typingTimeout = setTimeout(() => {
const stopTypingData = {
event_type: 'stop_typing'
};
socket.send(JSON.stringify(stopTypingData));
}, 1500);
});
form.addEventListener('submit', (event) => {
event.preventDefault();
const formData = new FormData(form);
const description = formData.get('description');
const files = formData.getAll('filePath');
const filePaths = [];
for (const file of files) {
const filePath = `/path/to/uploaded/files/${file.name}`;
filePaths.push(filePath);
}
const data = {
event_type: 'update',
description: description,
filePath: filePaths
};
socket.send(JSON.stringify(data));
form.reset();
});
document.addEventListener('click', (event) => {
if (event.target.classList.contains('reaction-button')) {
const updateId = event.target.dataset.updateId;
const reaction = event.target.dataset.reaction;
const data = {
event_type: 'update_reaction',
update_id: updateId,
reaction: reaction
};
socket.send(JSON.stringify(data));
}
});
});
</script>
<div class="w-full mt-5">
<div
class=" bg-gray-200 rounded-t-md flex justify-between items-center text-white text-xl font-bold h-[50px]">
<div class="px-3">
<p class="text-secondosiblue uppercase font-bold">Task</p>
</div>
</div>
<div class="w-full flex flex-col gap-3">
{% if points %}
{% for point in points %}
<p class="pointId" data-point-id="{{ point.id }}" style="display: none;">{{ point.id }}</p>
<div class="w-full flex flex-col gap-1
{% if point.status == 'Completed' %}
bg-green-700
{% elif point.status == 'Working On' %}
bg-orange-500
{% elif point.status == 'Paused' %}
bg-red-500
{% else %}
bg-slate-700
{% endif %}
bg-opacity-50 rounded-md shadow-md p-3 mt-4">
<div class="w-full flex justify-between items-end pb-2 border-b border-gray-200">
<div class="w-[380px]">
{% if point.status == 'Completed' %}
<p class="text-white line-through">{{point.text}}</p>
{% else %}
<p class="text-white">{{point.text}}</p>
{% endif %}
</div>
<div class="flex justify-end items-center gap-2">
{% if point.status == 'Not Completed' or point.status == 'Paused' and not point.status == 'Completed' %}
<a href="{% url 'mark_point_working_on_task_page' point.id task.id %}">
<button
class="w-[40px] h-[40px] rounded-full bg-transparent shadow-md text-white border border-white flex justify-center items-center hover:scale-105 transition-transform duration-300"
id="startPointButton">
<i class="fa fa-play"></i>
</button>
</a>
{% endif %}
{% if point.status == 'Working On' and not point.status == 'Completed' %}
<a href="{% url 'mark_point_paused_task_page' point.id task.id %}">
<button
class="w-[40px] h-[40px] rounded-full bg-transparent shadow-md text-white border border-white justify-center items-center hover:scale-105 transition-transform duration-300"
id="pausePointButton">
<i class="fa fa-pause"></i>
</button>
</a>
{% endif %}
{% if not point.status == 'Completed' and not point.status == 'Paused' %}
<a href="{% url 'mark_point_completed_task_page' point.id task.id %}">
<button
class="w-[40px] h-[40px] rounded-full bg-transparent shadow-md text-white border border-white flex justify-center items-center hover:scale-105 transition-transform duration-300">
<i class="fa fa-check"></i>
</button>
</a>
{% endif %}
{% if point.status == 'Completed' %}
<button
class="w-[40px] h-[40px] rounded-full bg-transparent shadow-md text-white border border-white flex justify-center items-center opacity-30 cursor-default">
<i class="fa fa-check"></i>
</button>
{% endif %}
{% if not point.status == 'Completed' %}
<form method="post" action="{% url 'deletepointmodal' point.id task.id %}">
{% csrf_token %}
<button type="submit"
class="w-[40px] h-[40px] bg-transparent border border-white rounded-full text-white flex justify-center items-center shadow-md hover:scale-105 transition-transform duration-300">
<i class="fa fa-trash"></i>
</button>
</form>
{% endif %}
</div>
</div>
<div class="flex justify-between items-center gap-3 pt-2">
<div class="text-white text-sm">
<p>Total Time:
<span class="font-semibold pointTotalTime">
<span class="hours">{{ point.total_activity_time.0 }}</span> hours,
<span class="minutes">{{ point.total_activity_time.1 }}</span> minutes,
<span class="seconds">{{ point.total_activity_time.2 }}</span> seconds
</span>
</p>
</div>
<div class="w-fit py-1 px-2 bg-white text-secondosiblue text-xs rounded-md shadow-md">
{% if point.status == 'Completed' %}
<p class="text-green-700 opacity-50">Completed</p>
{% elif point.status == 'Working On' %}
<p class="text-orange-500 opacity-50">Working On</p>
{% elif point.status == 'Paused' %}
<p class="text-red-500 opacity-50">Paused</p>
{% else %}
<p class="text-secondosiblue opacity-50">Created</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="w-full flex justify-center items-center p-5 text-secondosiblue text-center">
<p>No Available Points</p>
</div>
{% endif %}
</div>
</div>
</div>
<!---------------------- JS SCRIPTS -------------------->
<script type="text/javascript" src="{% static 'js/tickets/ticket-details.js' %}"></script>
<script type="text/javascript" src="{% static 'js/inputs/text-editor.js' %}"></script>
<script type="text/javascript" src='{% static "js/inputs/file-uploader.js" %}'></script>
{% endblock %}

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,27 @@
"""osinaweb URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.urls import path, include
from support import views
from django.conf.urls.static import static
from django.conf import settings
urlpatterns = [
path('tickets/<str:ticket_number>/', views.ticket_room, name='ticketroom'),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

@ -0,0 +1,28 @@
from django.shortcuts import render, get_object_or_404
from .models import *
from .decorators import *
# Create your views here.
@ticket_member_required
def ticket_room(request, ticket_number):
if hasattr(request.user, 'customerprofile'):
base_template = "customer_main.html"
else:
base_template = "main.html"
ticket = get_object_or_404(Ticket, ticket_number=ticket_number)
ticket_updates = TicketUpdate.objects.filter(ticket=ticket).order_by('id')
for update in TicketUpdate.objects.filter(ticket=ticket).exclude(added_by=request.user).order_by('id'):
if not TicketRead.objects.filter(ticket_update=update, user=request.user).exists():
TicketRead.objects.create(ticket_update=update, user=request.user, read=True)
last_ticket_status = TicketStatus.objects.filter(ticket=ticket).last()
context = {
'base_template': base_template,
'ticket' : ticket,
'ticket_updates': ticket_updates,
'last_ticket_status': last_ticket_status,
}
return render(request, 'details_templates/ticket-room.html', context)
Loading…
Cancel
Save