Fix admin totals template syntax error and enhance search box styling
This commit is contained in:
parent
82b68976c5
commit
f1de11cc52
@ -66,6 +66,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.humanize',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'drf_yasg',
|
||||
@ -287,7 +288,7 @@ JAZZMIN_SETTINGS = {
|
||||
"default_icon_children": "fas fa-circle",
|
||||
"related_modal_active": False,
|
||||
"custom_css": "css/custom.css",
|
||||
"custom_js": None,
|
||||
"custom_js": "js/admin_date_range_dropdown.js",
|
||||
"use_google_fonts_cdn": True,
|
||||
"show_ui_builder": False,
|
||||
"language_chooser": True,
|
||||
|
||||
@ -16,9 +16,10 @@ from django.http import HttpResponse, HttpResponseRedirect
|
||||
from rangefilter.filters import DateRangeFilter
|
||||
from django.template.loader import render_to_string
|
||||
import weasyprint
|
||||
from django.db.models import Sum
|
||||
|
||||
class DropdownDateRangeFilter(DateRangeFilter):
|
||||
template = 'admin/dropdown_date_range_filter.html'
|
||||
pass
|
||||
|
||||
class ProfileInline(admin.StackedInline):
|
||||
model = Profile
|
||||
@ -97,6 +98,8 @@ class CustomUserAdmin(UserAdmin):
|
||||
send_whatsapp_link.allow_tags = True
|
||||
|
||||
class ParcelAdmin(admin.ModelAdmin):
|
||||
change_list_template = 'admin/core/parcel/change_list.html'
|
||||
|
||||
list_display = ('tracking_number', 'shipper', 'carrier', 'price', 'driver_amount', 'platform_fee', 'distance_km', 'status', 'payment_status', 'created_at')
|
||||
list_filter = (
|
||||
'status',
|
||||
@ -106,6 +109,9 @@ class ParcelAdmin(admin.ModelAdmin):
|
||||
search_fields = ('tracking_number', 'shipper__username', 'receiver_name', 'carrier__username')
|
||||
actions = ['export_as_csv', 'print_parcels', 'export_pdf']
|
||||
|
||||
class Media:
|
||||
js = ('js/admin_date_range_dropdown.js',)
|
||||
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('tracking_number', 'shipper', 'carrier', 'status', 'payment_status', 'thawani_session_id')
|
||||
@ -124,6 +130,21 @@ class ParcelAdmin(admin.ModelAdmin):
|
||||
'fields': ('delivery_country', 'delivery_governate', 'delivery_city', 'delivery_address', 'delivery_lat', 'delivery_lng')
|
||||
}),
|
||||
)
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
response = super().changelist_view(request, extra_context)
|
||||
|
||||
# Calculate totals for the filtered queryset
|
||||
if hasattr(response, 'context_data') and 'cl' in response.context_data:
|
||||
qs = response.context_data['cl'].queryset
|
||||
metrics = qs.aggregate(
|
||||
total_price=Sum('price'),
|
||||
total_driver_amount=Sum('driver_amount'),
|
||||
total_platform_fee=Sum('platform_fee')
|
||||
)
|
||||
response.context_data['summary_metrics'] = metrics
|
||||
|
||||
return response
|
||||
|
||||
def export_as_csv(self, request, queryset):
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
|
||||
33
core/templates/admin/core/parcel/change_list.html
Normal file
33
core/templates/admin/core/parcel/change_list.html
Normal file
@ -0,0 +1,33 @@
|
||||
{% extends "admin/change_list.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block result_list %}
|
||||
{{ block.super }}
|
||||
{% if summary_metrics %}
|
||||
<div class="card" style="margin-top: 15px; border: 1px solid #dee2e6;">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="card-title m-0">
|
||||
<i class="fas fa-calculator mr-2"></i> {% trans "Totals (Current Page/Filter)" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-striped table-bordered m-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">{% trans "Total Price" %}</th>
|
||||
<th class="text-center">{% trans "Total Driver Amount" %}</th>
|
||||
<th class="text-center">{% trans "Total Platform Fee" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center text-bold">{{ summary_metrics.total_price|default:0|stringformat:".3f" }} OMR</td>
|
||||
<td class="text-center text-bold">{{ summary_metrics.total_driver_amount|default:0|stringformat:".3f" }} OMR</td>
|
||||
<td class="text-center text-bold">{{ summary_metrics.total_platform_fee|default:0|stringformat:".3f" }} OMR</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@ -1,213 +0,0 @@
|
||||
{% load i18n rangefilter_compat static %}
|
||||
<h3>{{ title }}</h3>
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/widgets.css' %}">
|
||||
<style nonce="{{ spec.request.csp_nonce }}">
|
||||
{% default_css_vars_if_needed %}
|
||||
.admindatefilter .button, .admindatefilter input[type=submit], .admindatefilter input[type=button], .admindatefilter .submit-row input, .admindatefilter a.button,
|
||||
.admindatefilter .button, .admindatefilter input[type=reset] {
|
||||
background: var(--button-bg);
|
||||
padding: 4px 5px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: var(--button-fg);
|
||||
cursor: pointer;
|
||||
}
|
||||
.admindatefilter {
|
||||
padding-left: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.admindatefilter p {
|
||||
padding-left: 0px;
|
||||
line-height: 0;
|
||||
}
|
||||
.admindatefilter p.datetime {
|
||||
line-height: 0;
|
||||
}
|
||||
.admindatefilter .timezonewarning {
|
||||
display: none;
|
||||
}
|
||||
.admindatefilter .datetimeshortcuts a:first-child {
|
||||
margin-right: 4px;
|
||||
display: none;
|
||||
}
|
||||
.calendarbox {
|
||||
z-index: 1100;
|
||||
}
|
||||
.clockbox {
|
||||
z-index: 1100;
|
||||
margin-left: -8em !important;
|
||||
margin-top: 5em !important;
|
||||
}
|
||||
.admindatefilter .datetimeshortcuts {
|
||||
font-size: 0;
|
||||
float: right;
|
||||
position: absolute;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.admindatefilter a {
|
||||
color: #999;
|
||||
position: absolute;
|
||||
padding-top: 3px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
.range-preset-select {
|
||||
width: 95%;
|
||||
margin: 10px 0 10px 15px;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--body-bg);
|
||||
color: var(--body-fg);
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.calendarbox {
|
||||
margin-left: -16em !important;
|
||||
margin-top: 9em !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.calendarbox {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
|
||||
<script type="text/javascript" nonce="{{ spec.request.csp_nonce }}">
|
||||
function embedScript(url) {
|
||||
return new Promise(function pr(resolve, reject) {
|
||||
var newScript = document.createElement("script");
|
||||
newScript.type = "text/javascript";
|
||||
newScript.src = url;
|
||||
newScript.onload = resolve;
|
||||
if ("{{ spec.request.csp_nonce }}" !== "") {
|
||||
newScript.setAttribute("nonce", "{{ spec.request.csp_nonce }}");
|
||||
}
|
||||
document.head.appendChild(newScript);
|
||||
});
|
||||
}
|
||||
|
||||
django.jQuery(window).on('load', function () {
|
||||
if (!('DateTimeShortcuts' in window)) {
|
||||
var promiseList = [];
|
||||
{% for m in spec.form.js %}
|
||||
promiseList.push(embedScript("{{ m }}"));
|
||||
{% endfor %}
|
||||
Promise.all(promiseList).then(function() {
|
||||
django.jQuery('.datetimeshortcuts').remove();
|
||||
if ('DateTimeShortcuts' in window) {
|
||||
window.DateTimeShortcuts.init();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
django.jQuery('document').ready(function () {
|
||||
// Original logic for submit/reset
|
||||
django.jQuery('.admindatefilter #{{ choices.0.system_name }}-form input[type="submit"]').click(function(event) {
|
||||
event.preventDefault();
|
||||
var form = django.jQuery(this).closest('div.admindatefilter').find('form');
|
||||
var query_string = django.jQuery('input#{{ choices.0.system_name }}-query-string').val();
|
||||
var form_data = form.serialize();
|
||||
var amp = query_string === "?" ? "" : "&";
|
||||
window.location = window.location.pathname + query_string + amp + form_data;
|
||||
});
|
||||
|
||||
django.jQuery('.admindatefilter #{{ choices.0.system_name }}-form input[type="reset"]').click(function() {
|
||||
var form = django.jQuery(this).closest('div.admindatefilter').find('form');
|
||||
var query_string = form.find('input#{{ choices.0.system_name }}-query-string').val();
|
||||
window.location = window.location.pathname + query_string;
|
||||
});
|
||||
|
||||
// Custom Dropdown Logic
|
||||
var $ = django.jQuery;
|
||||
var formId = '#{{ choices.0.system_name }}-form';
|
||||
var $form = $(formId);
|
||||
var $container = $form.closest('.admindatefilter');
|
||||
|
||||
// Build Select
|
||||
var $select = $('<select class="range-preset-select">' +
|
||||
'<option value="all">{% trans "Any Date" %}</option>' +
|
||||
'<option value="today">{% trans "Today" %}</option>' +
|
||||
'<option value="7days">{% trans "Last 7 Days" %}</option>' +
|
||||
'<option value="month">{% trans "This Month" %}</option>' +
|
||||
'<option value="year">{% trans "This Year" %}</option>' +
|
||||
'<option value="custom">{% trans "Custom Range..." %}</option>' +
|
||||
'</select>');
|
||||
|
||||
$container.before($select);
|
||||
|
||||
var $gte = $form.find('input[name$="__gte"]');
|
||||
var $lte = $form.find('input[name$="__lte"]');
|
||||
var $submit = $form.find('input[type="submit"]');
|
||||
|
||||
function formatDate(d) {
|
||||
var year = d.getFullYear();
|
||||
var month = ('0' + (d.getMonth() + 1)).slice(-2);
|
||||
var day = ('0' + d.getDate()).slice(-2);
|
||||
return year + '-' + month + '-' + day;
|
||||
}
|
||||
|
||||
// Check initial state
|
||||
if ($gte.val() || $lte.val()) {
|
||||
$select.val('custom');
|
||||
$container.show();
|
||||
} else {
|
||||
$select.val('all');
|
||||
$container.hide();
|
||||
}
|
||||
|
||||
$select.on('change', function() {
|
||||
var val = $(this).val();
|
||||
var today = new Date();
|
||||
|
||||
if (val === 'custom') {
|
||||
$container.slideDown();
|
||||
} else {
|
||||
// If not custom, calculate and submit
|
||||
if (val === 'all') {
|
||||
// Reset
|
||||
$form.find('input[type="reset"]').click();
|
||||
return;
|
||||
}
|
||||
|
||||
var startStr = '';
|
||||
var endStr = formatDate(today); // Default end is today
|
||||
|
||||
if (val === 'today') {
|
||||
startStr = formatDate(today);
|
||||
} else if (val === '7days') {
|
||||
var past = new Date();
|
||||
past.setDate(today.getDate() - 7);
|
||||
startStr = formatDate(past);
|
||||
} else if (val === 'month') {
|
||||
var firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
startStr = formatDate(firstDay);
|
||||
} else if (val === 'year') {
|
||||
var firstDay = new Date(today.getFullYear(), 0, 1);
|
||||
startStr = formatDate(firstDay);
|
||||
}
|
||||
|
||||
$gte.val(startStr);
|
||||
$lte.val(endStr);
|
||||
|
||||
// Trigger submit
|
||||
$submit.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="admindatefilter">
|
||||
<form method="GET" action="." id="{{ choices.0.system_name }}-form">
|
||||
{{ spec.form.as_p }}
|
||||
{% for choice in choices %}
|
||||
<input type="hidden" id="{{ choice.system_name }}-query-string" value="{{ choice.query_string }}">
|
||||
{% endfor %}
|
||||
<div class="controls">
|
||||
<input type="submit" class="button" value="{% trans "Search" %}">
|
||||
<input type="reset" class="button" value="{% trans "Reset" %}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -177,4 +177,39 @@ body.model-platformprofile label[for="id_admin_panel_logo"] {
|
||||
.form-row {
|
||||
padding: 15px 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* --- Fix Admin Search Box Layout --- */
|
||||
|
||||
/* Target the search form container in Jazzmin/AdminLTE */
|
||||
#changelist-search,
|
||||
#changelist-search .form-group,
|
||||
#changelist-search .input-group {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
flex-wrap: nowrap !important; /* Prevent wrapping */
|
||||
max-width: 400px !important; /* Reduce the overall width */
|
||||
}
|
||||
|
||||
/* Make the input field take available space but respect the container width */
|
||||
#changelist-search input[type="text"] {
|
||||
width: auto !important;
|
||||
flex-grow: 1 !important;
|
||||
margin-right: 8px !important; /* Space between input and button */
|
||||
}
|
||||
|
||||
/* Fix for RTL */
|
||||
[dir="rtl"] #changelist-search input[type="text"] {
|
||||
margin-right: 0 !important;
|
||||
margin-left: 8px !important;
|
||||
}
|
||||
|
||||
/* Ensure the button stays inline and overrides any block behavior */
|
||||
#changelist-search button[type="submit"],
|
||||
#changelist-search .btn {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
white-space: nowrap !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
}
|
||||
135
static/js/admin_date_range_dropdown.js
Normal file
135
static/js/admin_date_range_dropdown.js
Normal file
@ -0,0 +1,135 @@
|
||||
(function($) {
|
||||
$(document).ready(function() {
|
||||
// Helper to format date as YYYY-MM-DD
|
||||
function formatDate(d) {
|
||||
var year = d.getFullYear();
|
||||
var month = ('0' + (d.getMonth() + 1)).slice(-2);
|
||||
var day = ('0' + d.getDate()).slice(-2);
|
||||
return year + '-' + month + '-' + day;
|
||||
}
|
||||
|
||||
function initDateRangeDropdown() {
|
||||
// Find start date inputs for 'created_at' (or generic generic approach for any range filter)
|
||||
// rangefilter inputs typically have names ending in __gte and __lte
|
||||
var $gteInputs = $('input[name$="__gte"]');
|
||||
|
||||
$gteInputs.each(function() {
|
||||
var $gte = $(this);
|
||||
var name = $gte.attr('name');
|
||||
var prefix = name.substring(0, name.lastIndexOf('__gte'));
|
||||
var $lte = $('input[name="' + prefix + '__lte"]');
|
||||
|
||||
if ($lte.length === 0) return; // Not a pair
|
||||
|
||||
// Find a container to inject the dropdown.
|
||||
// In Jazzmin/Standard, this might be inside a .admindatefilter div,
|
||||
// or just a li, or a div.controls.
|
||||
// We'll look for the closest container that seems to wrap the filter.
|
||||
|
||||
// Try to find .admindatefilter first
|
||||
var $container = $gte.closest('.admindatefilter');
|
||||
if ($container.length === 0) {
|
||||
// Fallback for Jazzmin or other themes: closest .card-body or similar?
|
||||
// Or just the parent form/div
|
||||
$container = $gte.closest('div[data-filter-name], li, .form-row, .card-body');
|
||||
}
|
||||
|
||||
if ($container.length === 0) $container = $gte.parent();
|
||||
|
||||
if ($container.data('dropdown-init')) return;
|
||||
$container.data('dropdown-init', true);
|
||||
|
||||
// Hide the original inputs/controls container
|
||||
// We need to be careful not to hide the form itself if it's the main filter form
|
||||
// Usually rangefilter puts inputs in a 'controls' div or paragraphs.
|
||||
var $controls = $gte.closest('.controls');
|
||||
if ($controls.length === 0) {
|
||||
// Try to find the immediate parent if it contains both inputs
|
||||
$controls = $gte.parent();
|
||||
}
|
||||
|
||||
// Create Select
|
||||
var $select = $('<select class="form-control admin-date-dropdown" style="width: 100%; margin-bottom: 10px; margin-top: 5px;">' +
|
||||
'<option value="any">Any Date</option>' +
|
||||
'<option value="today">Today</option>' +
|
||||
'<option value="7days">Last 7 Days</option>' +
|
||||
'<option value="month">This Month</option>' +
|
||||
'<option value="year">This Year</option>' +
|
||||
'<option value="custom">Custom Range...</option>' +
|
||||
'</select>');
|
||||
|
||||
// Inject before the controls (inputs)
|
||||
if ($controls.length) {
|
||||
$controls.before($select);
|
||||
} else {
|
||||
$gte.before($select);
|
||||
}
|
||||
|
||||
// Initial State
|
||||
var gteVal = $gte.val();
|
||||
var lteVal = $lte.val();
|
||||
|
||||
if (gteVal || lteVal) {
|
||||
$select.val('custom');
|
||||
$controls.show();
|
||||
} else {
|
||||
$select.val('any');
|
||||
$controls.hide();
|
||||
}
|
||||
|
||||
$select.on('change', function() {
|
||||
var val = $(this).val();
|
||||
var today = new Date();
|
||||
|
||||
if (val === 'custom') {
|
||||
$controls.slideDown();
|
||||
} else {
|
||||
if (val === 'any') {
|
||||
$gte.val('');
|
||||
$lte.val('');
|
||||
} else {
|
||||
var startStr = '';
|
||||
var endStr = formatDate(today);
|
||||
|
||||
if (val === 'today') {
|
||||
startStr = formatDate(today);
|
||||
} else if (val === '7days') {
|
||||
var past = new Date();
|
||||
past.setDate(today.getDate() - 7);
|
||||
startStr = formatDate(past);
|
||||
} else if (val === 'month') {
|
||||
var firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
startStr = formatDate(firstDay);
|
||||
} else if (val === 'year') {
|
||||
var firstDay = new Date(today.getFullYear(), 0, 1);
|
||||
startStr = formatDate(firstDay);
|
||||
}
|
||||
|
||||
$gte.val(startStr);
|
||||
$lte.val(endStr);
|
||||
}
|
||||
|
||||
// Submit form
|
||||
// In Jazzmin, the filter form might be #changelist-search or similiar
|
||||
var $form = $gte.closest('form');
|
||||
if ($form.length) {
|
||||
$form.submit();
|
||||
} else {
|
||||
// Try to find a global apply button or trigger change?
|
||||
// Some admin themes auto-submit on change.
|
||||
// rangefilter usually has a submit button.
|
||||
var $btn = $container.find('input[type="submit"], button[type="submit"]');
|
||||
if ($btn.length) $btn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Run init
|
||||
initDateRangeDropdown();
|
||||
|
||||
// Safety: Run again after a slight delay in case of dynamic loading (unlikely in admin but possible)
|
||||
setTimeout(initDateRangeDropdown, 500);
|
||||
});
|
||||
})(django.jQuery);
|
||||
Loading…
x
Reference in New Issue
Block a user