diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 82e7cc0..f7f591a 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 1c5c1d2..c9053e4 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 0d02fbb..9951dda 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/forms.py b/core/forms.py index d48204e..c9de2b1 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,5 +1,5 @@ from django import forms -from .models import Truck, Shipment, Bid, Profile, Country, OTPCode +from .models import Truck, Shipment, Bid, Profile, Country, OTPCode, City from django.utils.translation import gettext_lazy as _ from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User @@ -89,12 +89,19 @@ class TruckForm(forms.ModelForm): class ShipmentForm(forms.ModelForm): class Meta: model = Shipment - fields = ['description', 'weight', 'origin', 'destination', 'delivery_date'] + fields = [ + 'description', 'weight', + 'origin_country', 'origin_city', + 'destination_country', 'destination_city', + 'delivery_date' + ] widgets = { 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 'weight': forms.TextInput(attrs={'class': 'form-control'}), - 'origin': forms.TextInput(attrs={'class': 'form-control'}), - 'destination': forms.TextInput(attrs={'class': 'form-control'}), + 'origin_country': forms.Select(attrs={'class': 'form-select location-selector', 'data-type': 'origin'}), + 'origin_city': forms.Select(attrs={'class': 'form-select'}), + 'destination_country': forms.Select(attrs={'class': 'form-select location-selector', 'data-type': 'destination'}), + 'destination_city': forms.Select(attrs={'class': 'form-select'}), 'delivery_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), } @@ -120,8 +127,12 @@ class BidForm(forms.ModelForm): class ShipperOfferForm(forms.Form): description = forms.CharField(label=_('Goods Description'), widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3})) weight = forms.CharField(label=_('Weight/Volume'), widget=forms.TextInput(attrs={'class': 'form-control'})) - origin = forms.CharField(label=_('Origin'), widget=forms.TextInput(attrs={'class': 'form-control'})) - destination = forms.CharField(label=_('Destination'), widget=forms.TextInput(attrs={'class': 'form-control'})) + + origin_country = forms.ModelChoiceField(queryset=Country.objects.all(), widget=forms.Select(attrs={'class': 'form-select location-selector', 'data-type': 'origin'})) + origin_city = forms.ModelChoiceField(queryset=City.objects.all(), widget=forms.Select(attrs={'class': 'form-select'})) + destination_country = forms.ModelChoiceField(queryset=Country.objects.all(), widget=forms.Select(attrs={'class': 'form-select location-selector', 'data-type': 'destination'})) + destination_city = forms.ModelChoiceField(queryset=City.objects.all(), widget=forms.Select(attrs={'class': 'form-select'})) + delivery_date = forms.DateField(label=_('Requested Delivery Date'), widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date'})) amount = forms.DecimalField(label=_('Offer Amount'), max_digits=10, decimal_places=2, widget=forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'})) - comments = forms.CharField(label=_('Comments'), required=False, widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 2})) + comments = forms.CharField(label=_('Comments'), required=False, widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 2})) \ No newline at end of file diff --git a/core/migrations/0010_shipment_destination_country_shipment_origin_country_and_more.py b/core/migrations/0010_shipment_destination_country_shipment_origin_country_and_more.py new file mode 100644 index 0000000..eede752 --- /dev/null +++ b/core/migrations/0010_shipment_destination_country_shipment_origin_country_and_more.py @@ -0,0 +1,57 @@ +# Generated by Django 5.2.7 on 2026-01-23 14:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_otpcode_alter_profile_phone_number'), + ] + + operations = [ + migrations.AddField( + model_name='shipment', + name='destination_country', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='shipments_destination', to='core.country'), + ), + migrations.AddField( + model_name='shipment', + name='origin_country', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='shipments_origin', to='core.country'), + ), + migrations.AlterField( + model_name='shipment', + name='destination', + field=models.CharField(blank=True, max_length=255, verbose_name='Destination (Legacy)'), + ), + migrations.AlterField( + model_name='shipment', + name='origin', + field=models.CharField(blank=True, max_length=255, verbose_name='Origin (Legacy)'), + ), + migrations.CreateModel( + name='City', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='City Name')), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cities', to='core.country')), + ], + options={ + 'verbose_name': 'City', + 'verbose_name_plural': 'Cities', + 'ordering': ['name'], + }, + ), + migrations.AddField( + model_name='shipment', + name='destination_city', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='shipments_destination', to='core.city'), + ), + migrations.AddField( + model_name='shipment', + name='origin_city', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='shipments_origin', to='core.city'), + ), + ] diff --git a/core/migrations/__pycache__/0010_shipment_destination_country_shipment_origin_country_and_more.cpython-311.pyc b/core/migrations/__pycache__/0010_shipment_destination_country_shipment_origin_country_and_more.cpython-311.pyc new file mode 100644 index 0000000..b086e3e Binary files /dev/null and b/core/migrations/__pycache__/0010_shipment_destination_country_shipment_origin_country_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index f1331d6..db726e5 100644 --- a/core/models.py +++ b/core/models.py @@ -20,6 +20,18 @@ class Country(models.Model): def __str__(self): return f"{self.name} (+{self.code})" +class City(models.Model): + country = models.ForeignKey(Country, on_delete=models.CASCADE, related_name='cities') + name = models.CharField(_('City Name'), max_length=100) + + class Meta: + verbose_name = _('City') + verbose_name_plural = _('Cities') + ordering = ['name'] + + def __str__(self): + return f"{self.name} ({self.country.name})" + class Profile(models.Model): ROLE_CHOICES = ( ('SHIPPER', _('Shipper (Need Goods Moved)')), @@ -123,8 +135,15 @@ class Shipment(models.Model): shipper = models.ForeignKey(User, on_delete=models.CASCADE, related_name='shipments') description = models.TextField(_('Goods Description')) weight = models.CharField(_('Weight/Volume'), max_length=100) - origin = models.CharField(_('Origin'), max_length=255) - destination = models.CharField(_('Destination'), max_length=255) + + origin_country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, related_name='shipments_origin') + origin_city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, related_name='shipments_origin') + destination_country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, related_name='shipments_destination') + destination_city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, related_name='shipments_destination') + + origin = models.CharField(_('Origin (Legacy)'), max_length=255, blank=True) + destination = models.CharField(_('Destination (Legacy)'), max_length=255, blank=True) + delivery_date = models.DateField(_('Requested Delivery Date')) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='OPEN') @@ -133,7 +152,19 @@ class Shipment(models.Model): created_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return f"{self.origin} to {self.destination} - {self.status}" + return f"{self.display_origin} to {self.display_destination} - {self.status}" + + @property + def display_origin(self): + if self.origin_city and self.origin_country: + return f"{self.origin_city.name}, {self.origin_country.name}" + return self.origin + + @property + def display_destination(self): + if self.destination_city and self.destination_country: + return f"{self.destination_city.name}, {self.destination_country.name}" + return self.destination class Bid(models.Model): STATUS_CHOICES = ( @@ -202,4 +233,4 @@ def sync_user_groups(sender, instance, **kwargs): instance.user.groups.remove(*other_groups) # Add user to the correct group - instance.user.groups.add(group) + instance.user.groups.add(group) \ No newline at end of file diff --git a/core/templates/core/place_bid.html b/core/templates/core/place_bid.html index 6abaa7d..b3805c3 100644 --- a/core/templates/core/place_bid.html +++ b/core/templates/core/place_bid.html @@ -4,7 +4,7 @@ {% block content %}
-
+

