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 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.utils.translation import gettext_lazy as _
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -89,12 +89,19 @@ class TruckForm(forms.ModelForm):
class ShipmentForm(forms.ModelForm): class ShipmentForm(forms.ModelForm):
class Meta: class Meta:
model = Shipment model = Shipment
fields = ['description', 'weight', 'origin', 'destination', 'delivery_date'] fields = [
'description', 'weight',
'origin_country', 'origin_city',
'destination_country', 'destination_city',
'delivery_date'
]
widgets = { widgets = {
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'weight': forms.TextInput(attrs={'class': 'form-control'}), 'weight': forms.TextInput(attrs={'class': 'form-control'}),
'origin': forms.TextInput(attrs={'class': 'form-control'}), 'origin_country': forms.Select(attrs={'class': 'form-select location-selector', 'data-type': 'origin'}),
'destination': forms.TextInput(attrs={'class': 'form-control'}), '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'}), 'delivery_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
} }
@ -120,8 +127,12 @@ class BidForm(forms.ModelForm):
class ShipperOfferForm(forms.Form): class ShipperOfferForm(forms.Form):
description = forms.CharField(label=_('Goods Description'), widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3})) 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'})) 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'})) 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'})) 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): def __str__(self):
return f"{self.name} (+{self.code})" 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): class Profile(models.Model):
ROLE_CHOICES = ( ROLE_CHOICES = (
('SHIPPER', _('Shipper (Need Goods Moved)')), ('SHIPPER', _('Shipper (Need Goods Moved)')),
@ -123,8 +135,15 @@ class Shipment(models.Model):
shipper = models.ForeignKey(User, on_delete=models.CASCADE, related_name='shipments') shipper = models.ForeignKey(User, on_delete=models.CASCADE, related_name='shipments')
description = models.TextField(_('Goods Description')) description = models.TextField(_('Goods Description'))
weight = models.CharField(_('Weight/Volume'), max_length=100) 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')) delivery_date = models.DateField(_('Requested Delivery Date'))
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='OPEN') 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) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): 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): class Bid(models.Model):
STATUS_CHOICES = ( STATUS_CHOICES = (
@ -202,4 +233,4 @@ def sync_user_groups(sender, instance, **kwargs):
instance.user.groups.remove(*other_groups) instance.user.groups.remove(*other_groups)
# Add user to the correct group # Add user to the correct group
instance.user.groups.add(group) instance.user.groups.add(group)

View File

@ -4,7 +4,7 @@
{% block content %} {% block content %}
<div class="container py-5"> <div class="container py-5">
<div class="row justify-content-center"> <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 shadow-lg border-0">
<div class="card-body p-5"> <div class="card-body p-5">
<h2 class="mb-4 text-center">{% trans "Send Shipping Offer" %}</h2> <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> <h6 class="border-bottom pb-2 mb-3">{% trans "Shipment Details" %}</h6>
<div class="mb-3"> <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 }}
{{ form.description.errors }} {{ form.description.errors }}
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">{{ form.origin.label }}</label> <label class="form-label fw-bold">{{ form.weight.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>
{{ form.weight }} {{ form.weight }}
{{ form.weight.errors }} {{ form.weight.errors }}
</div> </div>
<div class="col-md-6"> <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 }}
{{ form.delivery_date.errors }} {{ form.delivery_date.errors }}
</div> </div>
</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> <h6 class="border-bottom pb-2 mb-3 mt-4">{% trans "Pricing & Terms" %}</h6>
<div class="mb-3"> <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"> <div class="input-group">
<span class="input-group-text">$</span> <span class="input-group-text">$</span>
{{ form.amount }} {{ form.amount }}
@ -77,7 +92,7 @@
</div> </div>
<div class="mb-4"> <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 }}
{{ form.comments.errors }} {{ form.comments.errors }}
</div> </div>
@ -94,4 +109,47 @@
</div> </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] || [];
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="container py-5">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-8"> <div class="col-md-8">
<div class="card shadow"> <div class="card shadow border-0">
<div class="card-body p-5"> <div class="card-body p-5">
<h2 class="mb-4">{% trans "Post a New Shipment" %}</h2> <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 %} <form method="post" id="shipmentForm">
<div class="alert alert-danger">
{% trans "Please correct the errors below." %}
{{ form.non_field_errors }}
</div>
{% endif %}
<form method="post">
{% csrf_token %} {% 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 }}
{{ form.description.errors }} {% if form.description.errors %}<div class="text-danger small">{{ form.description.errors }}</div>{% endif %}
</div> </div>
<div class="mb-3">
<label class="form-label">{% trans "Weight/Volume" %}</label> <div class="row mb-4">
{{ form.weight }} <div class="col-md-6">
{{ form.weight.errors }} <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> </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="row">
<div class="col-md-6 mb-3"> <div class="col-md-6">
<label class="form-label">{% trans "Origin" %}</label> <div class="card bg-light border-0 p-3 mb-3">
{{ form.origin }} <label class="form-label fw-bold">{% trans "Origin Country" %}</label>
{{ form.origin.errors }} {{ form.origin_country }}
<label class="form-label fw-bold mt-2">{% trans "Origin City" %}</label>
{{ form.origin_city }}
</div>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6">
<label class="form-label">{% trans "Destination" %}</label> <div class="card bg-light border-0 p-3 mb-3">
{{ form.destination }} <label class="form-label fw-bold">{% trans "Destination Country" %}</label>
{{ form.destination.errors }} {{ form.destination_country }}
<label class="form-label fw-bold mt-2">{% trans "Destination City" %}</label>
{{ form.destination_city }}
</div>
</div> </div>
</div> </div>
<div class="mb-3">
<label class="form-label">{% trans "Requested Delivery Date" %}</label> <button type="submit" class="btn btn-primary w-100 py-3 mt-4 fs-5">
{{ form.delivery_date }} <i class="fa-solid fa-paper-plane me-2"></i> {% trans "Post Shipment" %}
{{ form.delivery_date.errors }} </button>
</div>
<button type="submit" class="btn btn-primary w-100 py-3 mt-4">{% trans "Post Shipment" %}</button>
</form> </form>
</div> </div>
</div> </div>
</div> </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="container py-5">
<div class="row"> <div class="row">
<div class="col-md-8"> <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="card-body">
<div class="d-flex justify-content-between align-items-center mb-3"> <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"> <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 }} {{ shipment.get_status_display }}
</span> </span>
</div> </div>
<hr> <hr>
<h5>{% trans "Shipment Details" %}</h5> <h5 class="text-muted small text-uppercase fw-bold">{% trans "Shipment Details" %}</h5>
<p class="lead">{{ shipment.description }}</p> <p class="lead fw-normal">{{ shipment.description }}</p>
<div class="row"> <div class="row mt-4">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<small class="text-muted d-block">{% trans "Weight" %}</small> <small class="text-muted d-block text-uppercase small">{% trans "Weight / Volume" %}</small>
<strong>{{ shipment.weight }}</strong> <strong class="fs-5">{{ shipment.weight }}</strong>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<small class="text-muted d-block">{% trans "Requested Delivery Date" %}</small> <small class="text-muted d-block text-uppercase small">{% trans "Requested Delivery Date" %}</small>
<strong>{{ shipment.delivery_date }}</strong> <strong class="fs-5">{{ shipment.delivery_date }}</strong>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Offers related to this shipment --> <!-- Offers related to this shipment -->
<div class="card shadow-sm mb-4"> <div class="card shadow-sm mb-4 border-0">
<div class="card-header bg-white"> <div class="card-header bg-white py-3">
<h5 class="mb-0">{% trans "Offer Status" %}</h5> <h5 class="mb-0">{% trans "Offer Details" %}</h5>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover mb-0"> <table class="table table-hover align-middle mb-0">
<thead> <thead class="table-light">
<tr> <tr>
<th>{% trans "Truck" %}</th> <th>{% trans "Truck" %}</th>
<th>{% trans "Amount" %}</th> <th>{% trans "Amount" %}</th>
@ -48,8 +48,11 @@
<tbody> <tbody>
{% for bid in bids %} {% for bid in bids %}
<tr> <tr>
<td>{{ bid.truck.display_truck_type }} ({{ bid.truck.plate_no }})</td> <td>
<td>${{ bid.amount }}</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> <td>
<span class="badge {% if bid.status == 'PENDING' %}bg-info{% elif bid.status == 'ACCEPTED' %}bg-success{% else %}bg-secondary{% endif %}"> <span class="badge {% if bid.status == 'PENDING' %}bg-info{% elif bid.status == 'ACCEPTED' %}bg-success{% else %}bg-secondary{% endif %}">
{{ bid.get_status_display }} {{ bid.get_status_display }}
@ -68,7 +71,10 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -78,42 +84,56 @@
</div> </div>
{% if shipment.status == 'IN_PROGRESS' %} {% if shipment.status == 'IN_PROGRESS' %}
<div class="alert alert-success d-flex align-items-center p-4"> <div class="card border-0 bg-success bg-opacity-10 mb-4">
<i class="fa-solid fa-truck-moving fa-3x me-4 text-success opacity-50"></i> <div class="card-body d-flex align-items-center p-4">
<div> <i class="fa-solid fa-truck-moving fa-3x me-4 text-success opacity-75"></i>
<h5 class="alert-heading">{% trans "Shipment is IN PROGRESS" %}</h5> <div>
<p class="mb-0">{% trans "Assigned Truck:" %} <strong>{{ shipment.assigned_truck.display_truck_type }} ({{ shipment.assigned_truck.plate_no }})</strong></p> <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>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="col-md-4"> <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"> <div class="card-body">
<h5>{% trans "Stakeholders" %}</h5> <h5 class="mb-4">{% trans "Contact Information" %}</h5>
<div class="mb-3"> <div class="mb-4">
<small class="text-muted d-block">{% trans "Shipper" %}</small> <small class="text-muted d-block text-uppercase small">{% trans "Shipper" %}</small>
<strong>{{ shipment.shipper.username }}</strong> <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> </div>
{% if shipment.assigned_truck %} {% if shipment.assigned_truck %}
<div class="mb-3"> <div class="mb-4">
<small class="text-muted d-block">{% trans "Truck Owner" %}</small> <small class="text-muted d-block text-uppercase small">{% trans "Truck Owner" %}</small>
<strong>{{ shipment.assigned_truck.owner.username }}</strong> <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> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if shipment.status == 'IN_PROGRESS' %} {% if shipment.status == 'IN_PROGRESS' %}
<div class="d-grid gap-2"> <div class="d-grid gap-3">
{% if user == shipment.shipper %} {% 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"> <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 "Message Driver" %} <i class="fa-brands fa-whatsapp me-2"></i> {% trans "WhatsApp Driver" %}
</a> </a>
{% elif user == shipment.assigned_truck.owner %} {% 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"> <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 "Message Shipper" %} <i class="fa-brands fa-whatsapp me-2"></i> {% trans "WhatsApp Shipper" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
@ -121,4 +141,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

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

View File

@ -38,7 +38,7 @@
{% for bid in bids %} {% for bid in bids %}
<tr> <tr>
<td> <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> <small class="text-muted">{{ bid.shipment.delivery_date }}</small>
</td> </td>
<td>{{ bid.truck.plate_no }}</td> <td>{{ bid.truck.plate_no }}</td>
@ -122,4 +122,4 @@
{% endfor %} {% endfor %}
</div> </div>
</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.decorators import login_required
from django.contrib.auth import login, authenticate, logout from django.contrib.auth import login, authenticate, logout
from django.utils import timezone 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 .forms import TruckForm, ShipmentForm, BidForm, UserRegistrationForm, OTPVerifyForm, ShipperOfferForm
from django.contrib import messages from django.contrib import messages
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -77,7 +77,8 @@ def verify_otp_registration(request):
profile.save() profile.save()
login(request, user) 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!")) messages.success(request, _("Registration successful. Welcome!"))
return redirect('dashboard') return redirect('dashboard')
else: else:
@ -137,7 +138,8 @@ def verify_otp_login(request):
otp_record.save() otp_record.save()
login(request, user) 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!")) messages.success(request, _("Logged in successfully!"))
return redirect('dashboard') return redirect('dashboard')
else: else:
@ -253,7 +255,7 @@ def suspend_truck(request, truck_id):
@login_required @login_required
def post_shipment(request): 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': if request.user.profile.role != 'SHIPPER':
return redirect('dashboard') return redirect('dashboard')
@ -263,7 +265,7 @@ def post_shipment(request):
shipment = form.save(commit=False) shipment = form.save(commit=False)
shipment.shipper = request.user shipment.shipper = request.user
shipment.save() 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') return redirect('dashboard')
else: else:
messages.error(request, _("Please correct the errors in the form.")) messages.error(request, _("Please correct the errors in the form."))
@ -296,8 +298,10 @@ def place_bid(request, truck_id):
shipper=request.user, shipper=request.user,
description=form.cleaned_data['description'], description=form.cleaned_data['description'],
weight=form.cleaned_data['weight'], weight=form.cleaned_data['weight'],
origin=form.cleaned_data['origin'], origin_country=form.cleaned_data['origin_country'],
destination=form.cleaned_data['destination'], 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'], delivery_date=form.cleaned_data['delivery_date'],
status='OPEN' status='OPEN'
) )
@ -314,7 +318,7 @@ def place_bid(request, truck_id):
# Notify Truck Owner via WhatsApp # Notify Truck Owner via WhatsApp
owner_phone = getattr(truck.owner.profile, 'full_phone_number', None) owner_phone = getattr(truck.owner.profile, 'full_phone_number', None)
if owner_phone: 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) send_whatsapp_message(owner_phone, msg)
messages.success(request, _("Offer sent successfully!")) messages.success(request, _("Offer sent successfully!"))
@ -357,7 +361,7 @@ def accept_bid(request, bid_id):
# Notify Shipper via WhatsApp # Notify Shipper via WhatsApp
shipper_phone = getattr(bid.shipment.shipper.profile, 'full_phone_number', None) shipper_phone = getattr(bid.shipment.shipper.profile, 'full_phone_number', None)
if shipper_phone: 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) send_whatsapp_message(shipper_phone, msg)
messages.success(request, _("Offer accepted! Shipment is now in progress.")) messages.success(request, _("Offer accepted! Shipment is now in progress."))
@ -373,4 +377,4 @@ def reject_bid(request, bid_id):
bid.status = 'REJECTED' bid.status = 'REJECTED'
bid.save() bid.save()
messages.info(request, _("Offer rejected.")) messages.info(request, _("Offer rejected."))
return redirect('dashboard') return redirect('dashboard')