diff --git a/assets/pasted-20260201-135726-63c413b4.png b/assets/pasted-20260201-135726-63c413b4.png
new file mode 100644
index 0000000..6dd6d7e
Binary files /dev/null and b/assets/pasted-20260201-135726-63c413b4.png differ
diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc
index d8a1292..f06e27f 100644
Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ
diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc
index 7eb8830..08813f3 100644
Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ
diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc
index 10f3205..7fb33b1 100644
Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ
diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc
index 30866b8..28a9da4 100644
Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ
diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc
index cff6249..67cb0f9 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 bb32de7..3ac0780 100644
--- a/core/admin.py
+++ b/core/admin.py
@@ -1,7 +1,7 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
-from .models import Profile, Parcel, Country, Governate, City, PlatformProfile, Testimonial, DriverRating, NotificationTemplate, PricingRule
+from .models import Profile, Parcel, Country, Governate, City, PlatformProfile, Testimonial, DriverRating, NotificationTemplate, PricingRule, DriverReport, DriverRejection
from django.utils.translation import gettext_lazy as _
from django.urls import path, reverse
from django.shortcuts import render, redirect
@@ -23,7 +23,7 @@ class ProfileInline(admin.StackedInline):
can_delete = False
verbose_name_plural = _('Profiles')
fieldsets = (
- (None, {'fields': ('role', 'is_approved', 'phone_number', 'profile_picture', 'address')}),
+ (None, {'fields': ('role', 'is_approved', 'is_banned', 'ban_reason', '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',)}),
@@ -31,8 +31,8 @@ class ProfileInline(admin.StackedInline):
class CustomUserAdmin(UserAdmin):
inlines = (ProfileInline,)
- 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')
+ list_display = ('username', 'email', 'get_role', 'get_driver_grade', 'get_approval_status', 'get_ban_status', 'is_active', 'is_staff', 'send_whatsapp_link')
+ list_filter = ('is_active', 'is_staff', 'profile__role', 'profile__is_approved', 'profile__is_banned', 'profile__driver_grade')
def get_role(self, obj):
return obj.profile.get_role_display()
@@ -48,6 +48,11 @@ class CustomUserAdmin(UserAdmin):
return obj.profile.is_approved
get_approval_status.short_description = _('Approved')
get_approval_status.boolean = True
+
+ def get_ban_status(self, obj):
+ return obj.profile.is_banned
+ get_ban_status.short_description = _('Banned')
+ get_ban_status.boolean = True
def get_inline_instances(self, request, obj=None):
if not obj:
@@ -203,6 +208,10 @@ class PlatformProfileAdmin(admin.ModelAdmin):
'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.')
}),
+ (_('Driver Rejection / Auto-Ban'), {
+ 'fields': ('auto_ban_on_rejections', 'rejection_limit'),
+ 'description': _('Configure automatic banning for drivers who reject too many shipments.')
+ }),
(_('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).')
@@ -332,6 +341,28 @@ class TestimonialAdmin(admin.ModelAdmin):
search_fields = ('name_en', 'name_ar', 'content_en', 'content_ar')
list_editable = ('is_active',)
+class DriverReportAdmin(admin.ModelAdmin):
+ list_display = ('driver', 'reporter', 'reason', 'status', 'created_at')
+ list_filter = ('status', 'reason', 'created_at')
+ search_fields = ('driver__username', 'reporter__username', 'description', 'admin_note')
+ list_editable = ('status',)
+
+ fieldsets = (
+ (_('Report Details'), {
+ 'fields': ('reporter', 'driver', 'parcel', 'reason', 'description', 'created_at')
+ }),
+ (_('Investigation'), {
+ 'fields': ('status', 'admin_note')
+ }),
+ )
+ readonly_fields = ('created_at',)
+
+class DriverRejectionAdmin(admin.ModelAdmin):
+ list_display = ('driver', 'parcel', 'reason', 'created_at')
+ list_filter = ('created_at',)
+ search_fields = ('driver__username', 'parcel__tracking_number', 'reason')
+ readonly_fields = ('driver', 'parcel', 'reason', 'created_at')
+
admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)
admin.site.register(Parcel, ParcelAdmin)
@@ -342,6 +373,9 @@ admin.site.register(PlatformProfile, PlatformProfileAdmin)
admin.site.register(Testimonial, TestimonialAdmin)
admin.site.register(DriverRating)
admin.site.register(PricingRule, PricingRuleAdmin)
+admin.site.register(DriverReport, DriverReportAdmin)
+admin.site.register(DriverRejection, DriverRejectionAdmin)
+
class NotificationTemplateAdmin(admin.ModelAdmin):
list_display = ('key', 'description')
readonly_fields = ('key', 'description', 'available_variables')
diff --git a/core/forms.py b/core/forms.py
index 741107d..05ff5b3 100644
--- a/core/forms.py
+++ b/core/forms.py
@@ -2,7 +2,7 @@ from django import forms
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from django.utils.translation import get_language
-from .models import Profile, Parcel, Country, Governate, City, DriverRating
+from .models import Profile, Parcel, Country, Governate, City, DriverRating, DriverReport
class ContactForm(forms.Form):
name = forms.CharField(max_length=100, label=_("Name"), widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Your Name')}))
@@ -393,4 +393,17 @@ class DriverRatingForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Reverse choices for CSS star rating logic (5 to 1) to ensure left-to-right filling
- self.fields['rating'].choices = [(i, str(i)) for i in range(5, 0, -1)]
\ No newline at end of file
+ self.fields['rating'].choices = [(i, str(i)) for i in range(5, 0, -1)]
+
+class DriverReportForm(forms.ModelForm):
+ class Meta:
+ model = DriverReport
+ fields = ['reason', 'description']
+ widgets = {
+ 'reason': forms.Select(attrs={'class': 'form-select'}),
+ 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4, 'placeholder': _('Please provide details about the incident...')}),
+ }
+ labels = {
+ 'reason': _('Reason for Reporting'),
+ 'description': _('Details'),
+ }
diff --git a/core/migrations/0029_driverreport.py b/core/migrations/0029_driverreport.py
new file mode 100644
index 0000000..949d9de
--- /dev/null
+++ b/core/migrations/0029_driverreport.py
@@ -0,0 +1,36 @@
+# Generated by Django 5.2.7 on 2026-02-01 13:24
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0028_platformprofile_accepting_shipments_and_more'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='DriverReport',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('reason', models.CharField(choices=[('unprofessional', 'Unprofessional Behavior'), ('reckless_driving', 'Reckless Driving'), ('delayed_delivery', 'Significant Delay'), ('item_damaged', 'Item Damaged'), ('item_missing', 'Item Missing'), ('other', 'Other')], max_length=50, verbose_name='Reason')),
+ ('description', models.TextField(verbose_name='Detailed Description')),
+ ('status', models.CharField(choices=[('pending', 'Pending Investigation'), ('investigating', 'Investigating'), ('resolved', 'Resolved'), ('dismissed', 'Dismissed')], default='pending', max_length=20, verbose_name='Status')),
+ ('admin_note', models.TextField(blank=True, verbose_name='Admin Internal Note')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
+ ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
+ ('driver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reports_received', to=settings.AUTH_USER_MODEL, verbose_name='Driver')),
+ ('parcel', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reports', to='core.parcel', verbose_name='Related Parcel')),
+ ('reporter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='filed_reports', to=settings.AUTH_USER_MODEL, verbose_name='Reporter')),
+ ],
+ options={
+ 'verbose_name': 'Driver Report',
+ 'verbose_name_plural': 'Driver Reports',
+ 'ordering': ['-created_at'],
+ },
+ ),
+ ]
diff --git a/core/migrations/0030_platformprofile_auto_ban_on_rejections_and_more.py b/core/migrations/0030_platformprofile_auto_ban_on_rejections_and_more.py
new file mode 100644
index 0000000..edbefec
--- /dev/null
+++ b/core/migrations/0030_platformprofile_auto_ban_on_rejections_and_more.py
@@ -0,0 +1,51 @@
+# Generated by Django 5.2.7 on 2026-02-01 13:38
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0029_driverreport'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='platformprofile',
+ name='auto_ban_on_rejections',
+ field=models.BooleanField(default=False, help_text='Automatically ban drivers who exceed a certain number of rejections.', verbose_name='Enable Auto-Ban on Rejections'),
+ ),
+ migrations.AddField(
+ model_name='platformprofile',
+ name='rejection_limit',
+ field=models.PositiveIntegerField(default=5, help_text='Number of rejections allowed before auto-ban.', verbose_name='Rejection Limit'),
+ ),
+ migrations.AddField(
+ model_name='profile',
+ name='ban_reason',
+ field=models.TextField(blank=True, verbose_name='Ban Reason'),
+ ),
+ migrations.AddField(
+ model_name='profile',
+ name='is_banned',
+ field=models.BooleanField(default=False, verbose_name='Banned'),
+ ),
+ migrations.CreateModel(
+ name='DriverRejection',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('reason', models.TextField(verbose_name='Reason for Rejection')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
+ ('driver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rejections', to=settings.AUTH_USER_MODEL, verbose_name='Driver')),
+ ('parcel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rejections', to='core.parcel', verbose_name='Parcel')),
+ ],
+ options={
+ 'verbose_name': 'Driver Rejection',
+ 'verbose_name_plural': 'Driver Rejections',
+ 'ordering': ['-created_at'],
+ },
+ ),
+ ]
diff --git a/core/migrations/0031_platformprofile_ticker_bg_color_and_more.py b/core/migrations/0031_platformprofile_ticker_bg_color_and_more.py
new file mode 100644
index 0000000..b4f7399
--- /dev/null
+++ b/core/migrations/0031_platformprofile_ticker_bg_color_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 5.2.7 on 2026-02-01 13:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0030_platformprofile_auto_ban_on_rejections_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='platformprofile',
+ name='ticker_bg_color',
+ field=models.CharField(default='#FFFFFF', help_text='Background color for the live activity ticker (e.g. #FFFFFF or white).', max_length=20, verbose_name='Ticker Background Color'),
+ ),
+ migrations.AddField(
+ model_name='platformprofile',
+ name='ticker_limit',
+ field=models.PositiveIntegerField(default=10, help_text='Number of recent shipments to show in the live activity ticker.', verbose_name='Ticker Shipment Limit'),
+ ),
+ migrations.AddField(
+ model_name='platformprofile',
+ name='ticker_text_color',
+ field=models.CharField(default='#1A1A1D', help_text='Text color for the live activity ticker.', max_length=20, verbose_name='Ticker Text Color'),
+ ),
+ ]
diff --git a/core/migrations/__pycache__/0029_driverreport.cpython-311.pyc b/core/migrations/__pycache__/0029_driverreport.cpython-311.pyc
new file mode 100644
index 0000000..e3b8ba7
Binary files /dev/null and b/core/migrations/__pycache__/0029_driverreport.cpython-311.pyc differ
diff --git a/core/migrations/__pycache__/0030_platformprofile_auto_ban_on_rejections_and_more.cpython-311.pyc b/core/migrations/__pycache__/0030_platformprofile_auto_ban_on_rejections_and_more.cpython-311.pyc
new file mode 100644
index 0000000..829fa42
Binary files /dev/null and b/core/migrations/__pycache__/0030_platformprofile_auto_ban_on_rejections_and_more.cpython-311.pyc differ
diff --git a/core/migrations/__pycache__/0031_platformprofile_ticker_bg_color_and_more.cpython-311.pyc b/core/migrations/__pycache__/0031_platformprofile_ticker_bg_color_and_more.cpython-311.pyc
new file mode 100644
index 0000000..949c5ef
Binary files /dev/null and b/core/migrations/__pycache__/0031_platformprofile_ticker_bg_color_and_more.cpython-311.pyc differ
diff --git a/core/models.py b/core/models.py
index bbb6361..baee9fb 100644
--- a/core/models.py
+++ b/core/models.py
@@ -98,6 +98,10 @@ class Profile(models.Model):
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)."))
+
+ # Ban Status
+ is_banned = models.BooleanField(_('Banned'), default=False)
+ ban_reason = models.TextField(_('Ban Reason'), blank=True)
def __str__(self):
return f"{self.user.username} - {self.get_role_display()}"
@@ -183,6 +187,14 @@ class PlatformProfile(models.Model):
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."))
+ # Driver Rejection / Auto-Ban
+ auto_ban_on_rejections = models.BooleanField(_("Enable Auto-Ban on Rejections"), default=False, help_text=_("Automatically ban drivers who exceed a certain number of rejections."))
+ rejection_limit = models.PositiveIntegerField(_("Rejection Limit"), default=5, help_text=_("Number of rejections allowed before auto-ban."))
+ # Live Activity Ticker
+ ticker_limit = models.PositiveIntegerField(_("Ticker Shipment Limit"), default=10, help_text=_("Number of recent shipments to show in the live activity ticker."))
+ ticker_bg_color = models.CharField(_("Ticker Background Color"), max_length=20, default="#FFFFFF", help_text=_("Background color for the live activity ticker (e.g. #FFFFFF or white)."))
+ ticker_text_color = models.CharField(_("Ticker Text Color"), max_length=20, default="#1A1A1D", help_text=_("Text color for the live activity ticker."))
+
@property
def maintenance_message(self):
if get_language() == "ar":
@@ -442,3 +454,55 @@ class NotificationTemplate(models.Model):
class Meta:
verbose_name = _('Notification Template')
verbose_name_plural = _('Notification Templates')
+
+class DriverReport(models.Model):
+ STATUS_CHOICES = (
+ ('pending', _('Pending Investigation')),
+ ('investigating', _('Investigating')),
+ ('resolved', _('Resolved')),
+ ('dismissed', _('Dismissed')),
+ )
+
+ REASON_CHOICES = (
+ ('unprofessional', _('Unprofessional Behavior')),
+ ('reckless_driving', _('Reckless Driving')),
+ ('delayed_delivery', _('Significant Delay')),
+ ('item_damaged', _('Item Damaged')),
+ ('item_missing', _('Item Missing')),
+ ('other', _('Other')),
+ )
+
+ reporter = models.ForeignKey(User, on_delete=models.CASCADE, related_name='filed_reports', verbose_name=_('Reporter'))
+ driver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reports_received', verbose_name=_('Driver'))
+ parcel = models.ForeignKey('Parcel', on_delete=models.SET_NULL, null=True, blank=True, related_name='reports', verbose_name=_('Related Parcel'))
+
+ reason = models.CharField(_('Reason'), max_length=50, choices=REASON_CHOICES)
+ description = models.TextField(_('Detailed Description'))
+
+ status = models.CharField(_('Status'), max_length=20, choices=STATUS_CHOICES, default='pending')
+ admin_note = models.TextField(_('Admin Internal Note'), blank=True)
+
+ created_at = models.DateTimeField(_('Created At'), auto_now_add=True)
+ updated_at = models.DateTimeField(_('Updated At'), auto_now=True)
+
+ def __str__(self):
+ return f"Report against {self.driver.username} by {self.reporter.username} - {self.get_status_display()}"
+
+ class Meta:
+ verbose_name = _('Driver Report')
+ verbose_name_plural = _('Driver Reports')
+ ordering = ['-created_at']
+
+class DriverRejection(models.Model):
+ driver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='rejections', verbose_name=_('Driver'))
+ parcel = models.ForeignKey(Parcel, on_delete=models.CASCADE, related_name='rejections', verbose_name=_('Parcel'))
+ reason = models.TextField(_('Reason for Rejection'))
+ created_at = models.DateTimeField(_('Created At'), auto_now_add=True)
+
+ def __str__(self):
+ return f"Rejection by {self.driver.username} for {self.parcel.tracking_number}"
+
+ class Meta:
+ verbose_name = _('Driver Rejection')
+ verbose_name_plural = _('Driver Rejections')
+ ordering = ['-created_at']
diff --git a/core/templates/base.html b/core/templates/base.html
index 3b9bef3..5888af0 100644
--- a/core/templates/base.html
+++ b/core/templates/base.html
@@ -35,6 +35,7 @@
+
{% if LANGUAGE_BIDI %}