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.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
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.urls import path, reverse
from django.shortcuts import render, redirect
@ -109,7 +109,7 @@ class CustomUserAdmin(UserAdmin):
class ParcelAdmin(admin.ModelAdmin):
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 = (
'status',
'payment_status',
@ -123,7 +123,7 @@ class ParcelAdmin(admin.ModelAdmin):
fieldsets = (
(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'), {
'fields': ('description', 'receiver_name', 'receiver_phone')
@ -402,3 +402,4 @@ class NotificationTemplateAdmin(admin.ModelAdmin):
return False
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.utils.translation import gettext_lazy as _
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):
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:
model = Parcel
fields = [
'parcel_type',
'description', 'weight', 'price',
'pickup_country', 'pickup_governate', 'pickup_city', 'pickup_address',
'pickup_lat', 'pickup_lng',
@ -249,6 +250,7 @@ class ParcelForm(forms.ModelForm):
'receiver_name', 'receiver_phone'
]
widgets = {
'parcel_type': forms.Select(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control', 'placeholder': _('What are you sending?')}),
'weight': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'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'}),
}
labels = {
'parcel_type': _('Parcel Type'),
'description': _('Package Description'),
'weight': _('Weight (kg)'),
'price': _('Calculated Price (OMR)'),

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_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):
ROLE_CHOICES = (
("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'))
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'))
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'))

View File

@ -11,7 +11,7 @@ def calculate_haversine_distance(lat1, lon1, lat2, lon2):
return Decimal('0.00')
# 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
dlon = lon2 - lon1

View File

@ -40,6 +40,13 @@
<div class="card-body">
<h5 class="card-title text-secondary mb-3">{% trans "Shipment Information" %}</h5>
<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">
<label class="form-label fw-bold" for="{{ form.description.id_for_label }}">{{ form.description.label }}</label>
{{ form.description }}
@ -166,6 +173,32 @@
</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">
<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>
@ -372,8 +405,12 @@
const deliveryLng = document.getElementById('id_delivery_lng').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) {
fetch('{% url "api_calculate_price" %}', {
fetch('{% url "api_pricing" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -391,12 +428,26 @@
.then(data => {
if (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 {
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_platform_fee').value = data.platform_fee;
document.getElementById('id_driver_amount').value = data.driver_amount;
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));
@ -411,13 +462,36 @@
}
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 src="https://maps.googleapis.com/maps/api/js?key={{ google_maps_api_key }}&loading=async&callback=initMap" async defer></script>
{% endif %}
<script>
document.addEventListener('DOMContentLoaded', function() {
function setupDependentDropdowns(countryId, governateId, cityId) {
function setupDependentDropdowns(countryId, governateId, cityId, mode) {
const countrySelect = document.getElementById(countryId);
const governateSelect = document.getElementById(governateId);
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_delivery_country', 'id_delivery_governate', 'id_delivery_city');
setupDependentDropdowns('id_pickup_country', 'id_pickup_governate', 'id_pickup_city', 'pickup');
setupDependentDropdowns('id_delivery_country', 'id_delivery_governate', 'id_delivery_city', 'delivery');
});
</script>
{% endblock %}

View File

@ -61,6 +61,6 @@ urlpatterns = [
# API Endpoints (for Mobile App)
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/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'),
]