Autosave: 20260201-171858

This commit is contained in:
Flatlogic Bot 2026-02-01 17:18:59 +00:00
parent 3bb4e04513
commit 4975582b68
14 changed files with 148 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

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 from .models import Profile, Parcel, Country, Governate, City, PlatformProfile, Testimonial, DriverRating, NotificationTemplate, PricingRule, DriverReport, DriverRejection, ParcelType
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
@ -109,7 +109,7 @@ class CustomUserAdmin(UserAdmin):
class ParcelAdmin(admin.ModelAdmin): class ParcelAdmin(admin.ModelAdmin):
change_list_template = 'admin/core/parcel/change_list.html' change_list_template = 'admin/core/parcel/change_list.html'
list_display = ('tracking_number', 'shipper', 'carrier', 'price', 'driver_amount', 'platform_fee', 'distance_km', 'status', 'payment_status', 'created_at') list_display = ('tracking_number', 'shipper', 'carrier', 'parcel_type', 'price', 'driver_amount', 'platform_fee', 'distance_km', 'status', 'payment_status', 'created_at')
list_filter = ( list_filter = (
'status', 'status',
'payment_status', 'payment_status',
@ -123,7 +123,7 @@ class ParcelAdmin(admin.ModelAdmin):
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('tracking_number', 'shipper', 'carrier', 'status', 'payment_status', 'thawani_session_id') 'fields': ('tracking_number', 'shipper', 'carrier', 'parcel_type', 'status', 'payment_status', 'thawani_session_id')
}), }),
(_('Description'), { (_('Description'), {
'fields': ('description', 'receiver_name', 'receiver_phone') 'fields': ('description', 'receiver_name', 'receiver_phone')
@ -402,3 +402,4 @@ class NotificationTemplateAdmin(admin.ModelAdmin):
return False return False
admin.site.register(NotificationTemplate, NotificationTemplateAdmin) admin.site.register(NotificationTemplate, NotificationTemplateAdmin)
admin.site.register(ParcelType)

View File

@ -2,7 +2,7 @@ from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
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 .models import Profile, Parcel, Country, Governate, City, DriverRating, DriverReport from .models import Profile, Parcel, Country, Governate, City, DriverRating, DriverReport, ParcelType
class ContactForm(forms.Form): class ContactForm(forms.Form):
name = forms.CharField(max_length=100, label=_("Name"), widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Your Name')})) name = forms.CharField(max_length=100, label=_("Name"), widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Your Name')}))
@ -240,6 +240,7 @@ class ParcelForm(forms.ModelForm):
class Meta: class Meta:
model = Parcel model = Parcel
fields = [ fields = [
'parcel_type',
'description', 'weight', 'price', 'description', 'weight', 'price',
'pickup_country', 'pickup_governate', 'pickup_city', 'pickup_address', 'pickup_country', 'pickup_governate', 'pickup_city', 'pickup_address',
'pickup_lat', 'pickup_lng', 'pickup_lat', 'pickup_lng',
@ -249,6 +250,7 @@ class ParcelForm(forms.ModelForm):
'receiver_name', 'receiver_phone' 'receiver_name', 'receiver_phone'
] ]
widgets = { widgets = {
'parcel_type': forms.Select(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control', 'placeholder': _('What are you sending?')}), 'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control', 'placeholder': _('What are you sending?')}),
'weight': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}), 'weight': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'price': forms.TextInput(attrs={'class': 'form-control', 'readonly': 'readonly'}), 'price': forms.TextInput(attrs={'class': 'form-control', 'readonly': 'readonly'}),
@ -276,6 +278,7 @@ class ParcelForm(forms.ModelForm):
'receiver_phone': forms.TextInput(attrs={'class': 'form-control'}), 'receiver_phone': forms.TextInput(attrs={'class': 'form-control'}),
} }
labels = { labels = {
'parcel_type': _('Parcel Type'),
'description': _('Package Description'), 'description': _('Package Description'),
'weight': _('Weight (kg)'), 'weight': _('Weight (kg)'),
'price': _('Calculated Price (OMR)'), 'price': _('Calculated Price (OMR)'),
@ -406,4 +409,4 @@ class DriverReportForm(forms.ModelForm):
labels = { labels = {
'reason': _('Reason for Reporting'), 'reason': _('Reason for Reporting'),
'description': _('Details'), 'description': _('Details'),
} }

View File

@ -0,0 +1,31 @@
# Generated by Django 5.2.7 on 2026-02-01 17:10
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0031_platformprofile_ticker_bg_color_and_more'),
]
operations = [
migrations.CreateModel(
name='ParcelType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name_en', models.CharField(max_length=100, verbose_name='Name (English)')),
('name_ar', models.CharField(max_length=100, verbose_name='Name (Arabic)')),
],
options={
'verbose_name': 'Parcel Type',
'verbose_name_plural': 'Parcel Types',
},
),
migrations.AddField(
model_name='parcel',
name='parcel_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.parceltype', verbose_name='Parcel Type'),
),
]

