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 %}
{% 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 %} +{{ profile.get_role_display }}
{{ platform_profile.maintenance_message }}
+{% 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 %}
{% trans "You have no active shipments." %}
- {% trans "Send your first shipment" %} + {% if platform_profile.accepting_shipments %}{% trans "Send your first shipment" %}{% else %}{% endif %}