{% trans "Send Shipping Offer" %}

@@ -34,41 +34,56 @@
{% trans "Shipment Details" %}
- + {{ form.description }} {{ form.description.errors }}
- +
- - {{ form.origin }} - {{ form.origin.errors }} -
-
- - {{ form.destination }} - {{ form.destination.errors }} -
-
- -
-
- + {{ form.weight }} {{ form.weight.errors }}
- + {{ form.delivery_date }} {{ form.delivery_date.errors }}
+ +
+
{% trans "Route" %}
+
+
+ +
+ {{ form.origin_country }} + {{ form.origin_country.errors }} +
+
+ {{ form.origin_city }} + {{ form.origin_city.errors }} +
+
+
+ +
+ {{ form.destination_country }} + {{ form.destination_country.errors }} +
+
+ {{ form.destination_city }} + {{ form.destination_city.errors }} +
+
+
+
{% trans "Pricing & Terms" %}
- +
$ {{ form.amount }} @@ -77,7 +92,7 @@
- + {{ form.comments }} {{ form.comments.errors }}
@@ -94,4 +109,47 @@
-{% endblock %} + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/post_shipment.html b/core/templates/core/post_shipment.html index 4e89f1e..2a11611 100644 --- a/core/templates/core/post_shipment.html +++ b/core/templates/core/post_shipment.html @@ -5,51 +5,111 @@
-
+

{% trans "Post a New Shipment" %}

+

{% trans "Enter shipment details to receive bids or send as an offer." %}

- {% if form.errors %} -
- {% trans "Please correct the errors below." %} - {{ form.non_field_errors }} -
- {% endif %} - -
+ {% csrf_token %} -
- + +
+ {{ form.description }} - {{ form.description.errors }} + {% if form.description.errors %}
{{ form.description.errors }}
{% endif %}
-
- - {{ form.weight }} - {{ form.weight.errors }} + +
+
+ + {{ form.weight }} + {% if form.weight.errors %}
{{ form.weight.errors }}
{% endif %} +
+
+ + {{ form.delivery_date }} + {% if form.delivery_date.errors %}
{{ form.delivery_date.errors }}
{% endif %} +
+ +
+ +
{% trans "Route Details" %}
+
-
- - {{ form.origin }} - {{ form.origin.errors }} +
+
+ + {{ form.origin_country }} + + + {{ form.origin_city }} +
-
- - {{ form.destination }} - {{ form.destination.errors }} +
+
+ + {{ form.destination_country }} + + + {{ form.destination_city }} +
-
- - {{ form.delivery_date }} - {{ form.delivery_date.errors }} -
- + +
-{% endblock %} \ No newline at end of file + + +{% endblock %} diff --git a/core/templates/core/shipment_detail.html b/core/templates/core/shipment_detail.html index df5aab2..c14a45d 100644 --- a/core/templates/core/shipment_detail.html +++ b/core/templates/core/shipment_detail.html @@ -5,39 +5,39 @@
-
+
-

{{ shipment.origin }} {{ shipment.destination }}

+

{{ shipment.display_origin }} {{ shipment.display_destination }}

{{ shipment.get_status_display }}

-
{% trans "Shipment Details" %}
-

{{ shipment.description }}

-
+
{% trans "Shipment Details" %}
+

{{ shipment.description }}

+
- {% trans "Weight" %} - {{ shipment.weight }} + {% trans "Weight / Volume" %} + {{ shipment.weight }}
- {% trans "Requested Delivery Date" %} - {{ shipment.delivery_date }} + {% trans "Requested Delivery Date" %} + {{ shipment.delivery_date }}
-
-
-
{% trans "Offer Status" %}
+
+
+
{% trans "Offer Details" %}
- - +
+ @@ -48,8 +48,11 @@ {% for bid in bids %} - - + + {% empty %} - + {% endfor %} @@ -78,42 +84,56 @@ {% if shipment.status == 'IN_PROGRESS' %} -
- -
-
{% trans "Shipment is IN PROGRESS" %}
-

{% trans "Assigned Truck:" %} {{ shipment.assigned_truck.display_truck_type }} ({{ shipment.assigned_truck.plate_no }})

+
+
+ +
+
{% trans "Shipment is IN PROGRESS" %}
+

+ {% trans "Assigned Truck:" %} {{ shipment.assigned_truck.display_truck_type }} ({{ shipment.assigned_truck.plate_no }}) +

+
{% endif %}
-
+
-
{% trans "Stakeholders" %}
-
- {% trans "Shipper" %} - {{ shipment.shipper.username }} +
{% trans "Contact Information" %}
+
+ {% trans "Shipper" %} +
+
+ +
+ {{ shipment.shipper.username }} +
{% if shipment.assigned_truck %} -
- {% trans "Truck Owner" %} - {{ shipment.assigned_truck.owner.username }} +
+ {% trans "Truck Owner" %} +
+
+ +
+ {{ shipment.assigned_truck.owner.username }} +
{% endif %}
{% if shipment.status == 'IN_PROGRESS' %} -
+
{% if user == shipment.shipper %} - - {% trans "Message Driver" %} + + {% trans "WhatsApp Driver" %} {% elif user == shipment.assigned_truck.owner %} - - {% trans "Message Shipper" %} + + {% trans "WhatsApp Shipper" %} {% endif %}
@@ -121,4 +141,4 @@
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/shipper_dashboard.html b/core/templates/core/shipper_dashboard.html index cf8ba59..5048969 100644 --- a/core/templates/core/shipper_dashboard.html +++ b/core/templates/core/shipper_dashboard.html @@ -8,9 +8,14 @@

{% trans "Shipper Dashboard" %}

{% trans "Manage your shipping offers and active shipments." %}

- - {% trans "Browse Trucks" %} - +
@@ -39,9 +44,9 @@ {{ bid.truck.plate_no }}
@@ -93,10 +98,9 @@ {% for shipment in shipments %} - {% if shipment.status != 'OPEN' or not shipment.bids.exists %} - + - {% endif %} {% empty %} @@ -126,4 +129,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/truck_owner_dashboard.html b/core/templates/core/truck_owner_dashboard.html index 8c1013d..850f182 100644 --- a/core/templates/core/truck_owner_dashboard.html +++ b/core/templates/core/truck_owner_dashboard.html @@ -38,7 +38,7 @@ {% for bid in bids %} @@ -122,4 +122,4 @@ {% endfor %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/views.py b/core/views.py index ee86512..435c180 100644 --- a/core/views.py +++ b/core/views.py @@ -2,7 +2,7 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib.auth import login, authenticate, logout from django.utils import timezone -from .models import Profile, Truck, Shipment, Bid, Message, OTPCode +from .models import Profile, Truck, Shipment, Bid, Message, OTPCode, Country, City from .forms import TruckForm, ShipmentForm, BidForm, UserRegistrationForm, OTPVerifyForm, ShipperOfferForm from django.contrib import messages from django.utils.translation import gettext as _ @@ -77,7 +77,8 @@ def verify_otp_registration(request): profile.save() login(request, user) - del request.session['registration_data'] + if 'registration_data' in request.session: + del request.session['registration_data'] messages.success(request, _("Registration successful. Welcome!")) return redirect('dashboard') else: @@ -137,7 +138,8 @@ def verify_otp_login(request): otp_record.save() login(request, user) - del request.session['pre_otp_user_id'] + if 'pre_otp_user_id' in request.session: + del request.session['pre_otp_user_id'] messages.success(request, _("Logged in successfully!")) return redirect('dashboard') else: @@ -253,7 +255,7 @@ def suspend_truck(request, truck_id): @login_required def post_shipment(request): - """Note: This is now largely redundant but kept for compatibility or direct posting.""" + """Note: This is used as the 'Add A Bid' / 'Add Post' action for Shippers.""" if request.user.profile.role != 'SHIPPER': return redirect('dashboard') @@ -263,7 +265,7 @@ def post_shipment(request): shipment = form.save(commit=False) shipment.shipper = request.user shipment.save() - messages.success(request, _("Shipment posted successfully!")) + messages.success(request, _("Shipment posted successfully! It is now open for bids or you can browse trucks to send it as an offer.")) return redirect('dashboard') else: messages.error(request, _("Please correct the errors in the form.")) @@ -296,8 +298,10 @@ def place_bid(request, truck_id): shipper=request.user, description=form.cleaned_data['description'], weight=form.cleaned_data['weight'], - origin=form.cleaned_data['origin'], - destination=form.cleaned_data['destination'], + origin_country=form.cleaned_data['origin_country'], + origin_city=form.cleaned_data['origin_city'], + destination_country=form.cleaned_data['destination_country'], + destination_city=form.cleaned_data['destination_city'], delivery_date=form.cleaned_data['delivery_date'], status='OPEN' ) @@ -314,7 +318,7 @@ def place_bid(request, truck_id): # Notify Truck Owner via WhatsApp owner_phone = getattr(truck.owner.profile, 'full_phone_number', None) if owner_phone: - msg = f"New offer received for your truck ({truck.plate_no})! Route: {shipment.origin} to {shipment.destination}. Amount: {bid.amount}" + msg = f"New offer received for your truck ({truck.plate_no})! Route: {shipment.display_origin} to {shipment.display_destination}. Amount: {bid.amount}" send_whatsapp_message(owner_phone, msg) messages.success(request, _("Offer sent successfully!")) @@ -357,7 +361,7 @@ def accept_bid(request, bid_id): # Notify Shipper via WhatsApp shipper_phone = getattr(bid.shipment.shipper.profile, 'full_phone_number', None) if shipper_phone: - msg = f"Your offer for truck {bid.truck.plate_no} ({bid.shipment.origin} to {bid.shipment.destination}) has been accepted!" + msg = f"Your offer for truck {bid.truck.plate_no} ({bid.shipment.display_origin} to {bid.shipment.display_destination}) has been accepted!" send_whatsapp_message(shipper_phone, msg) messages.success(request, _("Offer accepted! Shipment is now in progress.")) @@ -373,4 +377,4 @@ def reject_bid(request, bid_id): bid.status = 'REJECTED' bid.save() messages.info(request, _("Offer rejected.")) - return redirect('dashboard') + return redirect('dashboard') \ No newline at end of file
{% trans "Truck" %} {% trans "Amount" %}
{{ bid.truck.display_truck_type }} ({{ bid.truck.plate_no }})${{ bid.amount }} +
{{ bid.truck.display_truck_type }}
+ {{ bid.truck.plate_no }} +
${{ bid.amount }} {{ bid.get_status_display }} @@ -68,7 +71,10 @@
{% trans "No offers for this shipment." %} + +

{% trans "No offers for this shipment yet." %}

+
- {{ bid.shipment.origin }} + {{ bid.shipment.display_origin }} - {{ bid.shipment.destination }} + {{ bid.shipment.display_destination }} ${{ bid.amount }} {{ bid.created_at|date:"d M Y" }}
{{ shipment.description|truncatechars:30 }}{{ shipment.origin }} {{ shipment.destination }}{{ shipment.display_origin }} {{ shipment.display_destination }} {{ shipment.delivery_date }} @@ -114,7 +118,6 @@ {% trans "Track" %}
{% trans "No active shipments." %}
-
{{ bid.shipment.origin }} {{ bid.shipment.destination }}
+
{{ bid.shipment.display_origin }} {{ bid.shipment.display_destination }}
{{ bid.shipment.delivery_date }}
{{ bid.truck.plate_no }}