change18
This commit is contained in:
parent
80485899cb
commit
459f38ada6
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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}))
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -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)
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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')
|
||||
Loading…
x
Reference in New Issue
Block a user