diff --git a/assets/pasted-20260201-075032-c186b500.png b/assets/pasted-20260201-075032-c186b500.png new file mode 100644 index 0000000..f8f9f68 Binary files /dev/null and b/assets/pasted-20260201-075032-c186b500.png differ diff --git a/assets/pasted-20260201-081838-f3ed2987.png b/assets/pasted-20260201-081838-f3ed2987.png new file mode 100644 index 0000000..46a64d7 Binary files /dev/null and b/assets/pasted-20260201-081838-f3ed2987.png differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 7edfd23..d8a1292 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/api_views.cpython-311.pyc b/core/__pycache__/api_views.cpython-311.pyc index 1311734..9181153 100644 Binary files a/core/__pycache__/api_views.cpython-311.pyc and b/core/__pycache__/api_views.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 6cd1cb2..10f3205 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2314145..cff6249 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index aa3aeed..bb32de7 100644 --- a/core/admin.py +++ b/core/admin.py @@ -24,19 +24,26 @@ class ProfileInline(admin.StackedInline): verbose_name_plural = _('Profiles') fieldsets = ( (None, {'fields': ('role', 'is_approved', 'phone_number', 'profile_picture', 'address')}), + (_('Driver Assessment'), {'fields': ('driver_grade', 'is_recommended')}), (_('Driver Info'), {'fields': ('license_front_image', 'license_back_image', 'car_plate_number', 'bank_account_number'), 'classes': ('collapse',)}), (_('Location'), {'fields': ('country', 'governate', 'city'), 'classes': ('collapse',)}), ) class CustomUserAdmin(UserAdmin): inlines = (ProfileInline,) - list_display = ('username', 'email', 'get_role', 'get_approval_status', 'is_active', 'is_staff', 'send_whatsapp_link') - list_filter = ('is_active', 'is_staff', 'profile__role', 'profile__is_approved') + list_display = ('username', 'email', 'get_role', 'get_driver_grade', 'get_approval_status', 'is_active', 'is_staff', 'send_whatsapp_link') + list_filter = ('is_active', 'is_staff', 'profile__role', 'profile__is_approved', 'profile__driver_grade') def get_role(self, obj): return obj.profile.get_role_display() get_role.short_description = _('Role') + def get_driver_grade(self, obj): + if obj.profile.role == 'car_owner': + return obj.profile.get_driver_grade_display() + return "-" + get_driver_grade.short_description = _('Grade') + def get_approval_status(self, obj): return obj.profile.is_approved get_approval_status.short_description = _('Approved') @@ -192,6 +199,10 @@ class PlatformProfileAdmin(admin.ModelAdmin): (_('Financial Configuration'), { 'fields': ('platform_fee_percentage', 'enable_payment') }), + (_('Maintenance / Availability'), { + 'fields': ('accepting_shipments', 'maintenance_message_en', 'maintenance_message_ar'), + 'description': _('Toggle to allow or stop receiving new parcel shipments. If stopped, buttons will turn red and an alert will be shown.') + }), (_('Testing / Development'), { 'fields': ('auto_mark_paid',), 'description': _('Enable this to automatically mark NEW parcels as "Paid" (useful for testing so drivers can see them immediately).') @@ -356,4 +367,4 @@ class NotificationTemplateAdmin(admin.ModelAdmin): def has_delete_permission(self, request, obj=None): return False -admin.site.register(NotificationTemplate, NotificationTemplateAdmin) \ No newline at end of file +admin.site.register(NotificationTemplate, NotificationTemplateAdmin) diff --git a/core/api_views.py b/core/api_views.py index 267d59e..7f55810 100644 --- a/core/api_views.py +++ b/core/api_views.py @@ -47,6 +47,10 @@ class ParcelListCreateView(generics.ListCreateAPIView): return Parcel.objects.none() def perform_create(self, serializer): + from .models import PlatformProfile + platform_profile = PlatformProfile.objects.first() + if platform_profile and not platform_profile.accepting_shipments: + raise permissions.PermissionDenied(platform_profile.maintenance_message or "The platform is currently not accepting new shipments.") # Only shippers can create if self.request.user.profile.role != 'shipper': raise permissions.PermissionDenied("Only shippers can create parcels.") diff --git a/core/migrations/0027_profile_driver_grade_profile_is_recommended.py b/core/migrations/0027_profile_driver_grade_profile_is_recommended.py new file mode 100644 index 0000000..fc776c7 --- /dev/null +++ b/core/migrations/0027_profile_driver_grade_profile_is_recommended.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2026-02-01 12:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0026_profile_bank_account_number'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='driver_grade', + field=models.CharField(choices=[('none', 'No Grade'), ('bronze_3', 'Bronze III'), ('bronze_2', 'Bronze II'), ('bronze_1', 'Bronze I'), ('silver', 'Silver'), ('gold', 'Gold')], default='none', max_length=20, verbose_name='Driver Grade'), + ), + migrations.AddField( + model_name='profile', + name='is_recommended', + field=models.BooleanField(default=False, verbose_name='Recommended by Shippers'), + ), + ] diff --git a/core/migrations/0028_platformprofile_accepting_shipments_and_more.py b/core/migrations/0028_platformprofile_accepting_shipments_and_more.py new file mode 100644 index 0000000..7d7ba3f --- /dev/null +++ b/core/migrations/0028_platformprofile_accepting_shipments_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2026-02-01 13:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0027_profile_driver_grade_profile_is_recommended'), + ] + + operations = [ + migrations.AddField( + model_name='platformprofile', + name='accepting_shipments', + field=models.BooleanField(default=True, help_text='Toggle to allow or stop receiving new parcel shipments.', verbose_name='Accepting Shipments'), + ), + migrations.AddField( + model_name='platformprofile', + name='maintenance_message_ar', + field=models.TextField(blank=True, help_text='Message to show when shipments are stopped.', verbose_name='Maintenance Message (Arabic)'), + ), + migrations.AddField( + model_name='platformprofile', + name='maintenance_message_en', + field=models.TextField(blank=True, help_text='Message to show when shipments are stopped.', verbose_name='Maintenance Message (English)'), + ), + ] diff --git a/core/migrations/__pycache__/0027_profile_driver_grade_profile_is_recommended.cpython-311.pyc b/core/migrations/__pycache__/0027_profile_driver_grade_profile_is_recommended.cpython-311.pyc new file mode 100644 index 0000000..19f9d8e Binary files /dev/null and b/core/migrations/__pycache__/0027_profile_driver_grade_profile_is_recommended.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0028_platformprofile_accepting_shipments_and_more.cpython-311.pyc b/core/migrations/__pycache__/0028_platformprofile_accepting_shipments_and_more.cpython-311.pyc new file mode 100644 index 0000000..8a93b44 Binary files /dev/null and b/core/migrations/__pycache__/0028_platformprofile_accepting_shipments_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 9f05d9d..bbb6361 100644 --- a/core/models.py +++ b/core/models.py @@ -65,8 +65,16 @@ class City(models.Model): class Profile(models.Model): ROLE_CHOICES = ( - ('shipper', _('Shipper')), - ('car_owner', _('Car Owner')), + ("shipper", _("Shipper")), + ("car_owner", _("Car Owner")), + ) + DRIVER_GRADE_CHOICES = ( + ("none", _("No Grade")), + ("bronze_3", _("Bronze III")), + ("bronze_2", _("Bronze II")), + ("bronze_1", _("Bronze I")), + ("silver", _("Silver")), + ("gold", _("Gold")), ) user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name=_('User')) role = models.CharField(_('Role'), max_length=20, choices=ROLE_CHOICES, default='shipper') @@ -85,6 +93,10 @@ class Profile(models.Model): city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('City')) # Approval Status + # Driver Assessment + driver_grade = models.CharField(_("Driver Grade"), max_length=20, choices=DRIVER_GRADE_CHOICES, default="none") + is_recommended = models.BooleanField(_("Recommended by Shippers"), default=False) + is_approved = models.BooleanField(_('Approved'), default=False, help_text=_("Designates whether this user is approved to use the platform (mainly for drivers).")) def __str__(self): @@ -165,6 +177,17 @@ class PlatformProfile(models.Model): # Testing / Development auto_mark_paid = models.BooleanField(_('Test Mode: Auto-Paid'), default=False, help_text=_("If enabled, newly created parcels will automatically be marked as 'Paid' for testing.")) + + # Maintenance / Availability + accepting_shipments = models.BooleanField(_("Accepting Shipments"), default=True, help_text=_("Toggle to allow or stop receiving new parcel shipments.")) + maintenance_message_en = models.TextField(_("Maintenance Message (English)"), blank=True, help_text=_("Message to show when shipments are stopped.")) + maintenance_message_ar = models.TextField(_("Maintenance Message (Arabic)"), blank=True, help_text=_("Message to show when shipments are stopped.")) + + @property + def maintenance_message(self): + if get_language() == "ar": + return self.maintenance_message_ar or _("Service is temporarily suspended. Please try again later.") + return self.maintenance_message_en or _("Service is temporarily suspended. Please try again later.") @property def privacy_policy(self): @@ -418,4 +441,4 @@ class NotificationTemplate(models.Model): class Meta: verbose_name = _('Notification Template') - verbose_name_plural = _('Notification Templates') \ No newline at end of file + verbose_name_plural = _('Notification Templates') diff --git a/core/templates/base.html b/core/templates/base.html index 8abc884..3b9bef3 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -170,7 +170,7 @@ {% if not user.is_authenticated %} {% endif %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index f9a9518..4b7cb92 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -2,6 +2,14 @@ {% load static i18n %} {% block content %} + + +
@@ -9,8 +17,13 @@

{% trans "Small Shipments," %}
{% trans "Smart Delivery." %}

{% trans "masarX connects shippers with local car owners for fast, reliable, and trackable deliveries. Your cargo, our priority." %}

+ {% if not platform_profile.accepting_shipments and platform_profile.maintenance_message %} +
+ {{ platform_profile.maintenance_message }} +
+ {% endif %}
- {% trans "Start Shipping" %} + {% if platform_profile.accepting_shipments %}{% trans "Start Shipping" %}{% else %}{% endif %} {% trans "Learn More" %}
@@ -94,7 +107,21 @@
-
{{ driver.user.first_name }} {{ driver.user.last_name|first }}.
+
+ {{ driver.user.first_name }} {{ driver.user.last_name|first }}. + {% if driver.driver_grade != "none" %} + {% if "bronze" in driver.driver_grade %} + {{ driver.get_driver_grade_display }} + {% elif driver.driver_grade == "silver" %} + Silver + {% elif driver.driver_grade == "gold" %} + Gold + {% endif %} + {% endif %} + {% if driver.is_recommended %} + + {% endif %} +
{% trans "Driver" %}
diff --git a/core/templates/core/profile.html b/core/templates/core/profile.html index 5bcb89e..2ce2f21 100644 --- a/core/templates/core/profile.html +++ b/core/templates/core/profile.html @@ -4,6 +4,13 @@ {% block title %}{% trans "My Profile" %} | masarX{% endblock %} {% block content %} + + +
@@ -32,7 +39,21 @@ {{ profile.user.first_name|first|upper }}
{% endif %} -

{{ profile.user.get_full_name }}

+

+ {{ profile.user.get_full_name }} + {% if profile.driver_grade != "none" %} + {% if "bronze" in profile.driver_grade %} + {{ profile.get_driver_grade_display }} + {% elif profile.driver_grade == "silver" %} + Silver + {% elif profile.driver_grade == "gold" %} + Gold + {% endif %} + {% endif %} + {% if profile.is_recommended %} + + {% endif %} +

{{ profile.get_role_display }}

diff --git a/core/templates/core/shipper_dashboard.html b/core/templates/core/shipper_dashboard.html index ffc725c..b66f53b 100644 --- a/core/templates/core/shipper_dashboard.html +++ b/core/templates/core/shipper_dashboard.html @@ -2,10 +2,26 @@ {% load i18n core_tags %} {% block content %} + + +
+ {% if not platform_profile.accepting_shipments and platform_profile.maintenance_message %} +
+ +
+
{% trans "Service Notice" %}
+

{{ platform_profile.maintenance_message }}

+
+
+ {% endif %}

{% trans "My Shipments" %}

- {% trans "New Shipment" %} + {% if platform_profile.accepting_shipments %}{% trans "New Shipment" %}{% else %}{% endif %}
@@ -97,7 +113,24 @@

{% trans "Receiver" %}: {{ parcel.receiver_name }}

-

{% trans "Carrier" %}: {% if parcel.carrier %}{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}{% else %}{% trans "Waiting for pickup" %}{% endif %}

+

{% trans "Carrier" %}: + {% if parcel.carrier %} + {{ parcel.carrier.get_full_name|default:parcel.carrier.username }} + {% if parcel.carrier.profile.driver_grade != "none" %} + {% if "bronze" in parcel.carrier.profile.driver_grade %} + + {% elif parcel.carrier.profile.driver_grade == "silver" %} + + {% elif parcel.carrier.profile.driver_grade == "gold" %} + + {% endif %} + {% endif %} + {% if parcel.carrier.profile.is_recommended %} + + {% endif %} + {% else %} + {% trans "Waiting for pickup" %} + {% endif %}

@@ -130,7 +163,23 @@ - {% if parcel.carrier %}{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}{% else %}{% trans "Waiting" %}{% endif %} + {% if parcel.carrier %} + {{ parcel.carrier.get_full_name|default:parcel.carrier.username }} + {% if parcel.carrier.profile.driver_grade != "none" %} + {% if "bronze" in parcel.carrier.profile.driver_grade %} + + {% elif parcel.carrier.profile.driver_grade == "silver" %} + + {% elif parcel.carrier.profile.driver_grade == "gold" %} + + {% endif %} + {% endif %} + {% if parcel.carrier.profile.is_recommended %} + + {% endif %} + {% else %} + {% trans "Waiting" %} + {% endif %} @@ -223,7 +272,7 @@ {% else %}

{% trans "You have no active shipments." %}

- {% trans "Send your first shipment" %} + {% if platform_profile.accepting_shipments %}{% trans "Send your first shipment" %}{% else %}{% endif %}
{% endif %} diff --git a/core/views.py b/core/views.py index ce8936d..881c139 100644 --- a/core/views.py +++ b/core/views.py @@ -286,6 +286,11 @@ def dashboard(request): @login_required def shipment_request(request): + from .models import PlatformProfile + platform_profile = PlatformProfile.objects.first() + if platform_profile and not platform_profile.accepting_shipments: + messages.warning(request, platform_profile.maintenance_message or _("The platform is currently not accepting new shipments.")) + return redirect("dashboard") profile, created = Profile.objects.get_or_create(user=request.user) if profile.role != 'shipper': messages.error(request, _("Only shippers can request shipments.")) diff --git a/static/css/custom_v2.css b/static/css/custom_v2.css index 8587f17..ee1e4ac 100644 --- a/static/css/custom_v2.css +++ b/static/css/custom_v2.css @@ -238,4 +238,31 @@ h1, h2, h3, h4, h5, h6 { } [dir="rtl"] .text-left { text-align: right !important; -} \ No newline at end of file +} +/* Shipment Status Buttons */ +.btn-masarx-active { + background-color: #2fb344 !important; + color: white !important; + border: none !important; + padding: 12px 30px !important; + border-radius: 12px !important; + font-weight: 600 !important; +} +.btn-masarx-active:hover { + background-color: #248a35 !important; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(47, 179, 68, 0.2); +} +.btn-masarx-stopped { + background-color: #d63939 !important; + color: white !important; + border: none !important; + padding: 12px 30px !important; + border-radius: 12px !important; + font-weight: 600 !important; +} +.btn-masarx-stopped:hover { + background-color: #b02a2a !important; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(214, 57, 57, 0.2); +} diff --git a/staticfiles/css/custom_v2.css b/staticfiles/css/custom_v2.css index 440a5f1..ee1e4ac 100644 --- a/staticfiles/css/custom_v2.css +++ b/staticfiles/css/custom_v2.css @@ -26,11 +26,12 @@ h1, h2, h3, h4, h5, h6 { flex-direction: row !important; align-items: center !important; justify-content: space-between !important; - flex-wrap: nowrap !important; /* Ensure they stay on one line */ + flex-wrap: nowrap !important; gap: 2px !important; width: 100% !important; box-sizing: border-box !important; - /* overflow: hidden; Removed to allow dropdown list to show */ + position: relative !important; + z-index: 100 !important; } /* Hide any stray shortcuts that might have survived JS cleanup */ @@ -39,14 +40,21 @@ h1, h2, h3, h4, h5, h6 { } .masar-date-filter-row select { + appearance: auto !important; /* Force native dropdown appearance */ + -webkit-appearance: auto !important; + -moz-appearance: auto !important; + width: 32% !important; min-width: 0 !important; font-size: 11px !important; - padding: 0 2px !important; + padding: 2px !important; /* Relaxed padding */ height: 28px !important; - line-height: 1 !important; box-sizing: border-box !important; margin: 0 !important; + background-color: white !important; + border: 1px solid #ced4da !important; + border-radius: 4px !important; + color: #495057 !important; } .masar-date-filter-row input { @@ -57,22 +65,23 @@ h1, h2, h3, h4, h5, h6 { height: 28px !important; margin: 0 !important; box-sizing: border-box !important; + background-color: white !important; + border: 1px solid #ced4da !important; + border-radius: 4px !important; } /* Specific fix for date inputs to ensure they look clean */ .masar-date-filter-row input[type="date"] { - -webkit-appearance: none; /* Remove some browser defaults */ - appearance: none; line-height: 28px; } .masar-date-filter-row input[type="date"]::-webkit-inner-spin-button, .masar-date-filter-row input[type="date"]::-webkit-calendar-picker-indicator { - /* Make the calendar icon smaller and fit */ width: 12px; height: 12px; margin: 0; padding: 0; opacity: 0.6; + cursor: pointer; } @@ -229,4 +238,31 @@ h1, h2, h3, h4, h5, h6 { } [dir="rtl"] .text-left { text-align: right !important; -} \ No newline at end of file +} +/* Shipment Status Buttons */ +.btn-masarx-active { + background-color: #2fb344 !important; + color: white !important; + border: none !important; + padding: 12px 30px !important; + border-radius: 12px !important; + font-weight: 600 !important; +} +.btn-masarx-active:hover { + background-color: #248a35 !important; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(47, 179, 68, 0.2); +} +.btn-masarx-stopped { + background-color: #d63939 !important; + color: white !important; + border: none !important; + padding: 12px 30px !important; + border-radius: 12px !important; + font-weight: 600 !important; +} +.btn-masarx-stopped:hover { + background-color: #b02a2a !important; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(214, 57, 57, 0.2); +} diff --git a/staticfiles/js/admin_date_range_dropdown.js b/staticfiles/js/admin_date_range_dropdown.js index 210290d..320bcb3 100644 --- a/staticfiles/js/admin_date_range_dropdown.js +++ b/staticfiles/js/admin_date_range_dropdown.js @@ -1,7 +1,7 @@ (function($) { - // Masar Date Range Filter Layout Fix v4 + // Masar Date Range Filter Layout Fix v5 // Forces a horizontal layout for the Date Range Filter in Django Admin Sidebar - // v4: Switches to type="date", removes Django's calendar shortcuts to prevent layout breakage. + // v5: Removes Bootstrap classes from Select to ensure native popup works reliably. function initDateRangeDropdown() { @@ -30,7 +30,8 @@ var $wrapper = $('
'); // Create the Quick Select Dropdown - var $select = $('' + '' + '' + '' + @@ -39,7 +40,6 @@ ''); // CONVERT INPUTS TO HTML5 DATE - // This gives us a native picker and removes the need for Django's clunky JS shortcuts $gte.attr('type', 'date').removeClass('vDateField'); $lte.attr('type', 'date').removeClass('vDateField'); @@ -53,8 +53,6 @@ $wrapper.append($lte); // 3. AGGRESSIVE CLEANUP - // Remove text nodes, BRs, AND Django's calendar shortcuts (.datetimeshortcuts) - // We search the *original parent* for these leftovers. $parent.contents().filter(function() { return ( (this.nodeType === 3 && $.trim($(this).text()) !== '') || // Text @@ -63,9 +61,6 @@ ); }).remove(); - // Also hide any shortcuts that might be dynamically appended later (via CSS rule or observer) - // But removing the 'vDateField' class above usually prevents Django from initializing them. - // Logic for Dropdown Changes function formatDate(d) { var year = d.getFullYear(); @@ -87,7 +82,7 @@ var today = new Date(); if (val === 'custom') { - // Do nothing, let user edit + // Do nothing } else { if (val === 'any') { $gte.val(''); diff --git a/staticfiles/pasted-20260201-075032-c186b500.png b/staticfiles/pasted-20260201-075032-c186b500.png new file mode 100644 index 0000000..f8f9f68 Binary files /dev/null and b/staticfiles/pasted-20260201-075032-c186b500.png differ diff --git a/staticfiles/pasted-20260201-081838-f3ed2987.png b/staticfiles/pasted-20260201-081838-f3ed2987.png new file mode 100644 index 0000000..46a64d7 Binary files /dev/null and b/staticfiles/pasted-20260201-081838-f3ed2987.png differ