Autosave: 20260202-033511
This commit is contained in:
parent
9f0927a406
commit
8ac308c73f
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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, 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.urls import path, reverse
|
||||
from django.shortcuts import render, redirect
|
||||
@ -18,19 +18,23 @@ from django.template.loader import render_to_string
|
||||
import weasyprint
|
||||
from django.db.models import Sum
|
||||
|
||||
class DriverWarningInline(admin.TabularInline):
|
||||
model = DriverWarning
|
||||
extra = 1
|
||||
|
||||
class ProfileInline(admin.StackedInline):
|
||||
model = Profile
|
||||
can_delete = False
|
||||
verbose_name_plural = _('Profiles')
|
||||
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 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,)
|
||||
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_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'),
|
||||
'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.')
|
||||
(_('Driver Warning & Rejection / Auto-Ban'), {
|
||||
'fields': (
|
||||
'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'), {
|
||||
'fields': ('auto_mark_paid',),
|
||||
@ -403,3 +410,10 @@ class NotificationTemplateAdmin(admin.ModelAdmin):
|
||||
|
||||
admin.site.register(NotificationTemplate, NotificationTemplateAdmin)
|
||||
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)
|
||||
|
||||
@ -115,6 +115,7 @@ class UserRegistrationForm(forms.ModelForm):
|
||||
profile.car_plate_number = self.cleaned_data['car_plate_number']
|
||||
if 'bank_account_number' in self.cleaned_data:
|
||||
profile.bank_account_number = self.cleaned_data['bank_account_number']
|
||||
profile.language = get_language()
|
||||
|
||||
profile.save()
|
||||
return user
|
||||
@ -158,16 +159,18 @@ class UserProfileForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
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 = {
|
||||
'country': forms.Select(attrs={'class': 'form-control'}),
|
||||
'governate': forms.Select(attrs={'class': 'form-control'}),
|
||||
'city': forms.Select(attrs={'class': 'form-control'}),
|
||||
'language': forms.Select(attrs={'class': 'form-control'}),
|
||||
}
|
||||
labels = {
|
||||
'country': _('Country'),
|
||||
'governate': _('Governate'),
|
||||
'city': _('City'),
|
||||
'language': _('Language'),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
18
core/migrations/0034_profile_language.py
Normal file
18
core/migrations/0034_profile_language.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@ -115,6 +115,7 @@ 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)."))
|
||||
language = models.CharField(_("Language"), max_length=10, choices=[("en", _("English")), ("ar", _("Arabic"))], default="ar")
|
||||
|
||||
# Ban Status
|
||||
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."))
|
||||
|
||||
# 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."))
|
||||
rejection_limit = models.PositiveIntegerField(_("Rejection Limit"), default=5, help_text=_("Number of rejections allowed before auto-ban."))
|
||||
# Live Activity Ticker
|
||||
@ -523,4 +528,35 @@ class DriverRejection(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _('Driver Rejection')
|
||||
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']
|
||||
|
||||
@ -5,9 +5,33 @@
|
||||
<div class="container py-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="mb-0">{% trans "Driver Dashboard" %}</h1>
|
||||
<a href="{% url 'scan_qr' %}" class="btn btn-primary rounded-pill px-4">
|
||||
<i class="bi bi-qr-code-scan me-2"></i> {% trans "Scan Parcel" %}
|
||||
</a>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'scan_qr' %}" class="btn btn-primary rounded-pill px-4">
|
||||
<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>
|
||||
|
||||
{% if not is_approved %}
|
||||
@ -15,10 +39,58 @@
|
||||
<i class="bi bi-hourglass-split fs-3 me-3 text-warning"></i>
|
||||
<div>
|
||||
<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>
|
||||
{% 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">
|
||||
<li class="nav-item" role="presentation">
|
||||
@ -176,7 +248,7 @@
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if available_parcels.has_previous %}
|
||||
<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">«</span>
|
||||
</a>
|
||||
</li>
|
||||
@ -190,13 +262,13 @@
|
||||
{% if available_parcels.number == i %}
|
||||
<li class="page-item active"><span class="page-link">{{ i }}</span></li>
|
||||
{% 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 %}
|
||||
{% endfor %}
|
||||
|
||||
{% if available_parcels.has_next %}
|
||||
<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">»</span>
|
||||
</a>
|
||||
</li>
|
||||
@ -210,7 +282,12 @@
|
||||
{% endif %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% else %}
|
||||
@ -267,7 +344,12 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
||||
@ -312,7 +394,10 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -358,7 +443,10 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -80,11 +80,13 @@
|
||||
<label class="text-muted small text-uppercase">{% trans "Address" %}</label>
|
||||
<p class="fw-semibold">{{ profile.address|default:"-" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating Section (Drivers Only) -->
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small text-uppercase">{% trans "Preferred Language" %}</label>
|
||||
<p class="fw-semibold">{{ profile.get_language_display }}</p>
|
||||
</div>
|
||||
{% if profile.role == 'car_owner' %}
|
||||
<hr class="my-5">
|
||||
{% if profile.role == 'car_owner' %}
|
||||
<div class="mb-4">
|
||||
<h4 class="fw-bold">{% trans "Driver Rating" %}</h4>
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
|
||||
@ -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 django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import get_language
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse, HttpResponse
|
||||
from django.urls import reverse
|
||||
@ -205,6 +206,9 @@ def verify_registration(request):
|
||||
|
||||
# Login
|
||||
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!"))
|
||||
return redirect('dashboard')
|
||||
@ -257,6 +261,9 @@ def dashboard(request):
|
||||
})
|
||||
else:
|
||||
# Car Owner view
|
||||
# Search logic
|
||||
search_query = request.GET.get('q', '').strip()
|
||||
|
||||
platform_profile = PlatformProfile.objects.first()
|
||||
payments_enabled = platform_profile.enable_payment if platform_profile else True
|
||||
|
||||
@ -270,7 +277,7 @@ def dashboard(request):
|
||||
|
||||
# Check Approval Status
|
||||
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
|
||||
available_parcels_list = Parcel.objects.none()
|
||||
|
||||
@ -285,23 +292,49 @@ def dashboard(request):
|
||||
except EmptyPage:
|
||||
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
|
||||
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
|
||||
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_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', {
|
||||
'available_parcels': available_parcels,
|
||||
'my_parcels': my_parcels,
|
||||
'completed_parcels': completed_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
|
||||
def shipment_request(request):
|
||||
from .models import PlatformProfile
|
||||
@ -369,7 +402,7 @@ def accept_parcel(request, parcel_id):
|
||||
return redirect('dashboard')
|
||||
|
||||
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')
|
||||
|
||||
platform_profile = PlatformProfile.objects.first()
|
||||
@ -574,6 +607,7 @@ def edit_profile(request):
|
||||
'country_id': data['country'].id if data['country'] else None,
|
||||
'governate_id': data['governate'].id if data['governate'] else None,
|
||||
'city_id': data['city'].id if data['city'] else None,
|
||||
'language': data['language'],
|
||||
}
|
||||
request.session['pending_profile_update'] = safe_data
|
||||
|
||||
@ -641,6 +675,9 @@ def verify_otp_view(request):
|
||||
profile.governate_id = data['governate_id']
|
||||
if data.get('city_id'):
|
||||
profile.city_id = data['city_id']
|
||||
if data.get('language'):
|
||||
profile.language = data['language']
|
||||
request.session["_language"] = data['language']
|
||||
profile.save()
|
||||
|
||||
# Cleanup
|
||||
@ -784,6 +821,9 @@ def verify_login_otp(request):
|
||||
|
||||
# Login
|
||||
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')})
|
||||
else:
|
||||
@ -1150,12 +1190,20 @@ def verify_2fa_otp(request):
|
||||
is_verified=False
|
||||
).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():
|
||||
otp_record.is_verified = True
|
||||
otp_record.save()
|
||||
|
||||
# ACTUAL LOGIN HAPPENS HERE
|
||||
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
|
||||
if 'pre_2fa_user_id' in request.session:
|
||||
@ -1168,7 +1216,7 @@ def verify_2fa_otp(request):
|
||||
messages.error(request, _("Invalid or expired OTP."))
|
||||
|
||||
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')
|
||||
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
staticfiles/pasted-20260201-154837-60ba680b.png
Normal file
BIN
staticfiles/pasted-20260201-154837-60ba680b.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Loading…
x
Reference in New Issue
Block a user