View File

@ -63,6 +63,23 @@ class City(models.Model):
verbose_name = _('City') verbose_name = _('City')
verbose_name_plural = _('Cities') verbose_name_plural = _('Cities')
class ParcelType(models.Model):
name_en = models.CharField(_('Name (English)'), max_length=100)
name_ar = models.CharField(_('Name (Arabic)'), max_length=100)
@property
def name(self):
if get_language() == 'ar':
return self.name_ar
return self.name_en
def __str__(self):
return self.name
class Meta:
verbose_name = _('Parcel Type')
verbose_name_plural = _('Parcel Types')
class Profile(models.Model): class Profile(models.Model):
ROLE_CHOICES = ( ROLE_CHOICES = (
("shipper", _("Shipper")), ("shipper", _("Shipper")),
@ -257,6 +274,7 @@ class Parcel(models.Model):
shipper = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_parcels', verbose_name=_('Shipper')) shipper = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_parcels', verbose_name=_('Shipper'))
carrier = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='carried_parcels', verbose_name=_('Carrier')) carrier = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='carried_parcels', verbose_name=_('Carrier'))
parcel_type = models.ForeignKey(ParcelType, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Parcel Type'))
description = models.TextField(_('Description')) description = models.TextField(_('Description'))
weight = models.DecimalField(_('Weight (kg)'), max_digits=5, decimal_places=2, help_text=_("Weight in kg")) weight = models.DecimalField(_('Weight (kg)'), max_digits=5, decimal_places=2, help_text=_("Weight in kg"))
price = models.DecimalField(_('Total Price (OMR)'), max_digits=10, decimal_places=3, default=Decimal('0.000')) price = models.DecimalField(_('Total Price (OMR)'), max_digits=10, decimal_places=3, default=Decimal('0.000'))
@ -505,4 +523,4 @@ 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']

View File

@ -11,7 +11,7 @@ def calculate_haversine_distance(lat1, lon1, lat2, lon2):
return Decimal('0.00') return Decimal('0.00')
# Convert decimal degrees to radians # Convert decimal degrees to radians
lat1, lon1, lat2, lon2 = map(float, [lat1, lon1, lat2, lon2]) lat1, lon1, lat2, lon2 = map(math.radians, [float(lat1), float(lon1), float(lat2), float(lon2)])
# Haversine formula # Haversine formula
dlon = lon2 - lon1 dlon = lon2 - lon1

View File

@ -40,6 +40,13 @@
<div class="card-body"> <div class="card-body">
<h5 class="card-title text-secondary mb-3">{% trans "Shipment Information" %}</h5> <h5 class="card-title text-secondary mb-3">{% trans "Shipment Information" %}</h5>
<div class="row g-3"> <div class="row g-3">
<div class="col-12">
<label class="form-label fw-bold" for="{{ form.parcel_type.id_for_label }}">{{ form.parcel_type.label }}</label>
{{ form.parcel_type }}
{% if form.parcel_type.errors %}
<div class="text-danger small">{{ form.parcel_type.errors }}</div>
{% endif %}
</div>
<div class="col-12"> <div class="col-12">
<label class="form-label fw-bold" for="{{ form.description.id_for_label }}">{{ form.description.label }}</label> <label class="form-label fw-bold" for="{{ form.description.id_for_label }}">{{ form.description.label }}</label>
{{ form.description }} {{ form.description }}
@ -166,6 +173,32 @@
</div> </div>
</div> </div>
<!-- Pricing Summary Display -->
<div id="pricingSummary" class="card border-0 bg-light p-4 mb-4 d-none" style="border-radius: 20px; border: 2px dashed #0d6efd !important;">
<div class="row align-items-center">
<div class="col-md-6 mb-3 mb-md-0">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 p-3 rounded-circle me-3">
<i class="bi bi-geo-alt-fill text-primary h3 mb-0"></i>
</div>
<div>
<p class="text-muted mb-0 small text-uppercase fw-bold">{% trans "Estimated Distance" %}</p>
<h2 class="fw-bold mb-0"><span id="summaryDistance">0</span> <small class="h6 text-muted">km</small></h2>
</div>
</div>
</div>
<div class="col-md-6 text-md-end">
<div class="d-inline-block text-start text-md-end">
<p class="text-muted mb-0 small text-uppercase fw-bold">{% trans "Total Price" %}</p>
<h1 class="fw-bold text-success mb-0" style="font-size: 3rem;"><span id="summaryPrice">0.000</span> <small class="h4">OMR</small></h1>
</div>
</div>
</div>
<div id="pricingError" class="mt-3 text-danger d-none">
<i class="bi bi-exclamation-triangle me-1"></i> <span id="pricingErrorMessage"></span>
</div>
</div>
<div class="d-flex gap-3 justify-content-end"> <div class="d-flex gap-3 justify-content-end">
<a href="{% url 'dashboard' %}" class="btn btn-outline-secondary px-5 py-2">{% trans "Cancel" %}</a> <a href="{% url 'dashboard' %}" class="btn btn-outline-secondary px-5 py-2">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-masarx-primary px-5 py-2">{% trans "Submit Request" %}</button> <button type="submit" class="btn btn-masarx-primary px-5 py-2">{% trans "Submit Request" %}</button>
@ -372,8 +405,12 @@
const deliveryLng = document.getElementById('id_delivery_lng').value; const deliveryLng = document.getElementById('id_delivery_lng').value;
const weight = document.getElementById('id_weight').value; const weight = document.getElementById('id_weight').value;
const summaryDiv = document.getElementById('pricingSummary');
const errorDiv = document.getElementById('pricingError');
const errorMessage = document.getElementById('pricingErrorMessage');
if (pickupLat && pickupLng && deliveryLat && deliveryLng && weight) { if (pickupLat && pickupLng && deliveryLat && deliveryLng && weight) {
fetch('{% url "api_calculate_price" %}', { fetch('{% url "api_pricing" %}', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -391,12 +428,26 @@
.then(data => { .then(data => {
if (data.error) { if (data.error) {
console.error(data.error); console.error(data.error);
summaryDiv.classList.remove('d-none');
errorDiv.classList.remove('d-none');
errorMessage.textContent = data.error;
document.getElementById('summaryPrice').textContent = "0.000";
document.getElementById('summaryDistance').textContent = "0";
} else { } else {
document.getElementById('id_price').value = data.price; // Update hidden and readonly fields
document.getElementById('id_price').value = data.price.toFixed(3);
document.getElementById('id_distance_km').value = data.distance_km; document.getElementById('id_distance_km').value = data.distance_km;
document.getElementById('id_platform_fee').value = data.platform_fee; document.getElementById('id_platform_fee').value = data.platform_fee;
document.getElementById('id_driver_amount').value = data.driver_amount; document.getElementById('id_driver_amount').value = data.driver_amount;
document.getElementById('id_platform_fee_percentage').value = data.platform_fee_percentage; document.getElementById('id_platform_fee_percentage').value = data.platform_fee_percentage;
// Update visual summary
document.getElementById('summaryDistance').textContent = data.distance_km;
document.getElementById('summaryPrice').textContent = data.price.toFixed(3);
// Show the summary, hide error
summaryDiv.classList.remove('d-none');
errorDiv.classList.add('d-none');
} }
}) })
.catch(error => console.error('Error:', error)); .catch(error => console.error('Error:', error));
@ -411,13 +462,36 @@
} }
window.initMap = initMap; window.initMap = initMap;
// Helper to geocode from dropdown selection
function geocodeFromDropdown(mode) {
const city = getSelectedText(`id_${mode}_city`);
const governate = getSelectedText(`id_${mode}_governate`);
const country = getSelectedText(`id_${mode}_country`);
if (city && typeof google !== 'undefined' && google.maps && google.maps.Geocoder) {
let addressQuery = city;
if (governate) addressQuery += `, ${governate}`;
if (country) addressQuery += `, ${country}`;
const geocoder = new google.maps.Geocoder();
geocoder.geocode({ address: addressQuery }, (results, status) => {
if (status === "OK" && results[0]) {
const location = results[0].geometry.location;
document.getElementById(`id_${mode}_lat`).value = location.lat();
document.getElementById(`id_${mode}_lng`).value = location.lng();
calculatePrice();
}
});
}
}
</script> </script>
<script src="https://maps.googleapis.com/maps/api/js?key={{ google_maps_api_key }}&loading=async&callback=initMap" async defer></script> <script src="https://maps.googleapis.com/maps/api/js?key={{ google_maps_api_key }}&loading=async&callback=initMap" async defer></script>
{% endif %} {% endif %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
function setupDependentDropdowns(countryId, governateId, cityId) { function setupDependentDropdowns(countryId, governateId, cityId, mode) {
const countrySelect = document.getElementById(countryId); const countrySelect = document.getElementById(countryId);
const governateSelect = document.getElementById(governateId); const governateSelect = document.getElementById(governateId);
const citySelect = document.getElementById(cityId); const citySelect = document.getElementById(cityId);
@ -460,10 +534,17 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
}); });
// Trigger calculation on city change
citySelect.addEventListener('change', function() {
if (typeof geocodeFromDropdown === 'function') {
geocodeFromDropdown(mode);
}
});
} }
setupDependentDropdowns('id_pickup_country', 'id_pickup_governate', 'id_pickup_city'); setupDependentDropdowns('id_pickup_country', 'id_pickup_governate', 'id_pickup_city', 'pickup');
setupDependentDropdowns('id_delivery_country', 'id_delivery_governate', 'id_delivery_city'); setupDependentDropdowns('id_delivery_country', 'id_delivery_governate', 'id_delivery_city', 'delivery');
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -61,6 +61,6 @@ urlpatterns = [
# API Endpoints (for Mobile App) # API Endpoints (for Mobile App)
path('api/v1/parcels/', api_views.ParcelListCreateView.as_view(), name='api_parcel_list'), path('api/v1/parcels/', api_views.ParcelListCreateView.as_view(), name='api_parcel_list'),
path('api/v1/parcels/<int:pk>/', api_views.ParcelDetailView.as_view(), name='api_parcel_detail'), path('api/v1/parcels/<int:pk>/', api_views.ParcelDetailView.as_view(), name='api_parcel_detail'),
path('api/v1/pricing/', api_views.PriceCalculatorView.as_view(), name='api_pricing'), path('api/v1/pricing/', api_views.PriceCalculatorView.as_view(), name='api_pricing'), path('api/v1/calculate-price/', api_views.PriceCalculatorView.as_view(), name='api_calculate_price'),
path('api/v1/profile/', api_views.UserProfileView.as_view(), name='api_profile'), path('api/v1/profile/', api_views.UserProfileView.as_view(), name='api_profile'),
] ]