adding some features

This commit is contained in:
Flatlogic Bot 2026-02-01 13:17:06 +00:00
parent 856be645ab
commit e1e791abf6
23 changed files with 282 additions and 33 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -24,19 +24,26 @@ class ProfileInline(admin.StackedInline):
verbose_name_plural = _('Profiles')
fieldsets = (
(None, {'fields': ('role', 'is_approved', 'phone_number', 'profile_picture', 'address')}),
(_('Driver Assessment'), {'fields': ('driver_grade', 'is_recommended')}),
(_('Driver Info'), {'fields': ('license_front_image', 'license_back_image', 'car_plate_number', 'bank_account_number'), 'classes': ('collapse',)}),
(_('Location'), {'fields': ('country', 'governate', 'city'), 'classes': ('collapse',)}),
)
class CustomUserAdmin(UserAdmin):
inlines = (ProfileInline,)
list_display = ('username', 'email', 'get_role', 'get_approval_status', 'is_active', 'is_staff', 'send_whatsapp_link')
list_filter = ('is_active', 'is_staff', 'profile__role', 'profile__is_approved')
list_display = ('username', 'email', 'get_role', 'get_driver_grade', 'get_approval_status', 'is_active', 'is_staff', 'send_whatsapp_link')
list_filter = ('is_active', 'is_staff', 'profile__role', 'profile__is_approved', 'profile__driver_grade')
def get_role(self, obj):
return obj.profile.get_role_display()
get_role.short_description = _('Role')
def get_driver_grade(self, obj):
if obj.profile.role == 'car_owner':
return obj.profile.get_driver_grade_display()
return "-"
get_driver_grade.short_description = _('Grade')
def get_approval_status(self, obj):
return obj.profile.is_approved
get_approval_status.short_description = _('Approved')
@ -192,6 +199,10 @@ class PlatformProfileAdmin(admin.ModelAdmin):
(_('Financial Configuration'), {
'fields': ('platform_fee_percentage', 'enable_payment')
}),
(_('Maintenance / Availability'), {
'fields': ('accepting_shipments', 'maintenance_message_en', 'maintenance_message_ar'),
'description': _('Toggle to allow or stop receiving new parcel shipments. If stopped, buttons will turn red and an alert will be shown.')
}),
(_('Testing / Development'), {
'fields': ('auto_mark_paid',),
'description': _('Enable this to automatically mark NEW parcels as "Paid" (useful for testing so drivers can see them immediately).')

View File

@ -47,6 +47,10 @@ class ParcelListCreateView(generics.ListCreateAPIView):
return Parcel.objects.none()
def perform_create(self, serializer):
from .models import PlatformProfile
platform_profile = PlatformProfile.objects.first()
if platform_profile and not platform_profile.accepting_shipments:
raise permissions.PermissionDenied(platform_profile.maintenance_message or "The platform is currently not accepting new shipments.")
# Only shippers can create
if self.request.user.profile.role != 'shipper':
raise permissions.PermissionDenied("Only shippers can create parcels.")

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2026-02-01 12:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0026_profile_bank_account_number'),
]
operations = [
migrations.AddField(
model_name='profile',
name='driver_grade',
field=models.CharField(choices=[('none', 'No Grade'), ('bronze_3', 'Bronze III'), ('bronze_2', 'Bronze II'), ('bronze_1', 'Bronze I'), ('silver', 'Silver'), ('gold', 'Gold')], default='none', max_length=20, verbose_name='Driver Grade'),
),
migrations.AddField(
model_name='profile',
name='is_recommended',
field=models.BooleanField(default=False, verbose_name='Recommended by Shippers'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-02-01 13:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0027_profile_driver_grade_profile_is_recommended'),
]
operations = [
migrations.AddField(
model_name='platformprofile',
name='accepting_shipments',
field=models.BooleanField(default=True, help_text='Toggle to allow or stop receiving new parcel shipments.', verbose_name='Accepting Shipments'),
),
migrations.AddField(
model_name='platformprofile',
name='maintenance_message_ar',
field=models.TextField(blank=True, help_text='Message to show when shipments are stopped.', verbose_name='Maintenance Message (Arabic)'),
),
migrations.AddField(
model_name='platformprofile',
name='maintenance_message_en',
field=models.TextField(blank=True, help_text='Message to show when shipments are stopped.', verbose_name='Maintenance Message (English)'),
),
]

View File

@ -65,8 +65,16 @@ class City(models.Model):
class Profile(models.Model):
ROLE_CHOICES = (
('shipper', _('Shipper')),
('car_owner', _('Car Owner')),
("shipper", _("Shipper")),
("car_owner", _("Car Owner")),
)
DRIVER_GRADE_CHOICES = (
("none", _("No Grade")),
("bronze_3", _("Bronze III")),
("bronze_2", _("Bronze II")),
("bronze_1", _("Bronze I")),
("silver", _("Silver")),
("gold", _("Gold")),
)
user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name=_('User'))
role = models.CharField(_('Role'), max_length=20, choices=ROLE_CHOICES, default='shipper')
@ -85,6 +93,10 @@ class Profile(models.Model):
city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('City'))
# Approval Status
# Driver Assessment
driver_grade = models.CharField(_("Driver Grade"), max_length=20, choices=DRIVER_GRADE_CHOICES, default="none")
is_recommended = models.BooleanField(_("Recommended by Shippers"), default=False)
is_approved = models.BooleanField(_('Approved'), default=False, help_text=_("Designates whether this user is approved to use the platform (mainly for drivers)."))
def __str__(self):
@ -166,6 +178,17 @@ class PlatformProfile(models.Model):
# Testing / Development
auto_mark_paid = models.BooleanField(_('Test Mode: Auto-Paid'), default=False, help_text=_("If enabled, newly created parcels will automatically be marked as 'Paid' for testing."))
# Maintenance / Availability
accepting_shipments = models.BooleanField(_("Accepting Shipments"), default=True, help_text=_("Toggle to allow or stop receiving new parcel shipments."))
maintenance_message_en = models.TextField(_("Maintenance Message (English)"), blank=True, help_text=_("Message to show when shipments are stopped."))
maintenance_message_ar = models.TextField(_("Maintenance Message (Arabic)"), blank=True, help_text=_("Message to show when shipments are stopped."))
@property
def maintenance_message(self):
if get_language() == "ar":
return self.maintenance_message_ar or _("Service is temporarily suspended. Please try again later.")
return self.maintenance_message_en or _("Service is temporarily suspended. Please try again later.")
@property
def privacy_policy(self):
if get_language() == 'ar':

View File

@ -170,7 +170,7 @@
{% if not user.is_authenticated %}
<li class="nav-item ms-lg-3 d-none d-lg-block">
<a href="{% url 'shipment_request' %}" class="btn btn-masarx-primary btn-sm rounded-pill px-4">{% trans "Start Shipping" %}</a>
{% if platform_profile.accepting_shipments %}<a href="{% url 'shipment_request' %}" class="btn btn-masarx-active btn-sm rounded-pill px-4">{% trans "Start Shipping" %}</a>{% else %}<button class="btn btn-masarx-stopped btn-sm rounded-pill px-4" disabled title="{{ platform_profile.maintenance_message }}">{% trans "Stopped" %}</button>{% endif %}
</li>
{% endif %}
</ul>

View File

@ -2,6 +2,14 @@
{% load static i18n %}
{% block content %}
<style>
.bg-bronze { background-color: #cd7f32; }
.bg-silver { background-color: #c0c0c0; }
.bg-gold { background-color: #ffd700; }
.text-bronze { color: #cd7f32; }
</style>
<!-- Hero Section -->
<section class="hero-section">
<div class="container">
@ -9,8 +17,13 @@
<div class="col-lg-6 mb-5 mb-lg-0">
<h1 class="display-3 mb-4">{% trans "Small Shipments," %}<br><span style="color: var(--accent-orange)">{% trans "Smart Delivery." %}</span></h1>
<p class="lead mb-5 opacity-75">{% trans "masarX connects shippers with local car owners for fast, reliable, and trackable deliveries. Your cargo, our priority." %}</p>
{% if not platform_profile.accepting_shipments and platform_profile.maintenance_message %}
<div class="alert alert-danger mb-4 py-2 px-3 small border-0 shadow-sm" style="border-radius: 12px; max-width: 500px;">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ platform_profile.maintenance_message }}
</div>
{% endif %}
<div class="d-flex gap-3">
<a href="{% url 'shipment_request' %}" class="btn btn-masarx-primary">{% trans "Start Shipping" %}</a>
{% if platform_profile.accepting_shipments %}<a href="{% url 'shipment_request' %}" class="btn btn-masarx-active">{% trans "Start Shipping" %}</a>{% else %}<button class="btn btn-masarx-stopped" disabled style="cursor: not-allowed;">{% trans "Service Stopped" %}</button>{% endif %}
<a href="#how-it-works" class="btn btn-outline-light border-2 px-4 py-2" style="border-radius: 12px;">{% trans "Learn More" %}</a>
</div>
</div>
@ -94,7 +107,21 @@
</span>
</div>
<div class="flex-grow-1">
<h6 class="mb-0">{{ driver.user.first_name }} {{ driver.user.last_name|first }}.</h6>
<h6 class="mb-0">
{{ driver.user.first_name }} {{ driver.user.last_name|first }}.
{% if driver.driver_grade != "none" %}
{% if "bronze" in driver.driver_grade %}
<span class="badge bg-bronze text-white small" title="{{ driver.get_driver_grade_display }}"><i class="bi bi-award"></i> {{ driver.get_driver_grade_display }}</span>
{% elif driver.driver_grade == "silver" %}
<span class="badge bg-silver text-dark small" title="Silver"><i class="bi bi-award-fill"></i> Silver</span>
{% elif driver.driver_grade == "gold" %}
<span class="badge bg-gold text-dark small" title="Gold"><i class="bi bi-award-fill"></i> Gold</span>
{% endif %}
{% endif %}
{% if driver.is_recommended %}
<span class="badge bg-success small" title="{% trans "Recommended by Shippers" %}"><i class="bi bi-hand-thumbs-up"></i></span>
{% endif %}
</h6>
<small class="text-muted">{% trans "Driver" %}</small>
</div>
<div class="text-end">

View File

@ -4,6 +4,13 @@
{% block title %}{% trans "My Profile" %} | masarX{% endblock %}
{% block content %}
<style>
.bg-bronze { background-color: #cd7f32; }
.bg-silver { background-color: #c0c0c0; }
.bg-gold { background-color: #ffd700; }
</style>
<section class="py-5 bg-light" style="min-height: 80vh;">
<div class="container py-5">
<div class="row justify-content-center">
@ -32,7 +39,21 @@
<span class="text-white fs-1">{{ profile.user.first_name|first|upper }}</span>
</div>
{% endif %}
<h3 class="mt-3">{{ profile.user.get_full_name }}</h3>
<h3 class="mt-3">
{{ profile.user.get_full_name }}
{% if profile.driver_grade != "none" %}
{% if "bronze" in profile.driver_grade %}
<span class="badge bg-bronze text-white fs-6 ms-2" title="{{ profile.get_driver_grade_display }}"><i class="bi bi-award"></i> {{ profile.get_driver_grade_display }}</span>
{% elif profile.driver_grade == "silver" %}
<span class="badge bg-silver text-dark fs-6 ms-2" title="Silver"><i class="bi bi-award-fill"></i> Silver</span>
{% elif profile.driver_grade == "gold" %}
<span class="badge bg-gold text-dark fs-6 ms-2" title="Gold"><i class="bi bi-award-fill"></i> Gold</span>
{% endif %}
{% endif %}
{% if profile.is_recommended %}
<span class="badge bg-success fs-6 ms-2" title="{% trans "Recommended by Shippers" %}"><i class="bi bi-hand-thumbs-up"></i></span>
{% endif %}
</h3>
<p class="text-muted">{{ profile.get_role_display }}</p>
</div>

View File

@ -2,10 +2,26 @@
{% load i18n core_tags %}
{% block content %}
<style>
.bg-bronze { background-color: #cd7f32; }
.bg-silver { background-color: #c0c0c0; }
.bg-gold { background-color: #ffd700; }
</style>
<div class="container py-5">
{% if not platform_profile.accepting_shipments and platform_profile.maintenance_message %}
<div class="alert alert-danger mb-4 py-3 px-4 shadow-sm border-0 d-flex align-items-center" style="border-radius: 15px;">
<i class="bi bi-exclamation-triangle-fill fs-4 me-3"></i>
<div>
<h5 class="alert-heading mb-1">{% trans "Service Notice" %}</h5>
<p class="mb-0 opacity-75">{{ platform_profile.maintenance_message }}</p>
</div>
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>{% trans "My Shipments" %}</h1>
<a href="{% url 'shipment_request' %}" class="btn btn-masarx-primary">{% trans "New Shipment" %}</a>
{% if platform_profile.accepting_shipments %}<a href="{% url 'shipment_request' %}" class="btn btn-masarx-active">{% trans "New Shipment" %}</a>{% else %}<button class="btn btn-masarx-stopped" disabled style="cursor: not-allowed;">{% trans "Service Stopped" %}</button>{% endif %}
</div>
<!-- Tabs -->
@ -97,7 +113,24 @@
<hr>
<p class="card-text small mb-0"><strong>{% trans "Receiver" %}:</strong> {{ parcel.receiver_name }}</p>
<p class="card-text small"><strong>{% trans "Carrier" %}:</strong> {% if parcel.carrier %}{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}{% else %}{% trans "Waiting for pickup" %}{% endif %}</p>
<p class="card-text small"><strong>{% trans "Carrier" %}:</strong>
{% if parcel.carrier %}
{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}
{% if parcel.carrier.profile.driver_grade != "none" %}
{% if "bronze" in parcel.carrier.profile.driver_grade %}
<span class="badge bg-bronze text-white small ms-1" title="{{ parcel.carrier.profile.get_driver_grade_display }}}"><i class="bi bi-award"></i></span>
{% elif parcel.carrier.profile.driver_grade == "silver" %}
<span class="badge bg-silver text-dark small ms-1" title="Silver"><i class="bi bi-award-fill"></i></span>
{% elif parcel.carrier.profile.driver_grade == "gold" %}
<span class="badge bg-gold text-dark small ms-1" title="Gold"><i class="bi bi-award-fill"></i></span>
{% endif %}
{% endif %}
{% if parcel.carrier.profile.is_recommended %}
<span class="badge bg-success small ms-1" title="{% trans "Recommended by Shippers" %}"><i class="bi bi-hand-thumbs-up"></i></span>
{% endif %}
{% else %}
{% trans "Waiting for pickup" %}
{% endif %}</p>
</div>
</div>
</div>
@ -130,7 +163,23 @@
</span>
<span class="d-flex align-items-center gap-1">
<i class="bi bi-truck"></i>
{% if parcel.carrier %}{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}{% else %}{% trans "Waiting" %}{% endif %}
{% if parcel.carrier %}
{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}
{% if parcel.carrier.profile.driver_grade != "none" %}
{% if "bronze" in parcel.carrier.profile.driver_grade %}
<span class="badge bg-bronze text-white small ms-1" title="{{ parcel.carrier.profile.get_driver_grade_display }}}"><i class="bi bi-award"></i></span>
{% elif parcel.carrier.profile.driver_grade == "silver" %}
<span class="badge bg-silver text-dark small ms-1" title="Silver"><i class="bi bi-award-fill"></i></span>
{% elif parcel.carrier.profile.driver_grade == "gold" %}
<span class="badge bg-gold text-dark small ms-1" title="Gold"><i class="bi bi-award-fill"></i></span>
{% endif %}
{% endif %}
{% if parcel.carrier.profile.is_recommended %}
<span class="badge bg-success small ms-1" title="{% trans "Recommended by Shippers" %}"><i class="bi bi-hand-thumbs-up"></i></span>
{% endif %}
{% else %}
{% trans "Waiting" %}
{% endif %}
</span>
</div>
</div>
@ -223,7 +272,7 @@
{% else %}
<div class="text-center py-5">
<p class="lead">{% trans "You have no active shipments." %}</p>
<a href="{% url 'shipment_request' %}" class="btn btn-masarx-primary">{% trans "Send your first shipment" %}</a>
{% if platform_profile.accepting_shipments %}<a href="{% url 'shipment_request' %}" class="btn btn-masarx-active">{% trans "Send your first shipment" %}</a>{% else %}<button class="btn btn-masarx-stopped" disabled style="cursor: not-allowed;">{% trans "Service Stopped" %}</button>{% endif %}
</div>
{% endif %}
</div>

View File

@ -286,6 +286,11 @@ def dashboard(request):
@login_required
def shipment_request(request):
from .models import PlatformProfile
platform_profile = PlatformProfile.objects.first()
if platform_profile and not platform_profile.accepting_shipments:
messages.warning(request, platform_profile.maintenance_message or _("The platform is currently not accepting new shipments."))
return redirect("dashboard")
profile, created = Profile.objects.get_or_create(user=request.user)
if profile.role != 'shipper':
messages.error(request, _("Only shippers can request shipments."))

View File

@ -239,3 +239,30 @@ h1, h2, h3, h4, h5, h6 {
[dir="rtl"] .text-left {
text-align: right !important;
}
/* Shipment Status Buttons */
.btn-masarx-active {
background-color: #2fb344 !important;
color: white !important;
border: none !important;
padding: 12px 30px !important;
border-radius: 12px !important;
font-weight: 600 !important;
}
.btn-masarx-active:hover {
background-color: #248a35 !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(47, 179, 68, 0.2);
}
.btn-masarx-stopped {
background-color: #d63939 !important;
color: white !important;
border: none !important;
padding: 12px 30px !important;
border-radius: 12px !important;
font-weight: 600 !important;
}
.btn-masarx-stopped:hover {
background-color: #b02a2a !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(214, 57, 57, 0.2);
}

View File

@ -26,11 +26,12 @@ h1, h2, h3, h4, h5, h6 {
flex-direction: row !important;
align-items: center !important;
justify-content: space-between !important;
flex-wrap: nowrap !important; /* Ensure they stay on one line */
flex-wrap: nowrap !important;
gap: 2px !important;
width: 100% !important;
box-sizing: border-box !important;
/* overflow: hidden; Removed to allow dropdown list to show */
position: relative !important;
z-index: 100 !important;
}
/* Hide any stray shortcuts that might have survived JS cleanup */
@ -39,14 +40,21 @@ h1, h2, h3, h4, h5, h6 {
}
.masar-date-filter-row select {
appearance: auto !important; /* Force native dropdown appearance */
-webkit-appearance: auto !important;
-moz-appearance: auto !important;
width: 32% !important;
min-width: 0 !important;
font-size: 11px !important;
padding: 0 2px !important;
padding: 2px !important; /* Relaxed padding */
height: 28px !important;
line-height: 1 !important;
box-sizing: border-box !important;
margin: 0 !important;
background-color: white !important;
border: 1px solid #ced4da !important;
border-radius: 4px !important;
color: #495057 !important;
}
.masar-date-filter-row input {
@ -57,22 +65,23 @@ h1, h2, h3, h4, h5, h6 {
height: 28px !important;
margin: 0 !important;
box-sizing: border-box !important;
background-color: white !important;
border: 1px solid #ced4da !important;
border-radius: 4px !important;
}
/* Specific fix for date inputs to ensure they look clean */
.masar-date-filter-row input[type="date"] {
-webkit-appearance: none; /* Remove some browser defaults */
appearance: none;
line-height: 28px;
}
.masar-date-filter-row input[type="date"]::-webkit-inner-spin-button,
.masar-date-filter-row input[type="date"]::-webkit-calendar-picker-indicator {
/* Make the calendar icon smaller and fit */
width: 12px;
height: 12px;
margin: 0;
padding: 0;
opacity: 0.6;
cursor: pointer;
}
@ -230,3 +239,30 @@ h1, h2, h3, h4, h5, h6 {
[dir="rtl"] .text-left {
text-align: right !important;
}
/* Shipment Status Buttons */
.btn-masarx-active {
background-color: #2fb344 !important;
color: white !important;
border: none !important;
padding: 12px 30px !important;
border-radius: 12px !important;
font-weight: 600 !important;
}
.btn-masarx-active:hover {
background-color: #248a35 !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(47, 179, 68, 0.2);
}
.btn-masarx-stopped {
background-color: #d63939 !important;
color: white !important;
border: none !important;
padding: 12px 30px !important;
border-radius: 12px !important;
font-weight: 600 !important;
}
.btn-masarx-stopped:hover {
background-color: #b02a2a !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(214, 57, 57, 0.2);
}

View File

@ -1,7 +1,7 @@
(function($) {
// Masar Date Range Filter Layout Fix v4
// Masar Date Range Filter Layout Fix v5
// Forces a horizontal layout for the Date Range Filter in Django Admin Sidebar
// v4: Switches to type="date", removes Django's calendar shortcuts to prevent layout breakage.
// v5: Removes Bootstrap classes from Select to ensure native popup works reliably.
function initDateRangeDropdown() {
@ -30,7 +30,8 @@
var $wrapper = $('<div class="masar-date-filter-row"></div>');
// Create the Quick Select Dropdown
var $select = $('<select class="form-control custom-select admin-date-dropdown">' +
// REMOVED 'form-control' and 'custom-select' to prevent Bootstrap/AdminLTE from interfering with click/rendering
var $select = $('<select class="admin-date-dropdown">' +
'<option value="any">Any</option>' +
'<option value="today">Today</option>' +
'<option value="7days">7 Days</option>' +
@ -39,7 +40,6 @@
'</select>');
// CONVERT INPUTS TO HTML5 DATE
// This gives us a native picker and removes the need for Django's clunky JS shortcuts
$gte.attr('type', 'date').removeClass('vDateField');
$lte.attr('type', 'date').removeClass('vDateField');
@ -53,8 +53,6 @@
$wrapper.append($lte);
// 3. AGGRESSIVE CLEANUP
// Remove text nodes, BRs, AND Django's calendar shortcuts (.datetimeshortcuts)
// We search the *original parent* for these leftovers.
$parent.contents().filter(function() {
return (
(this.nodeType === 3 && $.trim($(this).text()) !== '') || // Text
@ -63,9 +61,6 @@
);
}).remove();
// Also hide any shortcuts that might be dynamically appended later (via CSS rule or observer)
// But removing the 'vDateField' class above usually prevents Django from initializing them.
// Logic for Dropdown Changes
function formatDate(d) {
var year = d.getFullYear();
@ -87,7 +82,7 @@
var today = new Date();
if (val === 'custom') {
// Do nothing, let user edit
// Do nothing
} else {
if (val === 'any') {
$gte.val('');

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB