This commit is contained in:
Flatlogic Bot 2026-01-23 15:01:49 +00:00
parent 80485899cb
commit 459f38ada6
13 changed files with 364 additions and 120 deletions

View File

@ -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}))

View File

@ -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'),
),
]

View File

@ -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)

View File

@ -4,7 +4,7 @@
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="col-md-8 col-lg-7">
<div class="card shadow-lg border-0">
<div class="card-body p-5">
<h2 class="mb-4 text-center">{% trans "Send Shipping Offer" %}</h2>
@ -34,41 +34,56 @@
<h6 class="border-bottom pb-2 mb-3">{% trans "Shipment Details" %}</h6>
<div class="mb-3">
<label class="form-label">{{ form.description.label }}</label>
<label class="form-label fw-bold">{{ form.description.label }}</label>
{{ form.description }}
{{ form.description.errors }}
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">{{ form.origin.label }}</label>
{{ form.origin }}
{{ form.origin.errors }}
</div>
<div class="col-md-6">
<label class="form-label">{{ form.destination.label }}</label>
{{ form.destination }}
{{ form.destination.errors }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">{{ form.weight.label }}</label>
<label class="form-label fw-bold">{{ form.weight.label }}</label>
{{ form.weight }}
{{ form.weight.errors }}
</div>
<div class="col-md-6">
<label class="form-label">{{ form.delivery_date.label }}</label>
<label class="form-label fw-bold">{{ form.delivery_date.label }}</label>
{{ form.delivery_date }}
{{ form.delivery_date.errors }}
</div>
</div>
<div class="card bg-light border-0 p-3 mb-4">
<h6 class="mb-3"><i class="fa-solid fa-map-marker-alt me-2 text-primary"></i> {% trans "Route" %}</h6>
<div class="row">
<div class="col-md-6 border-end">
<label class="form-label small text-muted">{% trans "Origin" %}</label>
<div class="mb-2">
{{ form.origin_country }}
{{ form.origin_country.errors }}
</div>
<div>
{{ form.origin_city }}
{{ form.origin_city.errors }}
</div>
</div>
<div class="col-md-6">
<label class="form-label small text-muted">{% trans "Destination" %}</label>
<div class="mb-2">
{{ form.destination_country }}
{{ form.destination_country.errors }}
</div>
<div>
{{ form.destination_city }}
{{ form.destination_city.errors }}
</div>
</div>
</div>
</div>
<h6 class="border-bottom pb-2 mb-3 mt-4">{% trans "Pricing & Terms" %}</h6>
<div class="mb-3">
<label class="form-label">{{ form.amount.label }}</label>
<label class="form-label fw-bold">{{ form.amount.label }}</label>
<div class="input-group">
<span class="input-group-text">$</span>
{{ form.amount }}
@ -77,7 +92,7 @@
</div>
<div class="mb-4">
<label class="form-label">{{ form.comments.label }}</label>
<label class="form-label fw-bold">{{ form.comments.label }}</label>
{{ form.comments }}
{{ form.comments.errors }}
</div>
@ -94,4 +109,47 @@
</div>
</div>
</div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const citiesByCountry = {
{% for country in form.fields.origin_country.queryset %}
"{{ country.id }}": [
{% for city in country.cities.all %}
{"id": "{{ city.id }}", "name": "{{ city.name }}"}{% if not forloop.last %},{% endif %}
{% endfor %}
]{% if not forloop.last %},{% endif %}
{% endfor %}
};
function updateCities(countrySelect, citySelect) {
const countryId = countrySelect.value;
const cities = citiesByCountry[countryId] || [];
const currentCityId = citySelect.value;
citySelect.innerHTML = '<option value="">---------</option>';
cities.forEach(city => {
const option = document.createElement('option');
option.value = city.id;
option.textContent = city.name;
if (city.id == currentCityId) option.selected = true;
citySelect.appendChild(option);
});
}
const originCountry = document.querySelector('select[name="origin_country"]');
const originCity = document.querySelector('select[name="origin_city"]');
const destCountry = document.querySelector('select[name="destination_country"]');
const destCity = document.querySelector('select[name="destination_city"]');
if (originCountry && originCity) {
originCountry.addEventListener('change', () => updateCities(originCountry, originCity));
updateCities(originCountry, originCity);
}
if (destCountry && destCity) {
destCountry.addEventListener('change', () => updateCities(destCountry, destCity));
updateCities(destCountry, destCity);
}
});
</script>
{% endblock %}

View File

@ -5,51 +5,111 @@
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card shadow border-0">
<div class="card-body p-5">
<h2 class="mb-4">{% trans "Post a New Shipment" %}</h2>
<p class="text-muted mb-4">{% trans "Enter shipment details to receive bids or send as an offer." %}</p>
{% if form.errors %}
<div class="alert alert-danger">
{% trans "Please correct the errors below." %}
{{ form.non_field_errors }}
</div>
{% endif %}
<form method="post">
<form method="post" id="shipmentForm">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">{% trans "Goods Description" %}</label>
<div class="mb-4">
<label class="form-label fw-bold">{% trans "Goods Description" %}</label>
{{ form.description }}
{{ form.description.errors }}
{% if form.description.errors %}<div class="text-danger small">{{ form.description.errors }}</div>{% endif %}
</div>
<div class="mb-3">
<label class="form-label">{% trans "Weight/Volume" %}</label>
{{ form.weight }}
{{ form.weight.errors }}
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label fw-bold">{% trans "Weight/Volume" %}</label>
{{ form.weight }}
{% if form.weight.errors %}<div class="text-danger small">{{ form.weight.errors }}</div>{% endif %}
</div>
<div class="col-md-6">
<label class="form-label fw-bold">{% trans "Requested Delivery Date" %}</label>
{{ form.delivery_date }}
{% if form.delivery_date.errors %}<div class="text-danger small">{{ form.delivery_date.errors }}</div>{% endif %}
</div>
</div>
<hr class="my-4">
<h5 class="mb-3 text-primary"><i class="fa-solid fa-map-marker-alt me-2"></i> {% trans "Route Details" %}</h5>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">{% trans "Origin" %}</label>
{{ form.origin }}
{{ form.origin.errors }}
<div class="col-md-6">
<div class="card bg-light border-0 p-3 mb-3">
<label class="form-label fw-bold">{% trans "Origin Country" %}</label>
{{ form.origin_country }}
<label class="form-label fw-bold mt-2">{% trans "Origin City" %}</label>
{{ form.origin_city }}
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">{% trans "Destination" %}</label>
{{ form.destination }}
{{ form.destination.errors }}
<div class="col-md-6">
<div class="card bg-light border-0 p-3 mb-3">
<label class="form-label fw-bold">{% trans "Destination Country" %}</label>
{{ form.destination_country }}
<label class="form-label fw-bold mt-2">{% trans "Destination City" %}</label>
{{ form.destination_city }}
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">{% trans "Requested Delivery Date" %}</label>
{{ form.delivery_date }}
{{ form.delivery_date.errors }}
</div>
<button type="submit" class="btn btn-primary w-100 py-3 mt-4">{% trans "Post Shipment" %}</button>
<button type="submit" class="btn btn-primary w-100 py-3 mt-4 fs-5">
<i class="fa-solid fa-paper-plane me-2"></i> {% trans "Post Shipment" %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const citiesByCountry = {
{% for country in form.fields.origin_country.queryset %}
"{{ country.id }}": [
{% for city in country.cities.all %}
{"id": "{{ city.id }}", "name": "{{ city.name }}"}{% if not forloop.last %},{% endif %}
{% endfor %}
]{% if not forloop.last %},{% endif %}
{% endfor %}
};
function updateCities(countrySelect, citySelect) {
const countryId = countrySelect.value;
const cities = citiesByCountry[countryId] || [];
// Save current selection if any
const currentCityId = citySelect.value;
citySelect.innerHTML = '<option value="">---------</option>';
cities.forEach(city => {
const option = document.createElement('option');
option.value = city.id;
option.textContent = city.name;
if (city.id == currentCityId) option.selected = true;
citySelect.appendChild(option);
});
}
const originCountry = document.querySelector('select[name="origin_country"]');
const originCity = document.querySelector('select[name="origin_city"]');
const destCountry = document.querySelector('select[name="destination_country"]');
const destCity = document.querySelector('select[name="destination_city"]');
if (originCountry && originCity) {
originCountry.addEventListener('change', () => updateCities(originCountry, originCity));
updateCities(originCountry, originCity);
}
if (destCountry && destCity) {
destCountry.addEventListener('change', () => updateCities(destCountry, destCity));
updateCities(destCountry, destCity);
}
});
</script>
{% endblock %}

View File

