[ADD] PMS: Summernote inside activity comment in OKR

This commit is contained in:
Horilla
2025-07-03 16:32:24 +05:30
parent 8fade9e47e
commit 5702a2135b
4 changed files with 188 additions and 165 deletions

View File

@@ -552,7 +552,7 @@ class EmployeeObjective(HorillaModel):
class Comment(models.Model):
"""comments for objectives"""
comment = models.CharField(max_length=150)
comment = models.TextField()
employee_id = models.ForeignKey(
Employee,
on_delete=models.DO_NOTHING,
@@ -572,6 +572,9 @@ class Comment(models.Model):
objects = HorillaCompanyManager(
related_company_field="employee_id__employee_work_info__company_id"
)
xss_exempt_fields = [
"comment",
]
def __str__(self):
return f"{self.employee_id.employee_first_name} - {self.comment} "

View File

@@ -1,177 +1,184 @@
{% load i18n %} {% load pmsfilters %}
<style>
#commentEditor .note-btn-group.send-group {
margin-right: 0;
}
</style>
{% if messages %}
<div class="oh-wrapper">
<div class="oh-wrapper">
{% for message in messages %}
<div class="oh-alert-container">
<div class="oh-alert oh-alert--animated {{message.tags}}">
{{ message }}
</div>
</div>
<div class="oh-alert-container">
<div class="oh-alert oh-alert--animated {{ message.tags }}">{{ message }}</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="oh-activity-sidebar__header">
<a
style="cursor: pointer;"
onclick="$('.oh-activity-sidebar--show').removeClass('oh-activity-sidebar--show');" >
<ion-icon
name="chevron-forward-outline"
class="oh-activity-sidebar__header-icon me-2 oh-activity-sidebar__close"
data-target="#leaveactivitySidebar"
></ion-icon>
</a>
<span class="oh-activity-sidebar__title"> {% trans "Activities" %} </span>
<a style="cursor: pointer;" onclick="$('.oh-activity-sidebar--show').removeClass('oh-activity-sidebar--show');"><ion-icon name="chevron-forward-outline" class="oh-activity-sidebar__header-icon me-2 oh-activity-sidebar__close" data-target="#leaveactivitySidebar"></ion-icon></a>
<span class="oh-activity-sidebar__title">{% trans 'Activities' %}</span>
</div>
<form method="post"
hx-target="#activityContainer"
hx-post=" {% url 'objective-detailed-view-comment' objective.id %} " id="commentForm">
{% csrf_token %}
<form method="post" hx-target="#activityContainer" hx-post=" {% url 'objective-detailed-view-comment' objective.id %} " id="commentForm">
{% csrf_token %}
<div>
<input type="text" name="comment" id="commentInput" class="oh-input w-100" placeholder="Comment here">
<button type="submit" id="commentButton" class="oh-btn oh-btn--secondary mt-2 mr-0 oh-btn--w-100-resp" style="display: none;">
{% trans "Comment" %}
</button>
</div>
<div id="commentEditor">
<textarea rows="2" cols="2" name="comment" id="editor"></textarea>
<button type="submit" id="commentButton" class="oh-btn oh-btn--secondary mt-2 mr-0 oh-btn--w-100-resp" style="display: none;">{% trans 'Comment' %}</button>
</div>
<div class="oh-inner-sidebar-content__footer"></div>
<div class="oh-inner-sidebar-content__footer"></div>
</form>
<ul class="oh-activity-list ">
{% comment %} <li class="oh-activity-list__comment-item">
<!-- comment section -->
{% for comment in comments %}
<div class="oh-activity-list__comment-title">
<ul class="oh-activity-list">
{% comment %} <li class="oh-activity-list__comment-item">
<!-- comment section -->
{% for comment in comments %}
<div class="oh-activity-list__comment-title">
<div class="oh-activity-list__photo oh-activity-list__photo--small me-2">
<img src="https://ui-avatars.com/api/?name={{ comment.employee_id }}&background=random" class="oh-activity-list__image" alt="Simone de Beauvoir" />
</div>
<small class="oh-activity-list__description">
<span><strong>{{ comment.employee_id }}</strong> {% trans 'added a comment' %}</span>
<div class="oh-activity-list__comment-timestamp mt-1">
<span class="dateformat_changer">{{ comment.created_at|date:'M. d, Y' }}</span>&nbsp,&nbsp
<span class="timeformat_changer">{{ comment.created_at|date:'g:i a' }}</span>
</div>
</small>
</div>
<div class="oh-activity-list__comment-container">
<p class="oh-activity-list__comment">{{ comment.comment }}</p>
</div>
{% endfor %}
<!-- end of comment -->
</li> {% endcomment %}
<!-- history section -->
{% for activity in activity_list %}
{% comment %} {% for history in history_object.delta.changes %}
{% endcomment %}
{% if activity.type == 'Changes' %}
<ul class="d-flex justify-content-between align-items-center pt-3 mb-3 border-top">
<li class="oh-activity-list__item align-items-center mb-0">
<div class="oh-activity-list__photo oh-activity-list__photo--small me-2">
<img src="https://ui-avatars.com/api/?name={{comment.employee_id}}&background=random"
class="oh-activity-list__image" alt="Simone de Beauvoir" />
<img src="https://ui-avatars.com/api/?name={{ activity.updated_by }}&background=random" class="oh-activity-list__image" alt="Albert Camus" />
</div>
<small class="oh-activity-list__description">
<span><strong>{{comment.employee_id}}</strong> {% trans "added a comment" %}</span>
<div class="oh-activity-list__comment-timestamp mt-1">
<span class="dateformat_changer">{{ comment.created_at|date:"M. d, Y" }}</span>&nbsp,&nbsp
<span class="timeformat_changer">{{ comment.created_at|date:"g:i a" }}</span>
</div>
<strong>{{ activity.updated_by }}</strong>
{% trans 'updated' %}
<strong>{{ activity.changes.0.field|title|cut:'_' }}</strong> {% trans 'from' %}
<strong>{{ activity.changes.0.old }}</strong> to <strong>{{ activity.changes.0.new }}</strong>
</small>
</div>
<div class="oh-activity-list__comment-container">
<p class="oh-activity-list__comment">
{{comment.comment}}
</p>
</div>
</li>
<li>
<small>
<span class="dateformat_changer">{{ activity.pair.0.history_date|date:'M. d, Y' }}</span>&nbsp,&nbsp
<span class="timeformat_changer">{{ activity.pair.0.history_date|date:'g:i a' }}</span>
</small>
</li>
</ul>
{% elif activity.type == 'key_result' %}
{% for history in activity.key_result.delta.changes %}
<ul class="d-flex justify-content-between align-items-center pt-3 mb-3 border-top">
<li class="oh-activity-list__item align-items-center mb-0">
<div class="oh-activity-list__photo oh-activity-list__photo--small me-2">
<img src="https://ui-avatars.com/api/?name={{ activity.key_result.changed_user }}&background=random" class="oh-activity-list__image" alt="Albert Camus" />
</div>
<small class="oh-activity-list__description">
<strong>{{ activity.key_result.changed_user }}</strong>
{% trans 'updated' %}
<strong>{{ history.field|replace|title }}</strong> {% trans 'field of ' %}
<strong>{{ activity.key_result.k_r }}</strong> {% trans 'key result' %},{% trans 'from' %}
<strong>{{ history.old }}</strong> {% trans 'to' %} <strong>{{ history.new }}</strong>
{% comment %} <strong>{{ activity.changes.0.field|title|cut:'_' }}</strong> {% trans 'from' %}
<strong>{{ activity.changes.0.old }}</strong> to <strong>{{ activity.changes.0.new }}</strong> {% endcomment %}
</small>
</li>
<li>
<small>
<span class="dateformat_changer">{{ activity.date|date:'M. d, Y' }}</span>&nbsp,&nbsp
<span class="timeformat_changer">{{ activity.date|date:'g:i a' }}</span>
</small>
</li>
</ul>
{% endfor %}
<!-- end of comment -->
</li> {% endcomment %}
<!-- history section -->
{% for activity in activity_list %}
{% comment %} {% for history in history_object.delta.changes %} {% endcomment %}
{% if activity.type == 'Changes' %}
<ul class="d-flex justify-content-between align-items-center pt-3 mb-3 border-top">
<li class="oh-activity-list__item align-items-center mb-0">
<div class="oh-activity-list__photo oh-activity-list__photo--small me-2">
<img src="https://ui-avatars.com/api/?name={{activity.updated_by}}&background=random"
class="oh-activity-list__image" alt="Albert Camus" />
</div>
<small class="oh-activity-list__description"><strong>{{activity.updated_by}} </strong>
{% trans "updated" %}
<strong> {{activity.changes.0.field |title|cut:'_'}}</strong> {% trans "from" %}
<strong>{{activity.changes.0.old}}</strong> to <strong>{{activity.changes.0.new}}</strong>
</small>
</li>
<li>
<small>
<span class="dateformat_changer">{{ activity.pair.0.history_date|date:"M. d, Y" }}</span>&nbsp,&nbsp
<span class="timeformat_changer">{{ activity.pair.0.history_date|date:"g:i a" }}</span>
</small>
</li>
</ul>
{% elif activity.type == 'key_result' %}
{% for history in activity.key_result.delta.changes %}
<ul class="d-flex justify-content-between align-items-center pt-3 mb-3 border-top">
<li class="oh-activity-list__item align-items-center mb-0">
<div class="oh-activity-list__photo oh-activity-list__photo--small me-2">
<img src="https://ui-avatars.com/api/?name={{activity.key_result.changed_user}}&background=random"
class="oh-activity-list__image" alt="Albert Camus" />
</div>
<small class="oh-activity-list__description"><strong>{{activity.key_result.changed_user}} </strong>
{% trans "updated" %}
<strong>{{history.field|replace|title}}</strong> {% trans "field of " %}
<strong>{{activity.key_result.k_r}} </strong> {% trans "key result" %},
{% trans "from" %}
<strong>{{history.old}}</strong> {% trans "to" %} <strong>{{history.new}}</strong>
{% comment %} <strong> {{activity.changes.0.field |title|cut:'_'}}</strong> {% trans "from" %}
<strong>{{activity.changes.0.old}}</strong> to <strong>{{activity.changes.0.new}}</strong> {% endcomment %}
</small>
</li>
<li>
<small>
<span class="dateformat_changer">{{ activity.date|date:"M. d, Y" }}</span>&nbsp,&nbsp
<span class="timeformat_changer">{{ activity.date|date:"g:i a" }}</span>
</small>
</li>
</ul>
{% endfor %}
{% elif activity.type == 'comment' %}
<ul class="pt-3 border-top">
<div class="oh-activity-list__comment-title d-flex justify-content-between align-items-center">
<div class="oh-activity-list__item align-items-center mb-0">
<div class="oh-activity-list__photo oh-activity-list__photo--small me-2">
<img src="https://ui-avatars.com/api/?name={{activity.comment.employee_id}}&background=random"
class="oh-activity-list__image" alt="Albert Camus" />
</div>
<small class="oh-activity-list__description">
<span><strong>{{activity.comment.employee_id}}</strong> {% trans "added a comment" %}</span>
</small>
</div>
<div>
<small>
<span class="dateformat_changer">{{ activity.comment.created_at|date:"M. d, Y" }}</span>&nbsp,&nbsp
<span class="timeformat_changer">{{ activity.comment.created_at|date:"g:i a" }}</span>
</small>
</div>
</div>
<div class="oh-activity-list__comment-container">
<p class="oh-activity-list__comment">
{{activity.comment.comment}}
</p>
</div>
</ul>
{% else %}
<ul class="d-flex justify-content-between align-items-center pt-3 mb-3 border-top">
<li class="oh-activity-list__item align-items-center mb-0">
<div class="oh-activity-list__photo oh-activity-list__photo--small me-2">
<img src="https://ui-avatars.com/api/?name={{activity.updated_by}}&background=random"
class="oh-activity-list__image" alt="Albert Camus" />
</div>
<small class="oh-activity-list__description"><strong>{{activity.updated_by}} </strong>
{% trans "Created Objective" %}
</small>
</li>
<li>
<small>
<span class="dateformat_changer">{{ activity.pair.0.history_date|date:"M. d, Y" }}</span>&nbsp,&nbsp
<span class="timeformat_changer">{{ activity.pair.0.history_date|date:"g:i a" }}</span>
</small>
</li>
</ul>
{% endif %}
{% comment %} {% endfor %} {% endcomment %}
{% endfor %}
{% elif activity.type == 'comment' %}
<ul class="pt-3 border-top">
<div class="oh-activity-list__comment-title d-flex justify-content-between align-items-center">
<div class="oh-activity-list__item align-items-center mb-0">
<div class="oh-activity-list__photo oh-activity-list__photo--small me-2">
<img src="https://ui-avatars.com/api/?name={{ activity.comment.employee_id }}&background=random" class="oh-activity-list__image" alt="Albert Camus" />
</div>
<small class="oh-activity-list__description"><span><strong>{{ activity.comment.employee_id }}</strong> {% trans 'added a comment' %}</span></small>
</div>
<div>
<small>
<span class="dateformat_changer">{{ activity.comment.created_at|date:'M. d, Y' }}</span>&nbsp,&nbsp
<span class="timeformat_changer">{{ activity.comment.created_at|date:'g:i a' }}</span>
</small>
</div>
</div>
<div class="oh-activity-list__comment-container">
<p class="oh-activity-list__comment">{{ activity.comment.comment|safe }}</p>
</div>
</ul>
{% else %}
<ul class="d-flex justify-content-between align-items-center pt-3 mb-3 border-top">
<li class="oh-activity-list__item align-items-center mb-0">
<div class="oh-activity-list__photo oh-activity-list__photo--small me-2">
<img src="https://ui-avatars.com/api/?name={{ activity.updated_by }}&background=random" class="oh-activity-list__image" alt="Albert Camus" />
</div>
<small class="oh-activity-list__description">
<strong>{{ activity.updated_by }}</strong>
{% trans 'Created Objective' %}
</small>
</li>
<li>
<small>
<span class="dateformat_changer">{{ activity.pair.0.history_date|date:'M. d, Y' }}</span>&nbsp,&nbsp
<span class="timeformat_changer">{{ activity.pair.0.history_date|date:'g:i a' }}</span>
</small>
</li>
</ul>
{% endif %}
{% comment %}
{% endfor %} {% endcomment %}
{% endfor %}
</ul>
<script>
// Get references to the input field and comment button
const commentInput = document.getElementById('commentInput');
const commentButton = document.getElementById('commentButton');
// ✅ Send Button styled like toolbar icon
var sendButton = function (context) {
var ui = $.summernote.ui
return ui
.button({
contents: 'Add comment <ion-icon name="paper-plane-outline"></ion-icon>',
tooltip: 'Send Content',
click: function () {
const content = context.invoke('code')
$("#commentEditor button[type=submit]").click()
}
})
.render()
}
// Add event listener to the input field
commentInput.addEventListener('input', function() {
// Show the comment button if the input field is not empty, hide it otherwise
if (commentInput.value.trim() !== '') {
commentButton.style.display = 'inline';
} else {
commentButton.style.display = 'none';
}
});
// ✅ Summernote Init
$('#editor').summernote({
height: 70, // compact height
placeholder: 'Write something...',
toolbar: [
['insert', ['picture', 'link', 'pdfUpload']],
['sendGroup', ['send']]
],
buttons: {
pdfUpload: pdfUploadButton,
send: sendButton,
}
})
console.log($("#commentEditor .note-btn-group.send-group").parent())
// ✅ Align last group to right
$(document).ready(function () {
$('.note-toolbar .note-btn-group').last().addClass('send-group')
$("#commentEditor .note-btn-group.send-group").parent().addClass("d-flex justify-content-between")
})
</script>

