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.

547 lines
32 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">
<div class="w-full rounded-md text-black" x-data="app()" x-init="init($refs.wysiwyg)">
<div class="border border-gray-200 overflow-hidden rounded-md">
<div class="w-full border-b border-gray-200 flex flex-wrap text-xl text-gray-600">
<button type="button"
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
@click="format('bold')">
<i class="mdi mdi-format-bold"></i>
</button>
<button type="button"
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
@click="format('italic')">
<i class="mdi mdi-format-italic"></i>
</button>
<button type="button"
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
@click="format('underline')">
<i class="mdi mdi-format-underline"></i>
</button>
<button type="button"
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
@click="format('formatBlock','P')">
<i class="mdi mdi-format-paragraph"></i>
</button>
<button type="button"
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
@click="format('formatBlock','H1')">
<i class="mdi mdi-format-header-1"></i>
</button>
<button type="button"
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
@click="format('formatBlock','H2')">
<i class="mdi mdi-format-header-2"></i>
</button>
<button type="button"
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
@click="format('formatBlock','H3')">
<i class="mdi mdi-format-header-3"></i>
</button>
<button type="button"
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
@click="format('insertUnorderedList')">
<i class="mdi mdi-format-list-bulleted"></i>
</button>
<button type="button"
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
@click="format('insertOrderedList')">
<i class="mdi mdi-format-list-numbered"></i>
</button>
<button type="button"
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
id="rightToLeft" @click="setDirection('rtl')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
class="w-[18px] h-[18px]" fill="none">
<path d="M15 3.5H21" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
<path d="M15 9.5H21" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
<path d="M3 15.5H21" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
<path d="M3 21.5H21" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
<path
d="M3.58579 9.91421C4.17157 10.5 5.11438 10.5 7 10.5C8.88562 10.5 9.82843 10.5 10.4142 9.91421C11 9.32843 11 8.38562 11 6.5C11 4.61438 11 3.67157 10.4142 3.08579C9.82843 2.5 8.88562 2.5 7 2.5C5.11438 2.5 4.17157 2.5 3.58579 3.08579C3 3.67157 3 4.61438 3 6.5C3 8.38562 3 9.32843 3.58579 9.91421Z"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button>
<button type="button"
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
id="leftToLeft" @click="setDirection('ltr')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
class="w-[18px] h-[18px]" fill="none">
<path d="M3 3.5H9" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
<path d="M3 9.5H9" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
<path d="M3 15.5H21" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
<path d="M3 21.5H21" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
<path
d="M13.5858 9.91421C14.1716 10.5 15.1144 10.5 17 10.5C18.8856 10.5 19.8284 10.5 20.4142 9.91421C21 9.32843 21 8.38562 21 6.5C21 4.61438 21 3.67157 20.4142 3.08579C19.8284 2.5 18.8856 2.5 17 2.5C15.1144 2.5 14.1716 2.5 13.5858 3.08579C13 3.67157 13 4.61438 13 6.5C13 8.38562 13 9.32843 13.5858 9.91421Z"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button>
</div>
<div class="w-full">
<iframe x-ref="wysiwyg" class="w-full h-[300px] overflow-y-auto"></iframe>
</div>
</div>
</div>
<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 hidden"
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>
function app(socket) {
return {
wysiwyg: null,
socket: socket,
typingTimeout: null,
init: function (el) {
this.wysiwyg = el;
this.setupWysiwyg();
},
setupWysiwyg: function() {
this.updateTextarea();
this.wysiwyg.contentDocument.body.addEventListener('input', this.onInput.bind(this));
this.wysiwyg.contentDocument.querySelector('head').innerHTML += `<style>
*, ::after, ::before {box-sizing: border-box;}
:root {tab-size: 4;}
html {line-height: 1.15;text-size-adjust: 100%;}
body {margin: 0px; padding: 1rem 0.5rem;}
body {font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";}
</style>`;
this.wysiwyg.contentDocument.body.innerHTML = '';
this.wysiwyg.contentDocument.designMode = "on";
},
onInput: function() {
this.updateTextarea();
this.sendTypingEvent();
},
updateTextarea: function() {
var wrapperDiv = document.createElement('div');
wrapperDiv.className = this.wysiwyg.classList.contains('rtl') ? 'rtl' : 'ltr';
wrapperDiv.innerHTML = this.wysiwyg.contentDocument.body.innerHTML;
document.getElementById('textEditor').value = wrapperDiv.outerHTML;
},
sendTypingEvent: function() {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
clearTimeout(this.typingTimeout);
const data = { event_type: 'typing' };
this.socket.send(JSON.stringify(data));
this.typingTimeout = setTimeout(() => {
const stopTypingData = { event_type: 'stop_typing' };
this.socket.send(JSON.stringify(stopTypingData));
}, 1500);
}
},
format: function (cmd, param) {
this.wysiwyg.contentDocument.execCommand(cmd, false, param || null);
this.updateTextarea();
},
setDirection: function(direction) {
this.wysiwyg.classList.remove('rtl', 'ltr');
this.wysiwyg.contentDocument.body.classList.remove('rtl', 'ltr');
document.getElementById('textEditor').classList.remove('rtl', 'ltr');
this.wysiwyg.classList.add(direction);
this.wysiwyg.contentDocument.body.classList.add(direction);
document.getElementById('textEditor').classList.add(direction);
this.wysiwyg.contentDocument.body.style.direction = direction;
this.updateTextarea();
}
}
}
function initializeWebSocket() {
const ticketId = '{{ticket.id}}';
const wsUrl = `wss://${window.location.host}/ws/ticketroom/${ticketId}/`;
const socket = new WebSocket(wsUrl);
socket.onopen = () => {
console.log('WebSocket connection established');
const appInstance = app(socket);
const wysiwygElement = document.querySelector('iframe'); // Replace with your specific iframe selector
appInstance.init(wysiwygElement);
};
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') {
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');
}
});
const submittedReactions = updateElement.querySelector('#submitted-reactions');
submittedReactions.classList.remove('w-full', 'border-t', 'border-gray-200', 'pt-5', 'flex', 'justify-start', 'items-center', 'gap-3', 'p-5');
updateElement.querySelectorAll('.submittedreaction-button').forEach(button => {
button.classList.add('hidden');
if (button.dataset.reaction === data.reaction) {
submittedReactions.classList.add('w-full', 'border-t', 'border-gray-200', 'pt-5', 'flex', 'justify-start', 'items-center', 'gap-3', 'p-5');
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');
const wysiwygElement = document.querySelector('iframe'); // Replace with your specific iframe selector
form.addEventListener('submit', (event) => {
event.preventDefault();
const formData = new FormData(form);
const description = formData.get('description');
const filePath = formData.getAll('filePath');
const data = {
event_type: 'update',
description: description,
filePath: filePath
};
socket.send(JSON.stringify(data));
form.reset();
textEditor.value = '';
wysiwygElement.contentDocument.body.innerHTML = ''; // Clear the WYSIWYG content
});
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));
}
});
}
document.addEventListener('DOMContentLoaded', () => {
initializeWebSocket();
});
</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/file-uploader.js" %}'></script>
{% endblock %}