diff --git a/config/settings.py b/config/settings.py index ced0121..270e992 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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, diff --git a/core/admin.py b/core/admin.py index cf5c280..3a39b62 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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') diff --git a/core/templates/admin/core/parcel/change_list.html b/core/templates/admin/core/parcel/change_list.html new file mode 100644 index 0000000..ec1ce00 --- /dev/null +++ b/core/templates/admin/core/parcel/change_list.html @@ -0,0 +1,33 @@ +{% extends "admin/change_list.html" %} +{% load i18n %} + +{% block result_list %} + {{ block.super }} + {% if summary_metrics %} +
+
+
+ {% trans "Totals (Current Page/Filter)" %} +
+
+
+ + + + + + + + + + + + + + + +
{% trans "Total Price" %}{% trans "Total Driver Amount" %}{% trans "Total Platform Fee" %}
{{ summary_metrics.total_price|default:0|stringformat:".3f" }} OMR{{ summary_metrics.total_driver_amount|default:0|stringformat:".3f" }} OMR{{ summary_metrics.total_platform_fee|default:0|stringformat:".3f" }} OMR
+
+
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/admin/dropdown_date_range_filter.html b/core/templates/admin/dropdown_date_range_filter.html deleted file mode 100644 index 7bbde95..0000000 --- a/core/templates/admin/dropdown_date_range_filter.html +++ /dev/null @@ -1,213 +0,0 @@ -{% load i18n rangefilter_compat static %} -

{{ title }}

- - - - - - -
-
- {{ spec.form.as_p }} - {% for choice in choices %} - - {% endfor %} -
- - -
-
-
diff --git a/static/css/custom.css b/static/css/custom.css index 0749166..b3c7f4b 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -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; } \ No newline at end of file diff --git a/static/js/admin_date_range_dropdown.js b/static/js/admin_date_range_dropdown.js new file mode 100644 index 0000000..80b92ae --- /dev/null +++ b/static/js/admin_date_range_dropdown.js @@ -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 = $(''); + + // 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); \ No newline at end of file