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 %}