@ -5,39 +5,39 @@
<div class="container py-5">
<div class="row">
<div class="col-md-8">
<div class="card shadow-sm mb-4">
<div class="card shadow-sm mb-4 border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="mb-0">{{ shipment.origin }} <i class="fa-solid fa-arrow-right mx-2"></i> {{ shipment.destination }}</h2>
<h2 class="mb-0">{{ shipment.display_origin }} <i class="fa-solid fa-arrow-right mx-2 text-muted"></i> {{ shipment.display_destination }}</h2>
<span class="badge {% if shipment.status == 'OPEN' %}bg-primary{% elif shipment.status == 'IN_PROGRESS' %}bg-warning{% else %}bg-success{% endif %} fs-6">
{{ shipment.get_status_display }}
</span>
</div>
<hr>
<h5>{% trans "Shipment Details" %}</h5>
<p class="lead">{{ shipment.description }}</p>
<div class="row">
<h5 class="text-muted small text-uppercase fw-bold">{% trans "Shipment Details" %}</h5>
<p class="lead fw-normal">{{ shipment.description }}</p>
<div class="row mt-4">
<div class="col-6 mb-3">
<small class="text-muted d-block">{% trans "Weight" %}</small>
<strong>{{ shipment.weight }}</strong>
<small class="text-muted d-block text-uppercase small">{% trans "Weight / Volume" %}</small>
<strong class="fs-5">{{ shipment.weight }}</strong>
</div>
<div class="col-6 mb-3">
<small class="text-muted d-block">{% trans "Requested Delivery Date" %}</small>
<strong>{{ shipment.delivery_date }}</strong>
<small class="text-muted d-block text-uppercase small">{% trans "Requested Delivery Date" %}</small>
<strong class="fs-5">{{ shipment.delivery_date }}</strong>
</div>
</div>
</div>
</div>
<!-- Offers related to this shipment -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-white">
<h5 class="mb-0">{% trans "Offer Status" %}</h5>
<div class="card shadow-sm mb-4 border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0">{% trans "Offer Details" %}</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>{% trans "Truck" %}</th>
<th>{% trans "Amount" %}</th>
@ -48,8 +48,11 @@
<tbody>
{% for bid in bids %}
<tr>
<td>{{ bid.truck.display_truck_type }} ({{ bid.truck.plate_no }})</td>
<td>${{ bid.amount }}</td>
<td>
<div class="fw-bold">{{ bid.truck.display_truck_type }}</div>
<small class="text-muted">{{ bid.truck.plate_no }}</small>
</td>
<td class="fw-bold text-success">${{ bid.amount }}</td>
<td>
<span class="badge {% if bid.status == 'PENDING' %}bg-info{% elif bid.status == 'ACCEPTED' %}bg-success{% else %}bg-secondary{% endif %}">
{{ bid.get_status_display }}
@ -68,7 +71,10 @@
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-4 text-muted">{% trans "No offers for this shipment." %}</td>
<td colspan="4" class="text-center py-5 text-muted">
<i class="fa-solid fa-receipt fa-2x mb-3 opacity-25"></i>
<p>{% trans "No offers for this shipment yet." %}</p>
</td>
</tr>
{% endfor %}
</tbody>
@ -78,42 +84,56 @@
</div>
{% if shipment.status == 'IN_PROGRESS' %}
<div class="alert alert-success d-flex align-items-center p-4">
<i class="fa-solid fa-truck-moving fa-3x me-4 text-success opacity-50"></i>
<div>
<h5 class="alert-heading">{% trans "Shipment is IN PROGRESS" %}</h5>
<p class="mb-0">{% trans "Assigned Truck:" %} <strong>{{ shipment.assigned_truck.display_truck_type }} ({{ shipment.assigned_truck.plate_no }})</strong></p>
<div class="card border-0 bg-success bg-opacity-10 mb-4">
<div class="card-body d-flex align-items-center p-4">
<i class="fa-solid fa-truck-moving fa-3x me-4 text-success opacity-75"></i>
<div>
<h5 class="text-success mb-1">{% trans "Shipment is IN PROGRESS" %}</h5>
<p class="mb-0 text-dark">
{% trans "Assigned Truck:" %} <strong>{{ shipment.assigned_truck.display_truck_type }} ({{ shipment.assigned_truck.plate_no }})</strong>
</p>
</div>
</div>
</div>
{% endif %}
</div>
<div class="col-md-4">
<div class="card shadow-sm mb-4">
<div class="card shadow-sm mb-4 border-0">
<div class="card-body">
<h5>{% trans "Stakeholders" %}</h5>
<div class="mb-3">
<small class="text-muted d-block">{% trans "Shipper" %}</small>
<strong>{{ shipment.shipper.username }}</strong>
<h5 class="mb-4">{% trans "Contact Information" %}</h5>
<div class="mb-4">
<small class="text-muted d-block text-uppercase small">{% trans "Shipper" %}</small>
<div class="d-flex align-items-center mt-1">
<div class="bg-primary bg-opacity-10 rounded-circle p-2 me-2">
<i class="fa-solid fa-user text-primary"></i>
</div>
<strong class="fs-5">{{ shipment.shipper.username }}</strong>
</div>
</div>
{% if shipment.assigned_truck %}
<div class="mb-3">
<small class="text-muted d-block">{% trans "Truck Owner" %}</small>
<strong>{{ shipment.assigned_truck.owner.username }}</strong>
<div class="mb-4">
<small class="text-muted d-block text-uppercase small">{% trans "Truck Owner" %}</small>
<div class="d-flex align-items-center mt-1">
<div class="bg-success bg-opacity-10 rounded-circle p-2 me-2">
<i class="fa-solid fa-user text-success"></i>
</div>
<strong class="fs-5">{{ shipment.assigned_truck.owner.username }}</strong>
</div>
</div>
{% endif %}
</div>
</div>
{% if shipment.status == 'IN_PROGRESS' %}
<div class="d-grid gap-2">
<div class="d-grid gap-3">
{% if user == shipment.shipper %}
<a href="https://wa.me/{{ shipment.assigned_truck.owner.profile.full_phone_number }}" target="_blank" class="btn btn-success btn-lg">
<i class="fa-brands fa-whatsapp me-2"></i> {% trans "Message Driver" %}
<a href="https://wa.me/{{ shipment.assigned_truck.owner.profile.full_phone_number }}" target="_blank" class="btn btn-success btn-lg py-3">
<i class="fa-brands fa-whatsapp me-2"></i> {% trans "WhatsApp Driver" %}
</a>
{% elif user == shipment.assigned_truck.owner %}
<a href="https://wa.me/{{ shipment.shipper.profile.full_phone_number }}" target="_blank" class="btn btn-success btn-lg">
<i class="fa-brands fa-whatsapp me-2"></i> {% trans "Message Shipper" %}
<a href="https://wa.me/{{ shipment.shipper.profile.full_phone_number }}" target="_blank" class="btn btn-success btn-lg py-3">
<i class="fa-brands fa-whatsapp me-2"></i> {% trans "WhatsApp Shipper" %}
</a>
{% endif %}
</div>
@ -121,4 +141,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -8,9 +8,14 @@
<h2 class="mb-1">{% trans "Shipper Dashboard" %}</h2>
<p class="text-muted">{% trans "Manage your shipping offers and active shipments." %}</p>
</div>
<a href="{% url 'marketplace' %}" class="btn btn-primary btn-lg">
<i class="fa-solid fa-search me-2"></i> {% trans "Browse Trucks" %}
</a>
<div class="d-flex gap-2">
<a href="{% url 'post_shipment' %}" class="btn btn-success btn-lg">
<i class="fa-solid fa-plus me-2"></i> {% trans "Add A Bid" %}
</a>
<a href="{% url 'marketplace' %}" class="btn btn-primary btn-lg">
<i class="fa-solid fa-search me-2"></i> {% trans "Browse Trucks" %}
</a>
</div>
</div>
<!-- Active Offers -->
@ -39,9 +44,9 @@
<small class="text-muted">{{ bid.truck.plate_no }}</small>
</td>
<td>
<span>{{ bid.shipment.origin }}</span>
<span>{{ bid.shipment.display_origin }}</span>
<i class="fa-solid fa-arrow-right mx-1 text-muted small"></i>
<span>{{ bid.shipment.destination }}</span>
<span>{{ bid.shipment.display_destination }}</span>
</td>
<td class="fw-bold">${{ bid.amount }}</td>
<td>{{ bid.created_at|date:"d M Y" }}</td>
@ -93,10 +98,9 @@
</thead>
<tbody>
{% for shipment in shipments %}
{% if shipment.status != 'OPEN' or not shipment.bids.exists %}
<tr>
<td>{{ shipment.description|truncatechars:30 }}</td>
<td>{{ shipment.origin }} <i class="fa-solid fa-arrow-right mx-1 text-muted small"></i> {{ shipment.destination }}</td>
<td>{{ shipment.display_origin }} <i class="fa-solid fa-arrow-right mx-1 text-muted small"></i> {{ shipment.display_destination }}</td>
<td>{{ shipment.delivery_date }}</td>
<td>
<span class="badge {% if shipment.status == 'IN_PROGRESS' %}bg-warning{% elif shipment.status == 'COMPLETED' %}bg-success{% else %}bg-secondary{% endif %}">
@ -114,7 +118,6 @@
<a href="{% url 'shipment_detail' shipment.id %}" class="btn btn-sm btn-outline-primary">{% trans "Track" %}</a>
</td>
</tr>
{% endif %}
{% empty %}
<tr>
<td colspan="6" class="text-center py-4 text-muted">{% trans "No active shipments." %}</td>
@ -126,4 +129,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -38,7 +38,7 @@
{% for bid in bids %}
<tr>
<td>
<div class="fw-bold">{{ bid.shipment.origin }} <i class="fa-solid fa-arrow-right mx-1 text-muted small"></i> {{ bid.shipment.destination }}</div>
<div class="fw-bold">{{ bid.shipment.display_origin }} <i class="fa-solid fa-arrow-right mx-1 text-muted small"></i> {{ bid.shipment.display_destination }}</div>
<small class="text-muted">{{ bid.shipment.delivery_date }}</small>
</td>
<td>{{ bid.truck.plate_no }}</td>
@ -122,4 +122,4 @@
{% endfor %}
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -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')