Autosave: 20260202-033511

This commit is contained in:
Flatlogic Bot 2026-02-02 03:35:13 +00:00
parent 9f0927a406
commit 8ac308c73f
17 changed files with 2208 additions and 643 deletions

View File

@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from .models import Profile, Parcel, Country, Governate, City, PlatformProfile, Testimonial, DriverRating, NotificationTemplate, PricingRule, DriverReport, DriverRejection, ParcelType from .models import Profile, Parcel, Country, Governate, City, PlatformProfile, Testimonial, DriverRating, NotificationTemplate, PricingRule, DriverReport, DriverRejection, ParcelType, DriverWarning
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.urls import path, reverse from django.urls import path, reverse
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
@ -18,19 +18,23 @@ from django.template.loader import render_to_string
import weasyprint import weasyprint
from django.db.models import Sum from django.db.models import Sum
class DriverWarningInline(admin.TabularInline):
model = DriverWarning
extra = 1
class ProfileInline(admin.StackedInline): class ProfileInline(admin.StackedInline):
model = Profile model = Profile
can_delete = False can_delete = False
verbose_name_plural = _('Profiles') verbose_name_plural = _('Profiles')
fieldsets = ( fieldsets = (
(None, {'fields': ('role', 'is_approved', 'is_banned', 'ban_reason', 'phone_number', 'profile_picture', 'address')}), (None, {'fields': ('role', 'is_approved', 'is_banned', 'ban_reason', 'phone_number', 'profile_picture', 'address', 'language')}),
(_('Driver Assessment'), {'fields': ('driver_grade', 'is_recommended')}), (_('Driver Assessment'), {'fields': ('driver_grade', 'is_recommended')}),
(_('Driver Info'), {'fields': ('license_front_image', 'license_back_image', 'car_plate_number', 'bank_account_number'), 'classes': ('collapse',)}), (_('Driver Info'), {'fields': ('license_front_image', 'license_back_image', 'car_plate_number', 'bank_account_number'), 'classes': ('collapse',)}),
(_('Location'), {'fields': ('country', 'governate', 'city'), 'classes': ('collapse',)}), (_('Location'), {'fields': ('country', 'governate', 'city'), 'classes': ('collapse',)}),
) )
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
inlines = (ProfileInline,) inlines = (ProfileInline, DriverWarningInline)
list_display = ('username', 'email', 'get_role', 'get_driver_grade', 'get_approval_status', 'get_ban_status', 'is_active', 'is_staff', 'send_whatsapp_link') 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') list_filter = ('is_active', 'is_staff', 'profile__role', 'profile__is_approved', 'profile__is_banned', 'profile__driver_grade')
@ -208,9 +212,12 @@ class PlatformProfileAdmin(admin.ModelAdmin):
'fields': ('accepting_shipments', 'maintenance_message_en', 'maintenance_message_ar'), '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.') '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'), { (_('Driver Warning & Rejection / Auto-Ban'), {
'fields': ('auto_ban_on_rejections', 'rejection_limit'), 'fields': (
'description': _('Configure automatic banning for drivers who reject too many shipments.') 'enable_auto_ban_on_warnings', 'max_warnings_before_ban',
'auto_ban_on_rejections', 'rejection_limit'
),
'description': _('Configure automatic banning for drivers who exceed warning or rejection limits.')
}), }),
(_('Testing / Development'), { (_('Testing / Development'), {
'fields': ('auto_mark_paid',), 'fields': ('auto_mark_paid',),
@ -403,3 +410,10 @@ class NotificationTemplateAdmin(admin.ModelAdmin):
admin.site.register(NotificationTemplate, NotificationTemplateAdmin) admin.site.register(NotificationTemplate, NotificationTemplateAdmin)
admin.site.register(ParcelType) admin.site.register(ParcelType)
class DriverWarningAdmin(admin.ModelAdmin):
list_display = ('driver', 'reason', 'created_at')
list_filter = ('created_at',)
search_fields = ('driver__username', 'reason')
admin.site.register(DriverWarning, DriverWarningAdmin)

View File

@ -115,6 +115,7 @@ class UserRegistrationForm(forms.ModelForm):
profile.car_plate_number = self.cleaned_data['car_plate_number'] profile.car_plate_number = self.cleaned_data['car_plate_number']
if 'bank_account_number' in self.cleaned_data: if 'bank_account_number' in self.cleaned_data:
profile.bank_account_number = self.cleaned_data['bank_account_number'] profile.bank_account_number = self.cleaned_data['bank_account_number']
profile.language = get_language()
profile.save() profile.save()
return user return user
@ -158,16 +159,18 @@ class UserProfileForm(forms.ModelForm):
class Meta: class Meta:
model = Profile model = Profile
fields = ['profile_picture', 'phone_number', 'address', 'country', 'governate', 'city', 'bank_account_number'] fields = ['profile_picture', 'phone_number', 'address', 'country', 'governate', 'city', 'bank_account_number', 'language']
widgets = { widgets = {
'country': forms.Select(attrs={'class': 'form-control'}), 'country': forms.Select(attrs={'class': 'form-control'}),
'governate': forms.Select(attrs={'class': 'form-control'}), 'governate': forms.Select(attrs={'class': 'form-control'}),
'city': forms.Select(attrs={'class': 'form-control'}), 'city': forms.Select(attrs={'class': 'form-control'}),
'language': forms.Select(attrs={'class': 'form-control'}),
} }
labels = { labels = {
'country': _('Country'), 'country': _('Country'),
'governate': _('Governate'), 'governate': _('Governate'),
'city': _('City'), 'city': _('City'),
'language': _('Language'),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -0,0 +1,40 @@
# Generated by Django 5.2.7 on 2026-02-02 02:52
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0032_parceltype_parcel_parcel_type'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='platformprofile',
name='enable_auto_ban_on_warnings',
field=models.BooleanField(default=False, help_text='Automatically ban drivers who exceed a certain number of warnings.', verbose_name='Enable Auto-Ban on Warnings'),
),
migrations.AddField(
model_name='platformprofile',
name='max_warnings_before_ban',
field=models.PositiveIntegerField(default=3, help_text='Number of warnings allowed before auto-ban.', verbose_name='Max Warnings Before Ban'),
),
migrations.CreateModel(
name='DriverWarning',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reason', models.TextField(verbose_name='Reason for Warning')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('driver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='warnings', to=settings.AUTH_USER_MODEL, verbose_name='Driver')),
],
options={
'verbose_name': 'Driver Warning',
'verbose_name_plural': 'Driver Warnings',
'ordering': ['-created_at'],
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-02-02 03:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0033_platformprofile_enable_auto_ban_on_warnings_and_more'),
]
operations = [
migrations.AddField(
model_name='profile',
name='language',
field=models.CharField(choices=[('en', 'English'), ('ar', 'Arabic')], default='ar', max_length=10, verbose_name='Language'),
),
]

View File

@ -115,6 +115,7 @@ class Profile(models.Model):
is_recommended = models.BooleanField(_("Recommended by Shippers"), default=False) 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).")) is_approved = models.BooleanField(_('Approved'), default=False, help_text=_("Designates whether this user is approved to use the platform (mainly for drivers)."))
language = models.CharField(_("Language"), max_length=10, choices=[("en", _("English")), ("ar", _("Arabic"))], default="ar")
# Ban Status # Ban Status
is_banned = models.BooleanField(_('Banned'), default=False) is_banned = models.BooleanField(_('Banned'), default=False)
@ -205,6 +206,10 @@ class PlatformProfile(models.Model):
maintenance_message_ar = models.TextField(_("Maintenance Message (Arabic)"), 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 # Driver Rejection / Auto-Ban
# Driver Warning / Auto-Ban
enable_auto_ban_on_warnings = models.BooleanField(_("Enable Auto-Ban on Warnings"), default=False, help_text=_("Automatically ban drivers who exceed a certain number of warnings."))
max_warnings_before_ban = models.PositiveIntegerField(_("Max Warnings Before Ban"), default=3, help_text=_("Number of warnings allowed before 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.")) 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.")) rejection_limit = models.PositiveIntegerField(_("Rejection Limit"), default=5, help_text=_("Number of rejections allowed before auto-ban."))
# Live Activity Ticker # Live Activity Ticker
@ -524,3 +529,34 @@ class DriverRejection(models.Model):
verbose_name = _('Driver Rejection') verbose_name = _('Driver Rejection')
verbose_name_plural = _('Driver Rejections') verbose_name_plural = _('Driver Rejections')
ordering = ['-created_at'] ordering = ['-created_at']
class DriverWarning(models.Model):
driver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='warnings', verbose_name=_('Driver'))
reason = models.TextField(_('Reason for Warning'))
created_at = models.DateTimeField(_('Created At'), auto_now_add=True)
def __str__(self):
return f"Warning for {self.driver.username} on {self.created_at.strftime('%Y-%m-%d')}"
def save(self, *args, **kwargs):
is_new = self.pk is None
super().save(*args, **kwargs)
if is_new:
# Check for auto-ban
try:
platform_profile = PlatformProfile.objects.first()
if platform_profile and platform_profile.enable_auto_ban_on_warnings:
warning_count = DriverWarning.objects.filter(driver=self.driver).count()
if warning_count >= platform_profile.max_warnings_before_ban:
profile = self.driver.profile
profile.is_banned = True
profile.ban_reason = _("Automatically banned due to exceeding warning limit (%(limit)d warnings).") % {
'limit': platform_profile.max_warnings_before_ban
}
profile.save()
except Exception:
pass
class Meta:
verbose_name = _('Driver Warning')
verbose_name_plural = _('Driver Warnings')
ordering = ['-created_at']

View File

@ -5,9 +5,33 @@
<div class="container py-5"> <div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="mb-0">{% trans "Driver Dashboard" %}</h1> <h1 class="mb-0">{% trans "Driver Dashboard" %}</h1>
<a href="{% url 'scan_qr' %}" class="btn btn-primary rounded-pill px-4"> <div class="d-flex gap-2">
<i class="bi bi-qr-code-scan me-2"></i> {% trans "Scan Parcel" %} <a href="{% url 'scan_qr' %}" class="btn btn-primary rounded-pill px-4">
</a> <i class="bi bi-qr-code-scan me-2"></i> {% trans "Scan Parcel" %}
</a>
</div>
</div>
<!-- Search Section -->
<div class="row mb-4">
<div class="col-md-12">
<form method="GET" class="position-relative">
<div class="input-group shadow-sm" style="border-radius: 12px; overflow: hidden;">
<span class="input-group-text bg-white border-end-0">
<i class="bi bi-search text-muted"></i>
</span>
<input type="text" name="q" class="form-control border-start-0 ps-0 py-3"
placeholder="{% trans 'Search by Parcel Number (e.g. MSX12345)...' %}"
value="{{ search_query }}">
<button class="btn btn-masarx-primary px-4" type="submit">{% trans "Search" %}</button>
{% if search_query %}
<a href="{% url 'dashboard' %}" class="btn btn-outline-secondary d-flex align-items-center">
<i class="bi bi-x-lg"></i>
</a>
{% endif %}
</div>
</form>
</div>
</div> </div>
{% if not is_approved %} {% if not is_approved %}
@ -15,11 +39,59 @@
<i class="bi bi-hourglass-split fs-3 me-3 text-warning"></i> <i class="bi bi-hourglass-split fs-3 me-3 text-warning"></i>
<div> <div>
<h5 class="alert-heading mb-1">{% trans "Account Pending Approval" %}</h5> <h5 class="alert-heading mb-1">{% trans "Account Pending Approval" %}</h5>
<p class="mb-0 text-muted">{% trans "Your driver account is currently under review by our administrators. You will be notified once your documents are verified and your account is approved to accept shipments." %}</p> <p class="mb-0 text-muted">{% trans "We are currently revising your documents. Please be patient. We will inform once we finish." %}</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
<!-- Statistics Section -->
<div class="row g-4 mb-5">
<div class="col-6 col-md-4 col-lg">
<div class="card border-0 shadow-sm text-center p-3 h-100" style="border-radius: 15px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);">
<div class="text-primary fs-3 mb-1"><i class="bi bi-box-seam"></i></div>
<div class="fw-bold fs-4">{{ stats.accepted_count }}</div>
<div class="text-muted small">{% trans "Accepted Parcels" %}</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg">
<div class="card border-0 shadow-sm text-center p-3 h-100" style="border-radius: 15px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);">
<div class="text-danger fs-3 mb-1"><i class="bi bi-x-circle"></i></div>
<div class="fw-bold fs-4">{{ stats.rejected_count }}</div>
<div class="text-muted small">{% trans "Rejected Parcels" %}</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg">
<div class="card border-0 shadow-sm text-center p-3 h-100" style="border-radius: 15px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);">
<div class="text-warning fs-3 mb-1">
{% if stats.average_rating %}
{{ stats.average_rating|floatformat:1 }} <i class="bi bi-star-fill"></i>
{% else %}
N/A
{% endif %}
</div>
<div class="fw-bold fs-4">{{ stats.rating_count }}</div>
<div class="text-muted small">{% trans "Shippers Rating" %}</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg">
<div class="card border-0 shadow-sm text-center p-3 h-100" style="border-radius: 15px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);">
<div class="text-info fs-3 mb-1"><i class="bi bi-shield-check"></i></div>
<div class="fw-bold fs-5">{{ stats.driver_grade }}</div>
<div class="text-muted small">{% trans "Admin Rate" %}</div>
{% if stats.is_recommended %}
<span class="badge bg-success rounded-pill mt-2" style="font-size: 0.6rem;">{% trans "Recommended" %}</span>
{% endif %}
</div>
</div>
<div class="col-6 col-md-4 col-lg">
<div class="card border-0 shadow-sm text-center p-3 h-100" style="border-radius: 15px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);">
<div class="text-danger fs-3 mb-1"><i class="bi bi-exclamation-triangle-fill"></i></div>
<div class="fw-bold fs-4">{{ stats.warning_count }} / {{ stats.max_warnings }}</div>
<div class="text-muted small">{% trans "Warnings" %}</div>
</div>
</div>
</div>
<ul class="nav nav-pills mb-4" id="pills-tab" role="tablist"> <ul class="nav nav-pills mb-4" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="pills-available-tab" data-bs-toggle="pill" data-bs-target="#pills-available" type="button" role="tab">{% trans "Available Shipments" %}</button> <button class="nav-link active" id="pills-available-tab" data-bs-toggle="pill" data-bs-target="#pills-available" type="button" role="tab">{% trans "Available Shipments" %}</button>
@ -176,7 +248,7 @@
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
{% if available_parcels.has_previous %} {% if available_parcels.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ available_parcels.previous_page_number }}" aria-label="Previous"> <a class="page-link" href="?page={{ available_parcels.previous_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span> <span aria-hidden="true">&laquo;</span>
</a> </a>
</li> </li>
@ -190,13 +262,13 @@
{% if available_parcels.number == i %} {% if available_parcels.number == i %}
<li class="page-item active"><span class="page-link">{{ i }}</span></li> <li class="page-item active"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="?page={{ i }}{% if search_query %}&q={{ search_query }}{% endif %}">{{ i }}</a></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if available_parcels.has_next %} {% if available_parcels.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ available_parcels.next_page_number }}" aria-label="Next"> <a class="page-link" href="?page={{ available_parcels.next_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}" aria-label="Next">
<span aria-hidden="true">&raquo;</span> <span aria-hidden="true">&raquo;</span>
</a> </a>
</li> </li>
@ -210,7 +282,12 @@
{% endif %} {% endif %}
{% else %} {% else %}
<p class="text-center py-5">{% trans "No shipments available at the moment." %}</p> <div class="text-center py-5">
<p class="text-muted">{% trans "No shipments available matching your criteria." %}</p>
{% if search_query %}
<a href="{% url 'dashboard' %}" class="btn btn-link">{% trans "Clear Search" %}</a>
{% endif %}
</div>
{% endif %} {% endif %}
{% else %} {% else %}
@ -267,7 +344,12 @@
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<p class="text-center py-5">{% trans "You haven't accepted any shipments yet." %}</p> <div class="text-center py-5">
<p class="text-muted">{% trans "No active deliveries matching your criteria." %}</p>
{% if search_query %}
<a href="{% url 'dashboard' %}" class="btn btn-link">{% trans "Clear Search" %}</a>
{% endif %}
</div>
{% endif %} {% endif %}
</div> </div>
@ -312,7 +394,10 @@
</div> </div>
{% else %} {% else %}
<div class="text-center py-5"> <div class="text-center py-5">
<p class="lead">{% trans "No completed deliveries yet." %}</p> <p class="lead">{% trans "No matching transaction history." %}</p>
{% if search_query %}
<a href="{% url 'dashboard' %}" class="btn btn-link">{% trans "Clear Search" %}</a>
{% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -358,7 +443,10 @@
</div> </div>
{% else %} {% else %}
<div class="text-center py-5"> <div class="text-center py-5">
<p class="lead">{% trans "No cancelled shipments." %}</p> <p class="lead">{% trans "No matching cancelled shipments." %}</p>
{% if search_query %}
<a href="{% url 'dashboard' %}" class="btn btn-link">{% trans "Clear Search" %}</a>
{% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -80,11 +80,13 @@
<label class="text-muted small text-uppercase">{% trans "Address" %}</label> <label class="text-muted small text-uppercase">{% trans "Address" %}</label>
<p class="fw-semibold">{{ profile.address|default:"-" }}</p> <p class="fw-semibold">{{ profile.address|default:"-" }}</p>
</div> </div>
</div> <div class="col-md-6">
<label class="text-muted small text-uppercase">{% trans "Preferred Language" %}</label>
<!-- Rating Section (Drivers Only) --> <p class="fw-semibold">{{ profile.get_language_display }}</p>
</div>
{% if profile.role == 'car_owner' %} {% if profile.role == 'car_owner' %}
<hr class="my-5"> <hr class="my-5">
{% if profile.role == 'car_owner' %}
<div class="mb-4"> <div class="mb-4">
<h4 class="fw-bold">{% trans "Driver Rating" %}</h4> <h4 class="fw-bold">{% trans "Driver Rating" %}</h4>
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">

View File

@ -8,6 +8,7 @@ from .models import Parcel, Profile, Country, Governate, City, OTPVerification,
from .forms import UserRegistrationForm, ParcelForm, ContactForm, UserProfileForm, DriverRatingForm, ShipperRegistrationForm, DriverRegistrationForm, DriverReportForm from .forms import UserRegistrationForm, ParcelForm, ContactForm, UserProfileForm, DriverRatingForm, ShipperRegistrationForm, DriverRegistrationForm, DriverReportForm
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.translation import get_language from django.utils.translation import get_language
from django.contrib import messages from django.contrib import messages
from django.http import JsonResponse, HttpResponse from django.http import JsonResponse, HttpResponse
from django.urls import reverse from django.urls import reverse
@ -205,6 +206,9 @@ def verify_registration(request):
# Login # Login
login(request, user) login(request, user)
# Set user's language preference
if hasattr(user, 'profile') and user.profile.language:
request.session["_language"] = user.profile.language
messages.success(request, _("Account verified successfully!")) messages.success(request, _("Account verified successfully!"))
return redirect('dashboard') return redirect('dashboard')
@ -257,6 +261,9 @@ def dashboard(request):
}) })
else: else:
# Car Owner view # Car Owner view
# Search logic
search_query = request.GET.get('q', '').strip()
platform_profile = PlatformProfile.objects.first() platform_profile = PlatformProfile.objects.first()
payments_enabled = platform_profile.enable_payment if platform_profile else True payments_enabled = platform_profile.enable_payment if platform_profile else True
@ -270,7 +277,7 @@ def dashboard(request):
# Check Approval Status # Check Approval Status
if not profile.is_approved: if not profile.is_approved:
messages.warning(request, _("Your account is pending approval. You cannot accept shipments yet.")) messages.warning(request, _("We are currently revising your documents. Please be patient. We will inform once we finish."))
# Empty list if not approved # Empty list if not approved
available_parcels_list = Parcel.objects.none() available_parcels_list = Parcel.objects.none()
@ -285,23 +292,49 @@ def dashboard(request):
except EmptyPage: except EmptyPage:
available_parcels = paginator.page(paginator.num_pages) available_parcels = paginator.page(paginator.num_pages)
# Apply search filter if query exists
if search_query:
available_parcels_list = available_parcels_list.filter(tracking_number__icontains=search_query)
# We need to re-paginate if we filtered
paginator = Paginator(available_parcels_list, 9)
try:
available_parcels = paginator.page(page)
except (PageNotAnInteger, EmptyPage):
available_parcels = paginator.page(1)
# Active: Picked up or In Transit # Active: Picked up or In Transit
my_parcels = Parcel.objects.filter(carrier=request.user).exclude(status__in=['delivered', 'cancelled']).order_by('-created_at') my_parcels = Parcel.objects.filter(carrier=request.user).exclude(status__in=['delivered', 'cancelled']).order_by('-created_at')
if search_query: my_parcels = my_parcels.filter(tracking_number__icontains=search_query)
# History: Delivered # History: Delivered
completed_parcels = Parcel.objects.filter(carrier=request.user, status='delivered').order_by('-created_at') completed_parcels = Parcel.objects.filter(carrier=request.user, status='delivered').order_by('-created_at')
if search_query: completed_parcels = completed_parcels.filter(tracking_number__icontains=search_query)
# Cancelled # Cancelled
cancelled_parcels = Parcel.objects.filter(carrier=request.user, status='cancelled').order_by('-created_at') cancelled_parcels = Parcel.objects.filter(carrier=request.user, status='cancelled').order_by('-created_at')
if search_query: cancelled_parcels = cancelled_parcels.filter(tracking_number__icontains=search_query)
# Statistics for Driver Dashboard
stats = {
'accepted_count': Parcel.objects.filter(carrier=request.user).exclude(status='cancelled').count(),
'rejected_count': DriverRejection.objects.filter(driver=request.user).count(),
'average_rating': profile.get_average_rating(),
'rating_count': profile.get_rating_count(),
'driver_grade': profile.get_driver_grade_display(),
'is_recommended': profile.is_recommended,
'warning_count': request.user.warnings.count(),
'max_warnings': platform_profile.max_warnings_before_ban if platform_profile else 3,
}
return render(request, 'core/driver_dashboard.html', { return render(request, 'core/driver_dashboard.html', {
'available_parcels': available_parcels, 'available_parcels': available_parcels,
'my_parcels': my_parcels, 'my_parcels': my_parcels,
'completed_parcels': completed_parcels, 'completed_parcels': completed_parcels,
'cancelled_parcels': cancelled_parcels, 'cancelled_parcels': cancelled_parcels,
'is_approved': profile.is_approved # Pass to template 'is_approved': profile.is_approved,
'stats': stats,
'search_query': search_query,
}) })
@login_required @login_required
def shipment_request(request): def shipment_request(request):
from .models import PlatformProfile from .models import PlatformProfile
@ -369,7 +402,7 @@ def accept_parcel(request, parcel_id):
return redirect('dashboard') return redirect('dashboard')
if not profile.is_approved: if not profile.is_approved:
messages.error(request, _("Your account is pending approval. You cannot accept shipments yet.")) messages.error(request, _("We are currently revising your documents. Please be patient. We will inform once we finish."))
return redirect('dashboard') return redirect('dashboard')
platform_profile = PlatformProfile.objects.first() platform_profile = PlatformProfile.objects.first()
@ -574,6 +607,7 @@ def edit_profile(request):
'country_id': data['country'].id if data['country'] else None, 'country_id': data['country'].id if data['country'] else None,
'governate_id': data['governate'].id if data['governate'] else None, 'governate_id': data['governate'].id if data['governate'] else None,
'city_id': data['city'].id if data['city'] else None, 'city_id': data['city'].id if data['city'] else None,
'language': data['language'],
} }
request.session['pending_profile_update'] = safe_data request.session['pending_profile_update'] = safe_data
@ -641,6 +675,9 @@ def verify_otp_view(request):
profile.governate_id = data['governate_id'] profile.governate_id = data['governate_id']
if data.get('city_id'): if data.get('city_id'):
profile.city_id = data['city_id'] profile.city_id = data['city_id']
if data.get('language'):
profile.language = data['language']
request.session["_language"] = data['language']
profile.save() profile.save()
# Cleanup # Cleanup
@ -784,6 +821,9 @@ def verify_login_otp(request):
# Login # Login
login(request, user) login(request, user)
# Set user's language preference
if hasattr(user, 'profile') and user.profile.language:
request.session["_language"] = user.profile.language
return JsonResponse({'success': True, 'redirect_url': reverse('dashboard')}) return JsonResponse({'success': True, 'redirect_url': reverse('dashboard')})
else: else:
@ -1150,12 +1190,20 @@ def verify_2fa_otp(request):
is_verified=False is_verified=False
).latest('created_at') ).latest('created_at')
if otp_record.code == code and otp_record.is_valid():
otp_record.is_verified = True
otp_record.save()
# ACTUAL LOGIN HAPPENS HERE
if otp_record.code == code and otp_record.is_valid(): if otp_record.code == code and otp_record.is_valid():
otp_record.is_verified = True otp_record.is_verified = True
otp_record.save() otp_record.save()
# ACTUAL LOGIN HAPPENS HERE # ACTUAL LOGIN HAPPENS HERE
login(request, user) login(request, user)
# Set user's language preference
if hasattr(user, 'profile') and user.profile.language:
request.session["_language"] = user.profile.language
# Clean up session # Clean up session
if 'pre_2fa_user_id' in request.session: if 'pre_2fa_user_id' in request.session:
@ -1168,7 +1216,7 @@ def verify_2fa_otp(request):
messages.error(request, _("Invalid or expired OTP.")) messages.error(request, _("Invalid or expired OTP."))
except OTPVerification.DoesNotExist: except OTPVerification.DoesNotExist:
messages.error(request, _("No valid OTP found. Please request a new one.")) messages.error(request, _("No valid OTP found. Please request a new one."))
return render(request, 'core/verify_2fa_otp.html') return render(request, 'core/verify_2fa_otp.html')

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB