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 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)
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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_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
|
||||||
@ -523,4 +528,35 @@ class DriverRejection(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
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']
|
||||||
|
|||||||
@ -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,10 +39,58 @@
|
|||||||
<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">
|
||||||
@ -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">«</span>
|
<span aria-hidden="true">«</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">»</span>
|
<span aria-hidden="true">»</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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
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