[UPDT] HORILLA DOCUMENTS: Updated horilla documents app by adding htmx script to trigger the bulk rejection action

This commit is contained in:
Horilla
2025-05-16 19:20:14 +05:30
parent 941c9488b8
commit 2a688851e0
9 changed files with 245 additions and 262 deletions

View File

@@ -0,0 +1,107 @@
var confirmMessages = {
approved: {
ar: "هل ترغب حقًا في الموافقة على جميع الطلبات المحددة؟",
de: "Möchten Sie wirklich alle ausgewählten Anfragen genehmigen?",
es: "¿Realmente quieres aprobar todas las solicitudes seleccionadas?",
en: "Do you really want to approve all the selected requests?",
fr: "Voulez-vous vraiment approuver toutes les demandes sélectionnées?",
},
rejected: {
ar: "هل تريد حقًا رفض جميع الطلبات المحددة؟",
de: "Möchten Sie wirklich alle ausgewählten Anfragen ablehnen?",
es: "¿Realmente deseas rechazar todas las solicitudes seleccionadas?",
en: "Do you really want to reject all the selected requests?",
fr: "Voulez-vous vraiment rejeter toutes les demandes sélectionnées?",
},
};
var alreadyActionMessages = {
approved: {
ar: "بعض الطلبات المحددة تم الموافقة عليها مسبقًا.",
de: "Einige ausgewählte Anfragen wurden bereits genehmigt.",
es: "Algunas solicitudes seleccionadas ya han sido aprobadas.",
en: "Some selected requests have already been approved.",
fr: "Certaines demandes sélectionnées ont déjà été approuvées.",
},
rejected: {
ar: "بعض الطلبات المحددة تم رفضها مسبقًا.",
de: "Einige ausgewählte Anfragen wurden bereits abgelehnt.",
es: "Algunas solicitudes seleccionadas ya han sido rechazadas.",
en: "Some selected requests have already been rejected.",
fr: "Certaines demandes sélectionnées ont déjà été rejetées.",
},
};
function validateDocsIds(event) {
getCurrentLanguageCode(function (lang) {
const ids = [];
const checkedRows = $("[type=checkbox]:checked");
const takeAction = $(event.currentTarget).data("action");
let alreadyTakeAction = false;
checkedRows.each(function () {
const id = $(this).attr("id");
const status = $(this).data("status");
if (id) {
if (status === takeAction) alreadyTakeAction = true;
ids.push(id);
}
});
if (ids.length === 0) {
var norowMessages = {
ar: "لم يتم تحديد أي صفوف.",
de: "Es wurden keine Zeilen ausgewählt.",
es: "No se han seleccionado filas.",
en: "No rows have been selected.",
fr: "Aucune ligne n'a été sélectionnée.",
};
event.preventDefault();
Swal.fire({
text: norowMessages[lang] || norowMessages.en,
icon: "warning",
confirmButtonText: "Close",
});
} else if (alreadyTakeAction) {
event.preventDefault();
Swal.fire({
text:
alreadyActionMessages[takeAction][lang] ||
alreadyActionMessages[takeAction].en,
icon: "warning",
confirmButtonText: "Close",
});
} else {
// Directly trigger action without confirmation
const triggerId =
takeAction === "approved"
? "#bulkApproveDocument"
: "#bulkRejectDocument";
$(triggerId).attr("hx-vals", JSON.stringify({ ids })).click();
}
});
}
function highlightRow(checkbox) {
checkbox.closest(".oh-user_permission-list_item").removeClass("highlight-selected");
if (checkbox.is(":checked")) {
checkbox.closest(".oh-user_permission-list_item").addClass("highlight-selected");
}
}
function selectAllDocuments(event) {
event.stopPropagation();
const checkbox = event.currentTarget;
const isChecked = checkbox.checked;
const accordionBody = checkbox
.closest(".oh-accordion-meta__header")
.nextElementSibling;
if (accordionBody) {
const checkboxes = accordionBody.querySelectorAll('[type="checkbox"]');
checkboxes.forEach(cb => cb.checked = isChecked);
}
}