View File

@@ -26,21 +26,34 @@ function handlePDFUploadMultiple(files, context) {
const reader = new FileReader();
reader.onload = function(e) {
const base64 = e.target.result;
const base64 = e.target.result; // full base64 data URL like "data:application/pdf;base64,...."
const fileName = file.name;
const uniqueId = `pdfModal_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
const base64Content = base64.split(',')[1];
const binary = atob(base64Content);
const len = binary.length;
const buffer = new Uint8Array(len);
for (let i = 0; i < len; i++) {
buffer[i] = binary.charCodeAt(i);
}
const blob = new Blob([buffer], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob);
const cardHTML = `
<div style="border:1px solid #ccc; padding:10px; border-radius:6px; display:flex; align-items:center; gap:10px; margin:10px 0;width:200px;overflow:hidden;">
<img src="https://cdn-icons-png.flaticon.com/512/337/337946.png" alt="PDF" width="40" />
<img src="/static/images/ui/document-icon.png" alt="PDF" width="40" />
<a href="javascript:void(0);" onclick="openPDFModal('${uniqueId}')" style="text-decoration:none; color:#007bff;">
${fileName}
</a>
</div>
<div id="${uniqueId}" class="custom-pdf-modal" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; background:rgba(0,0,0,0.7); z-index:9999; justify-content:center; align-items:center;">
<div id="${uniqueId}" class="custom-pdf-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:9999; justify-content:center; align-items:center;">
<div style="background:white; padding:20px; border-radius:8px; position:relative; width:80%; height:90%;">
<button onclick="closePDFModal('${uniqueId}')" style="position:absolute; top:10px; right:10px; font-size:18px; background:#dc3545; color:white; border:none; border-radius:4px; padding:4px 10px;">×</button>
<iframe src="${base64}" width="100%" height="100%" style="border:none;"></iframe>
<iframe src="${blobUrl}" width="100%" height="100%" style="border:none;"></iframe>
</div>
</div>
`;
@@ -80,16 +93,16 @@ function handlePDFUpload(file, context) {
max-width:100%;
white-space:nowrap;
">
<img src="https://cdn-icons-png.flaticon.com/512/337/337946.png" alt="PDF" width="20" />
<img src="/static/images/ui/document-icon.png" alt="PDF" width="20" />
<a href="javascript:void(0);" onclick="openPDFModal('${uniqueId}')"
style="text-decoration:none; color:#007bff; font-weight:500;">
${fileName}
</a>
</div>
</span>
<div id="${uniqueId}" class="custom-pdf-modal" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; background:rgba(0,0,0,0.7); z-index:9999; justify-content:center; align-items:center;">
<div id="${uniqueId}" class="custom-pdf-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:9999; justify-content:center; align-items:center;">
<div style="background:white; padding:20px; border-radius:8px; position:relative; width:80%; height:90%;">
<button onclick="closePDFModal('${uniqueId}')" style="position:absolute; top:10px; right:10px; font-size:18px; background:#dc3545; color:white; border:none; border-radius:4px; padding:4px 10px;">×</button>
<button type="button" onclick="closePDFModal('${uniqueId}')" style="position:absolute; top:10px; right:10px; font-size:18px; background:#dc3545; color:white; border:none; border-radius:4px; padding:4px 10px;">×</button>
<iframe src="${base64}" width="100%" height="100%" style="border:none;"></iframe>
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB