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.template.loader import render_to_string
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.conf import settings from django.conf import settings
from support .models import *
@customer_login_required @customer_login_required

@ -1,10 +1 @@
from django.contrib import admin 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 billing.models import *
from osinacore.models import * from osinacore.models import *
from customercore.models import * from customercore.models import *
from support.models import *
from django.db.models import Count, Q from django.db.models import Count, Q
def utilities(request): 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 %} {% 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-white rounded-md h-fit shadow-md p-5">
<h1 class="text-3xl text-secondosiblue text-center font-semibold"> <h1 class="text-3xl text-secondosiblue text-center font-semibold">
Create Ticket Create Ticket

@ -2,7 +2,7 @@
{%load static%} {%load static%}
{% block content %} {% 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"> <!--<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 %} {% 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-white rounded-md h-fit shadow-md p-5">
<div <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"> 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 %} {% 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-white rounded-md h-fit shadow-md p-5">
<div <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 %}"> 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%} {%load static%}
{% block content %} {% 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 --> <!-- INVOICES -->
<div class="w-full bg-white rounded-md h-fit shadow-md p-5"> <div class="w-full bg-white rounded-md h-fit shadow-md p-5">
<h1 class="text-secondosiblue text-[25px]">My Invoices</h1> <h1 class="text-secondosiblue text-[25px]">My Invoices</h1>

@ -3,7 +3,7 @@
{% block content %} {% 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-white rounded-md h-fit shadow-md p-5">
<h1 class="text-secondosiblue text-[25px]">My Orders</h1> <h1 class="text-secondosiblue text-[25px]">My Orders</h1>

@ -3,7 +3,7 @@
{% block content %} {% 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-white rounded-md h-fit shadow-md p-5">
<h1 class="text-secondosiblue text-[25px]">My Projects</h1> <h1 class="text-secondosiblue text-[25px]">My Projects</h1>

@ -3,7 +3,7 @@
{% block content %} {% 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-white rounded-md h-fit shadow-md p-5">
<h1 class="text-secondosiblue text-[25px]">My Tickets</h1> <h1 class="text-secondosiblue text-[25px]">My Tickets</h1>
@ -61,10 +61,10 @@
<tbody class="bg-white divide-y divide-gray-200"> <tbody class="bg-white divide-y divide-gray-200">
{% for ticket in open_tickets %} {% 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"> <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"> <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> <p class="text-secondosiblue">{{ticket.title }}</p>
</a> </a>
@ -139,9 +139,9 @@
<tbody class="bg-white divide-y divide-gray-200"> <tbody class="bg-white divide-y divide-gray-200">
{% for ticket in closed_tickets %} {% 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"> <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> <p class="text-secondosiblue cursor-pointer">{{ticket.title }}</p>
</a> </a>
</td> </td>

@ -3,7 +3,7 @@
{% block content %} {% 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-white rounded-md h-fit shadow-md p-5">
<div <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"> 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> <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="w-full h-fit bg-white rounded-md shadow-md">
<div class="grid grid-cols-1 xxlg1:grid-cols-2"> <div class="grid grid-cols-1 xxlg1:grid-cols-2">

@ -12,7 +12,7 @@
<p id="paymentId" class="hidden">{{payment.id}}</p> <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="w-full h-fit bg-white rounded-md shadow-md">
<div class="grid grid-cols-1 xxlg1:grid-cols-2"> <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="w-full h-fit bg-white rounded-md shadow-md">
<div class="grid grid-cols-1 xxlg1:grid-cols-2"> <div class="grid grid-cols-1 xxlg1:grid-cols-2">
<div <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="w-full h-fit bg-white rounded-md shadow-md">
<div class="grid grid-cols-1 xxlg1:grid-cols-2"> <div class="grid grid-cols-1 xxlg1:grid-cols-2">
<div <div

@ -8,7 +8,7 @@
{% block content %} {% 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"> <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> <p class="text-osiblue uppercase text-xl s:text-3xl font-poppinsBold text-center">Cloud VPS Hosting</p>

@ -8,7 +8,7 @@
{% block content %} {% 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"> <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]"> <img src="{% static 'images/cpanellogo.png' %}" class="w-[180px] s:w-[250px]">

@ -8,7 +8,7 @@
{% block content %} {% 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"> <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> <p class="text-osiblue uppercase text-xl s:text-3xl font-poppinsBold text-center">Dedicated CPU Servers</p>

@ -8,7 +8,7 @@
{% block content %} {% 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"> <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]"> <img src="{% static 'images/ositcom_logos/osicardblue.png' %}" class="w-[180px] s:w-[250px]">

@ -7,7 +7,7 @@
{% block content %} {% 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"> <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]"> <img src="{% static 'images/ositcom_logos/osimenublue.png' %}" class="w-[180px] s:w-[250px]">

@ -8,7 +8,7 @@
{% block content %} {% 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"> <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> <p class="text-osiblue uppercase text-xl s:text-3xl font-poppinsBold text-center">Shared Hosting</p>

@ -20,7 +20,6 @@ urlpatterns = [
# DETAILS # 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-orders/<str:order_id>/', views.customer_order_details, name='customerorderdetails'),
path('my-projects/<str:project_id>/', views.customer_project_details, name='customerprojectdetails'), 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.hashers import check_password
from django.contrib.auth import update_session_auth_hash, logout from django.contrib.auth import update_session_auth_hash, logout
import json 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 # PRODUCTS

Binary file not shown.

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

Binary file not shown.

@ -2,7 +2,7 @@
{%load static%} {%load static%}
{% block content %} {% 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"> <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"> <h1 class="text-3xl text-secondosiblue text-center font-semibold">
Add Business Add Business

@ -155,11 +155,11 @@
<!-- TABLE BODY --> <!-- TABLE BODY -->
{% for ticket in customer_open_tickets %} {% for ticket in customer_open_tickets %}
<tbody class="bg-white divide-y divide-gray-200"> <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"> 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"> <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"> <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> <p class="text-secondosiblue">{{ticket.title }}</p>
</a> </a>

@ -562,7 +562,7 @@
<!-- BODY --> <!-- BODY -->
<div class="w-full mb-5"> <div class="w-full px-5 s:px-9 mb-5">
{% block content %} {% block content %}
replace me replace me
{% endblock content %} {% 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> </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" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" stroke-width="1.5" stroke="currentColor"
class="w-[18px] text-fifthosiblue hover:scale-110 duration-500 transition-transform"> class="w-[18px] text-fifthosiblue hover:scale-110 duration-500 transition-transform">
@ -231,7 +231,7 @@
</svg> </svg>
</button> </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" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" stroke-width="1.5" stroke="currentColor"
class="w-[18px] text-fifthosiblue hover:scale-110 duration-500 transition-transform"> class="w-[18px] text-fifthosiblue hover:scale-110 duration-500 transition-transform">

@ -57,7 +57,6 @@ urlpatterns = [
#Details Templates #Details Templates
path('customers/<str:customer_id>/', views.customerdetails, name='customerdetails'), 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('businesses/<str:business_id>/', views.businessdetails, name='businessdetails'),
path('staffs/<str:staff_id>/', views.staffdetails, name='userdetails'), path('staffs/<str:staff_id>/', views.staffdetails, name='userdetails'),
path('projectdetails/<str:project_id>/', views.projectdetails, name='detailed-project'), 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.conf import settings
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode 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.db.models import Max
from django.core.paginator import Paginator from django.core.paginator import Paginator
@ -544,26 +544,6 @@ def customerdetails(request, customer_id):
return render(request, 'details_templates/customer-details.html', context) 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 @staff_login_required
def businessdetails(request, business_id): def businessdetails(request, business_id):
business = get_object_or_404(Business, business_id=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.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator from channels.security.websocket import AllowedHostsOriginValidator
from channels.auth import AuthMiddlewareStack from channels.auth import AuthMiddlewareStack
from osinacore import routing from support import routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'osinaweb.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'osinaweb.settings')

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

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

@ -1197,14 +1197,6 @@ video {
height: 150px; height: 150px;
} }
.h-\[16px\] {
height: 16px;
}
.h-\[18px\] {
height: 18px;
}
.h-\[20px\] { .h-\[20px\] {
height: 20px; height: 20px;
} }
@ -1229,10 +1221,6 @@ video {
height: 2px; height: 2px;
} }
.h-\[300px\] {
height: 300px;
}
.h-\[305px\] { .h-\[305px\] {
height: 305px; height: 305px;
} }
@ -3799,14 +3787,6 @@ video {
} }
@media (min-width: 650px) { @media (min-width: 650px) {
.s\:mb-0 {
margin-bottom: 0px;
}
.s\:mt-10 {
margin-top: 2.5rem;
}
.s\:mt-5 { .s\:mt-5 {
margin-top: 1.25rem; margin-top: 1.25rem;
} }
@ -3839,10 +3819,6 @@ video {
height: 55px; height: 55px;
} }
.s\:h-\[60px\] {
height: 60px;
}
.s\:h-\[90px\] { .s\:h-\[90px\] {
height: 90px; height: 90px;
} }
@ -3879,18 +3855,10 @@ video {
width: 500px; width: 500px;
} }
.s\:w-\[50px\] {
width: 50px;
}
.s\:w-\[550px\] { .s\:w-\[550px\] {
width: 550px; width: 550px;
} }
.s\:w-\[60px\] {
width: 60px;
}
.s\:w-\[85\%\] { .s\:w-\[85\%\] {
width: 85%; width: 85%;
} }
@ -3912,10 +3880,6 @@ video {
flex-direction: row; flex-direction: row;
} }
.s\:items-center {
align-items: center;
}
.s\:justify-end { .s\:justify-end {
justify-content: flex-end; justify-content: flex-end;
} }
@ -3992,10 +3956,6 @@ video {
grid-column: span 2 / span 2; grid-column: span 2 / span 2;
} }
.md\:mb-0 {
margin-bottom: 0px;
}
.md\:block { .md\:block {
display: block; display: block;
} }
@ -4033,10 +3993,6 @@ video {
flex-direction: row; flex-direction: row;
} }
.md\:items-center {
align-items: center;
}
.md\:text-3xl { .md\:text-3xl {
font-size: 1.875rem; font-size: 1.875rem;
line-height: 2.25rem; 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 channels.generic.websocket import WebsocketConsumer
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from customercore.models import * from .models import *
import json import json
from django.template.loader import render_to_string from django.template.loader import render_to_string
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
@ -47,6 +47,18 @@ class TicketRoomConsumer(WebsocketConsumer):
async_to_sync(self.channel_layer.group_send)( async_to_sync(self.channel_layer.group_send)(
self.ticket_number, event 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: else:
body = text_data_json['description'] body = text_data_json['description']
file_paths = text_data_json['filePath'] 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({ self.send(text_data=json.dumps({
'event_type': 'update', 'event_type': 'update',
'html': html 'html': html
@ -89,7 +101,7 @@ class TicketRoomConsumer(WebsocketConsumer):
context = { context = {
'user': event['user'] '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({ self.send(text_data=json.dumps({
'event_type': 'typing', 'event_type': 'typing',
'html': html 'html': html
@ -99,3 +111,37 @@ class TicketRoomConsumer(WebsocketConsumer):
self.send(text_data=json.dumps({ self.send(text_data=json.dumps({
'event_type': 'stop_typing' '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 id="messages" hx-swap-oob="beforeend">
<div class="fade-in-up"> <div class="fade-in-up">
{% include 'details_templates/ticket-message.html' %} {% include 'details_templates/partials/ticket-message.html' %}
</div> </div>
<style> <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>
<div class="w-[45px] s:w-[60px] h-[45px] s:h-[60px] rounded-full shadow-md border border-gray-100"> <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 %}
@ -65,5 +65,6 @@
{% endif %} {% endif %}
</div> </div>
{% include 'details_templates/partials/ticket-message-reactions.html' %}
</div> </div>
</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