View File

@@ -20,11 +20,11 @@
</div>
<ul class="oh-dashboard__events-nav" id="birthdayDots">
{% for birthday in birthdays %}
<li onclick="moveSlider(event)"
class="oh-dashboard__events-nav-item {% if forloop.first %}oh-dashboard__events-nav-item--active{% endif %}"
<li onclick="moveSlider(event)"
class="oh-dashboard__events-nav-item {% if forloop.first %}oh-dashboard__events-nav-item--active{% endif %}"
data-target="{{ forloop.counter0 }}">
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endif %}

View File

@@ -187,30 +187,33 @@
{% if perms.horilla_documnets.change_documentrequest or request.user|is_reportingmanager %}
<li class="oh-dropdown__item">
<a class="oh-dropdown__link mb-2 bulk_approve" style="cursor: pointer;"
>{% trans "Bulk Approve Requests" %}</a
>
<a class="oh-dropdown__link mb-2 bulk_approve" style="cursor: pointer;" onclick="validateDocsIds(event);"
data-action="approved">{% trans "Bulk Approve Requests" %}
</a>
<span hx-post="{% url 'document-bulk-approve' %}" hx-confirm="{% trans 'Do you really want to approve all the selected requests?' %}"
id="bulkApproveDocument">
</span>
</li>
{% endif %}
{% if perms.base.change_shiftrequest or request.user|is_reportingmanager %}
<li class="oh-dropdown__item">
<a class="oh-dropdown__link mb-2 " style="cursor: pointer;"
id="BulkRejectDocument"
>{% trans "Bulk Reject Requests" %}</a
>
</li>
{% comment %}
<li class="oh-dropdown__item">
<a
href="#"
class="oh-dropdown__link oh-dropdown__link--danger"
id="deleteShiftRequest"
>{% trans "Delete" %}</a
>
</li>
{% endcomment %} {% endif %}
<li class="oh-dropdown__item">
<a class="oh-dropdown__link mb-2 " style="cursor: pointer;" onclick="validateDocsIds(event);" data-action="rejected">
{% trans "Bulk Reject Requests" %}
</a>
<span data-toggle="oh-modal-toggle" data-target="#objectCreateModal" hx-target="#objectCreateModalTarget"
hx-get="{% url 'document-bulk-reject' %}" id="bulkRejectDocument"></span>
</li>
{% comment %}
<li class="oh-dropdown__item">
<a
href="#"
class="oh-dropdown__link oh-dropdown__link--danger"
id="deleteShiftRequest"
>{% trans "Delete" %}</a
>
</li>
{% endcomment %}
{% endif %}
</ul>
</div>
</div>
@@ -236,44 +239,5 @@
</div>
</section>
<div
class="oh-modal"
id="BulkRejectModal"
role="dialog"
aria-labelledby="BulkRejectModal"
aria-hidden="true"
>
<div class="oh-modal__dialog">
<div class="oh-modal__dialog-header">
<span class="oh-modal__dialog-title" id="BulkRejectModalLabel"
>{% trans "Bulk Rejection Reason" %}</span
>
<button class="oh-modal__close" aria-label="Close">
<ion-icon
name="close-outline"
role="img"
class="md hydrated"
aria-label="close outline"
></ion-icon>
</button>
</div>
<div class="oh-modal__dialog-body" id="BulkRejectForm">
<form id="bulk-reject-form">
<textarea name="rejection_reason" cols="40" rows="2" class="oh-input w-100" placeholder="Rejection reason" id="bulk_rejection_reason" required></textarea>
<div class="d-flex flex-row-reverse w-100 align-items-center mt-4">
<button type="submit" class="oh-btn oh-btn--secondary oh-btn--shadow bulk_reject">
{% trans "Save" %}
</button>
</div>
</form>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$("#id_field").on("change", function () {
$(".filterButton")[0].click();
});
});
</script>
<script src="{% static '/document/actions.js' %}"></script>
<script src="{% static '/base/filter.js' %}"></script>

View File

@@ -0,0 +1,28 @@
{% load i18n %}
<div class="oh-modal__dialog-header">
<span class="oh-modal__dialog-title" id="objectCreateModalLabel">{% trans "Bulk Reject Requests" %}</span>
<button class="oh-modal__close" aria-label="Close">
<ion-icon name="close-outline" role="img" class="md hydrated" aria-label="close outline"></ion-icon>
</button>
</div>
<div class="oh-modal__dialog-body" id="BulkRejectForm">
<form id="bulk-reject-form" class="oh-profile-section" hx-target="#objectCreateModalTarget"
hx-post="{% url 'document-bulk-reject' %}">
<div class="oh-input-group">
<label class="oh-label" for="{{form.reject_reason.id_for_label}}">{{form.reject_reason.label}}</label>
{{form.reject_reason}}
{% if form.reject_reason.errors %}
{{ form.reject_reason.errors }}
{% endif %}
</div>
{% for id in ids %}
<input type="hidden" name="ids" value="{{ id }}">
{% endfor %}
<div class="d-flex flex-row-reverse w-100 align-items-center mt-4">
<button type="submit" class="oh-btn oh-btn--secondary oh-btn--shadow bulk_reject">
{% trans "Save" %}
</button>
</div>
</form>
</div>

View File

@@ -9,9 +9,10 @@
}
.custom-dialog {
max-width:1000px;
max-width: 1000px;
max-height: 800px;
}
.oh-not-found {
display: flex;
justify-content: center;
@@ -21,167 +22,22 @@
opacity: 0.5;
}
.file-validation {
color: #4f5bd9;
background-color: #d8e7f0;
border-color: #d6e9c6;
padding: 15px;
border: 1px solid transparent;
border-radius: 4px;
}
.file-validation {
color: #4f5bd9;
background-color: #d8e7f0;
border-color: #d6e9c6;
padding: 15px;
border: 1px solid transparent;
border-radius: 4px;
}
</style>
{% include 'documents/document_nav.html' %}
<div
class="oh-checkpoint-badge mb-2"
id="selectedDocuments"
data-ids="[]"
data-clicked=""
style="display: none"
>
<div class="oh-checkpoint-badge mb-2" id="selectedDocuments" data-ids="[]" data-clicked="" style="display: none">
{% trans "Selected Documents" %}
</div>
<div id="view-container" class="oh-wrapper">
{% include 'documents/requests.html' %}
</div>
<script>
var norowMessages = {
ar: "لم يتم تحديد أي صفوف.",
de: "Es wurden keine Zeilen ausgewählt.",
es: "No se han seleccionado filas.",
en: "No rows have been selected.",
fr: "Aucune ligne n'a été sélectionnée.",
};
var approveMessages = {
ar: "هل ترغب حقًا في الموافقة على جميع الطلبات المحددة؟",
de: "Möchten Sie wirklich alle ausgewählten Anfragen genehmigen?",
es: "Realmente quieres aprobar todas las solicitudes seleccionadas?",
en: "Do you really want to approve all the selected requests?",
fr: "Voulez-vous vraiment approuver toutes les demandes sélectionnées?",
};
var rejectMessages = {
ar: "هل تريد حقًا رفض جميع الطلبات المحددة؟",
de: "Möchten Sie wirklich alle ausgewählten Anfragen ablehnen?",
es: "¿Realmente deseas rechazar todas las solicitudes seleccionadas?",
en: "Do you really want to reject all the selected requests?",
fr: "Voulez-vous vraiment rejeter toutes les demandes sélectionnées?",
};
$(document).ready(function () {
$(".bulk_approve").on("click", function () {
var languageCode = null;
getCurrentLanguageCode(function (code) {
languageCode = code;
var confirmMessage = approveMessages[languageCode];
var textMessage = norowMessages[languageCode];
checkedRows = $("[type=checkbox]:checked");
ids = [];
checkedRows.each(function () {
if($(this).attr("id") != ""){
ids.push($(this).attr("id"));
}
});
if (ids.length === 0) {
Swal.fire({
text: textMessage,
icon: "warning",
confirmButtonText: "Close",
});
} else {
// Use SweetAlert for the confirmation dialog
Swal.fire({
text: confirmMessage,
icon: "success",
showCancelButton: true,
confirmButtonColor: "#008000",
cancelButtonColor: "#d33",
confirmButtonText: "Confirm",
}).then(function (result) {
if (result.isConfirmed) {
$.ajax({
type: "GET",
url: "{% url 'document-bulk-approve' %}",
data: {
"ids": ids,
},
traditional:true,
success: function () {
window.location.reload()
},
error: function () {
console.log("Error");
},
});
}
});
}
})
});
$("#BulkRejectDocument").on("click", function () {
var languageCode = null;
getCurrentLanguageCode(function (code) {
languageCode = code;
var rejectMessage = rejectMessages[languageCode];
var textMessage = norowMessages[languageCode];
checkedRows = $("[type=checkbox]:checked");
reason = $("#bulk_rejection_reason").val()
ids = [];
checkedRows.each(function () {
if($(this).attr("id") != ""){
ids.push($(this).attr("id"));
}
});
if (ids.length === 0) {
Swal.fire({
text: textMessage,
icon: "warning",
confirmButtonText: "Close",
});
} else {
$("#BulkRejectModal").addClass("oh-modal--show");
$(".bulk-reject-form").on("submit", function(){
event.preventDefault()
Swal.fire({
text: rejectMessage,
icon: "info",
showCancelButton: true,
confirmButtonColor: "#008000",
cancelButtonColor: "#d33",
confirmButtonText: "Confirm",
}).then(function (result) {
if (result.isConfirmed) {
$.ajax({
type: "POST",
url: "{% url 'document-bulk-reject' %}",
data: {
"ids": ids,
"reason": reason,
csrfmiddlewaretoken: getCookie("csrftoken"),
},
traditional:true,
success: function () {
window.location.reload()
},
error: function () {
console.log("Error");
},
});
}
});
})
}
});
});
$(".select_all").on("change", function(){
var is_checked = $(this).prop("checked");
$(this).closest(".oh-accordion-meta__header").siblings(".oh-accordion-meta__body").find("[type=checkbox]").prop("checked", is_checked);
})
});
</script>
{% endblock content %}

View File

@@ -61,7 +61,7 @@
>
<span class="oh-accordion-meta__title pt-3 pb-3">
<div class="oh-tabs__input-badge-container">
<input type="checkbox" name="select_all" onclick="event.stopPropagation()" class="oh-input payslip-checkbox oh-input__checkbox select_all me-3">
<input type="checkbox" name="select_all" onclick="selectAllDocuments(event)" class="oh-input payslip-checkbox oh-input__checkbox select_all me-3">
{{document_list.grouper}}
<div class="oh-checkpoint-badge oh-checkpoint-badge--secondary" style="margin-left: 20px;" title="{% trans 'Uploaded / Requested' %}">
{{document_list.list.0.upload_documents_count}} / {{document_list.list|length}}
@@ -118,7 +118,7 @@
<div class="oh-user_permission-list_profile ps-2 {% if document.status == "approved" %}row-status--yellow {% elif document.status == 'rejected' %}row-status--red {% elif document.status == 'requested' %}row-status--blue{% endif %}">
<input type="checkbox" id="{{ document.id }}" onchange="highlightRow($(this))"
class="oh-input payslip-checkbox oh-input__checkbox all-documents-row"
onclick="event.stopPropagation()"
onclick="event.stopPropagation()" data-status="{{document.status}}"
>
<div class="oh-navbar__user-photo oh-user_permission--profile">
{% if document.document %}
@@ -395,15 +395,7 @@
<!-- end of empty page -->
{% endif %}
<script>
function highlightRow(checkbox) {
checkbox.closest(".oh-user_permission-list_item").removeClass("highlight-selected");
if (checkbox.is(":checked")) {
checkbox.closest(".oh-user_permission-list_item").addClass("highlight-selected");
}
}
$(document).ready(function(){
$(".oh-accordion-meta__header").first().click()
$(".oh-accordion-meta__body").first().removeClass('d-none');
})
</script>

View File

@@ -868,40 +868,69 @@ def document_reject(request, id):
@manager_can_enter("horilla_documents.add_document")
def document_bulk_approve(request):
"""
This function used to view the approve uploaded document.
Parameters:
This function is used to bulk-approve uploaded documents.
request (HttpRequest): The HTTP request object.
Parameters:
request (HttpRequest): The HTTP request object.
Returns:
HttpResponse: A 204 No Content response with HX-Refresh header.
"""
ids = request.GET.getlist("ids")
document_obj = Document.objects.filter(
id__in=ids,
).exclude(document="")
document_obj.update(status="approved")
messages.success(request, _(f"{len(document_obj)} Document request approved"))
if request.method == "POST":
ids = request.POST.getlist("ids")
return HttpResponse("success")
# Documents with uploaded files
approved_docs = Document.objects.filter(id__in=ids).exclude(document="")
count_approved = approved_docs.update(status="approved")
# Documents without uploaded files
not_uploaded_count = len(ids) - approved_docs.count()
if count_approved:
messages.success(
request, _(f"{count_approved} document request(s) approved")
)
if not_uploaded_count:
messages.info(
request, _(f"{not_uploaded_count} document(s) skipped (not uploaded)")
)
return HttpResponse(status=204, headers={"HX-Refresh": "true"})
@login_required
@manager_can_enter("horilla_documents.add_document")
def document_bulk_reject(request):
"""
This function used to view the reject uploaded document.
Parameters:
Handle bulk rejection of documents.
request (HttpRequest): The HTTP request object.
Returns:
On GET request, display a form to enter the rejection reason for selected documents.
On POST request, validate the rejection reason and update the status of documents
(excluding those already rejected) to 'rejected' with the provided reason.
"""
ids = request.POST.getlist("ids")
reason = request.POST.get("reason")
document_obj = Document.objects.filter(id__in=ids)
document_obj.update(status="rejected", reject_reason=reason)
messages.success(request, _("Document request rejected"))
return HttpResponse("success")
ids = (
request.POST.getlist("ids")
if request.method == "POST"
else request.GET.getlist("ids")
)
form = DocumentRejectForm(request.POST or None)
if request.method == "POST" and form.is_valid():
reject_reason = form.cleaned_data["reject_reason"]
updated_count = (
Document.objects.filter(id__in=ids)
.exclude(status="rejected")
.update(status="rejected", reject_reason=reject_reason)
)
messages.success(
request, _("{} Document request rejected").format(updated_count)
)
return HttpResponse(status=204, headers={"HX-Refresh": "true"})
return render(
request, "documents/document_reject_reason.html", {"ids": ids, "form": form}
)
@login_required

View File

@@ -92,13 +92,18 @@ class DocumentUpdateForm(ModelForm):
}
class DocumentRejectForm(ModelForm):
"""form to add rejection reason while rejecting a Document"""
class Meta:
model = Document
fields = ["reject_reason"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reject_reason"].widget.attrs["required"] = True
class DocumentRejectForm(forms.Form):
verbose_name = Document()._meta.get_field("reject_reason").verbose_name
reject_reason = forms.CharField(
widget=forms.Textarea(
attrs={
"class": "oh-input w-100",
"placeholder": verbose_name,
"rows": 2,
"cols": 40,
}
),
max_length=255,
required=True,
label=verbose_name,
)

View File

@@ -90,7 +90,9 @@ class Document(HorillaModel):
status = models.CharField(
choices=STATUS, max_length=10, default="requested", verbose_name=_("Status")
)
reject_reason = models.TextField(blank=True, null=True, max_length=255)
reject_reason = models.TextField(
blank=True, null=True, max_length=255, verbose_name=_("Reject Reason")
)
issue_date = models.DateField(null=True, blank=True, verbose_name=_("Issue Date"))
expiry_date = models.DateField(null=True, blank=True, verbose_name=_("Expiry Date"))
notify_before = models.IntegerField(