diff --git a/assets/pasted-20260201-154837-60ba680b.png b/assets/pasted-20260201-154837-60ba680b.png new file mode 100644 index 0000000..59b5193 Binary files /dev/null and b/assets/pasted-20260201-154837-60ba680b.png differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index f06e27f..a41ab66 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 08813f3..e3eb9ff 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 7fb33b1..92a5f4b 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/pricing.cpython-311.pyc b/core/__pycache__/pricing.cpython-311.pyc index ac1ab6c..979af60 100644 Binary files a/core/__pycache__/pricing.cpython-311.pyc and b/core/__pycache__/pricing.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 28a9da4..c75b3ce 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 3ac0780..e1dab42 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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) diff --git a/core/forms.py b/core/forms.py index 05ff5b3..57cc2dd 100644 --- a/core/forms.py +++ b/core/forms.py @@ -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)'), @@ -406,4 +409,4 @@ class DriverReportForm(forms.ModelForm): labels = { 'reason': _('Reason for Reporting'), 'description': _('Details'), - } + } \ No newline at end of file diff --git a/core/migrations/0032_parceltype_parcel_parcel_type.py b/core/migrations/0032_parceltype_parcel_parcel_type.py new file mode 100644 index 0000000..7005078 --- /dev/null +++ b/core/migrations/0032_parceltype_parcel_parcel_type.py @@ -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'), + ), + ] diff --git a/core/migrations/__pycache__/0032_parceltype_parcel_parcel_type.cpython-311.pyc b/core/migrations/__pycache__/0032_parceltype_parcel_parcel_type.cpython-311.pyc new file mode 100644 index 0000000..7344599 Binary files /dev/null and b/core/migrations/__pycache__/0032_parceltype_parcel_parcel_type.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index baee9fb..c6156d8 100644 --- a/core/models.py +++ b/core/models.py @@ -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')) @@ -505,4 +523,4 @@ class DriverRejection(models.Model): class Meta: verbose_name = _('Driver Rejection') verbose_name_plural = _('Driver Rejections') - ordering = ['-created_at'] + ordering = ['-created_at'] \ No newline at end of file diff --git a/core/pricing.py b/core/pricing.py index cebedb5..a33fddc 100644 --- a/core/pricing.py +++ b/core/pricing.py @@ -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 diff --git a/core/templates/core/shipment_request.html b/core/templates/core/shipment_request.html index 5ceca8c..3b94464 100644 --- a/core/templates/core/shipment_request.html +++ b/core/templates/core/shipment_request.html @@ -40,6 +40,13 @@
{% trans "Shipment Information" %}
+
+ + {{ form.parcel_type }} + {% if form.parcel_type.errors %} +
{{ form.parcel_type.errors }}
+ {% endif %} +
{{ form.description }} @@ -166,6 +173,32 @@
+ +
+
+
+
+
+ +
+
+

{% trans "Estimated Distance" %}

+

0 km

+
+
+
+
+
+

{% trans "Total Price" %}

+

0.000 OMR

+
+
+
+
+ +
+
+
{% trans "Cancel" %} @@ -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(); + } + }); + } + } {% endif %} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 7bd8771..6e4d483 100644 --- a/core/urls.py +++ b/core/urls.py @@ -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//', 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'), ]