You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
400 lines
21 KiB
HTML
400 lines
21 KiB
HTML
{% 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 %} |