This commit is contained in:
Flatlogic Bot 2026-01-23 13:50:58 +00:00
parent ef5454da33
commit 80485899cb
11 changed files with 457 additions and 279 deletions

View File

@ -115,4 +115,13 @@ class BidForm(forms.ModelForm):
# Only allow bidding with approved trucks # Only allow bidding with approved trucks
self.fields['truck'].queryset = Truck.objects.filter(owner=user, is_approved=True) self.fields['truck'].queryset = Truck.objects.filter(owner=user, is_approved=True)
if not self.fields['truck'].queryset.exists(): if not self.fields['truck'].queryset.exists():
self.fields['truck'].help_text = _("You must have an approved truck to place a bid.") self.fields['truck'].help_text = _("You must have an approved truck to place a bid.")
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'}))
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}))

View File

@ -1,39 +1,50 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% load static %}
{% block content %} {% block content %}
<div class="container py-5"> <div class="container py-5">
<h2 class="mb-4">{% trans "Shipment Marketplace" %}</h2> <h2 class="mb-4">{% trans "Truck Marketplace" %}</h2>
<p class="text-muted mb-4">{% trans "Browse available trucks and send your shipping offers directly to truck owners." %}</p>
<div class="row"> <div class="row">
{% for shipment in shipments %} {% for truck in trucks %}
<div class="col-md-6 mb-4"> <div class="col-md-6 col-lg-4 mb-4">
<div class="card shadow-sm h-100 border-0"> <div class="card shadow-sm h-100 border-0 overflow-hidden">
{% if truck.truck_picture %}
<img src="{{ truck.truck_picture.url }}" class="card-img-top" alt="{{ truck.display_truck_type }}" style="height: 200px; object-fit: cover;">
{% else %}
<div class="bg-light d-flex align-items-center justify-content-center" style="height: 200px;">
<i class="fa-solid fa-truck fa-4x text-muted opacity-25"></i>
</div>
{% endif %}
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<span class="badge bg-primary">{% trans "Open for Bids" %}</span> <span class="badge bg-success">{% trans "Available" %}</span>
<small class="text-muted">{{ shipment.created_at|timesince }} {% trans "ago" %}</small> <small class="text-muted">{% trans "Plate" %}: {{ truck.plate_no }}</small>
</div> </div>
<h5 class="card-title">{{ shipment.origin }} <i class="fa-solid fa-arrow-right mx-2"></i> {{ shipment.destination }}</h5> <h5 class="card-title">{{ truck.display_truck_type }} - {{ truck.display_model }}</h5>
<p class="card-text text-muted">{{ shipment.description|truncatechars:100 }}</p> <div class="mb-3">
<div class="d-flex gap-4 mb-3"> <span class="badge bg-light text-dark">{% trans "Year" %}: {{ truck.year }}</span>
<div> <span class="badge bg-light text-dark">{% trans "Capacity" %}: {{ truck.display_load_capacity }}</span>
<small class="text-muted d-block">{% trans "Weight" %}</small>
<strong>{{ shipment.weight }}</strong>
</div>
<div>
<small class="text-muted d-block">{% trans "Delivery Date" %}</small>
<strong>{{ shipment.delivery_date }}</strong>
</div>
</div> </div>
<a href="{% url 'place_bid' shipment.id %}" class="btn btn-outline-primary w-100">{% trans "Place an Offer" %}</a> <p class="card-text">
<small class="text-muted d-block">{% trans "Owner" %}: {{ truck.owner.username }}</small>
</p>
<a href="{% url 'place_bid' truck.id %}" class="btn btn-primary w-100">
<i class="fa-solid fa-paper-plane me-2"></i>{% trans "Send Shipping Offer" %}
</a>
</div> </div>
</div> </div>
</div> </div>
{% empty %} {% empty %}
<div class="col-12 text-center py-5"> <div class="col-12 text-center py-5">
<p class="lead text-muted">{% trans "No shipments available at the moment." %}</p> <div class="py-5">
<i class="fa-solid fa-truck-slash fa-4x text-muted mb-3"></i>
<p class="lead text-muted">{% trans "No approved trucks are currently available." %}</p>
</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -4,54 +4,94 @@
{% 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-6"> <div class="col-md-8 col-lg-6">
<div class="card shadow"> <div class="card shadow-lg border-0">
<div class="card-body p-5"> <div class="card-body p-5">
<h2 class="mb-4">{% trans "Place an Offer" %}</h2> <h2 class="mb-4 text-center">{% trans "Send Shipping Offer" %}</h2>
<div class="alert alert-info mb-4">
<strong>{% trans "Shipment:" %}</strong> {{ shipment.origin }} to {{ shipment.destination }}<br> <div class="card bg-light border-0 mb-4">
<strong>{% trans "Goods:" %}</strong> {{ shipment.description }} <div class="card-body">
<h6 class="text-primary mb-2">{% trans "Target Truck" %}</h6>
<div class="d-flex align-items-center">
{% if truck.truck_picture %}
<img src="{{ truck.truck_picture.url }}" class="rounded me-3" style="width: 60px; height: 60px; object-fit: cover;">
{% else %}
<div class="bg-secondary bg-opacity-10 rounded me-3 d-flex align-items-center justify-content-center" style="width: 60px; height: 60px;">
<i class="fa-solid fa-truck text-muted"></i>
</div>
{% endif %}
<div>
<div class="fw-bold">{{ truck.display_truck_type }} - {{ truck.display_model }}</div>
<small class="text-muted">{% trans "Owner" %}: {{ truck.owner.username }} | {% trans "Plate" %}: {{ truck.plate_no }}</small>
</div>
</div>
</div>
</div> </div>
{% if form.errors %} <form method="post" novalidate>
<div class="alert alert-danger">
{% trans "Please correct the errors below." %}
{{ form.non_field_errors }}
</div>
{% endif %}
{% if form.fields.truck.queryset.exists %}
<form method="post">
{% csrf_token %} {% csrf_token %}
<h6 class="border-bottom pb-2 mb-3">{% trans "Shipment Details" %}</h6>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{% trans "Select Truck" %}</label> <label class="form-label">{{ form.description.label }}</label>
{{ form.truck }} {{ form.description }}
{{ form.truck.errors }} {{ form.description.errors }}
</div> </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>
{{ form.weight }}
{{ form.weight.errors }}
</div>
<div class="col-md-6">
<label class="form-label">{{ form.delivery_date.label }}</label>
{{ form.delivery_date }}
{{ form.delivery_date.errors }}
</div>
</div>
<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">{% trans "Your Offer Amount" %}</label> <label class="form-label">{{ 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 }}
</div> </div>
{{ form.amount.errors }} {{ form.amount.errors }}
</div> </div>
<div class="mb-3">
<label class="form-label">{% trans "Comments/Conditions" %}</label> <div class="mb-4">
<label class="form-label">{{ form.comments.label }}</label>
{{ form.comments }} {{ form.comments }}
{{ form.comments.errors }} {{ form.comments.errors }}
</div> </div>
<button type="submit" class="btn btn-primary w-100 py-3">{% trans "Submit Offer" %}</button>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg py-3">
<i class="fa-solid fa-paper-plane me-2"></i>{% trans "Send Offer to Truck Owner" %}
</button>
<a href="{% url 'marketplace' %}" class="btn btn-link text-muted">{% trans "Cancel" %}</a>
</div>
</form> </form>
{% else %}
<div class="text-center py-4">
<p class="text-danger">{% trans "You must register a truck before placing a bid." %}</p>
<a href="{% url 'truck_register' %}" class="btn btn-success">{% trans "Register Truck Now" %}</a>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -14,50 +14,61 @@
</span> </span>
</div> </div>
<hr> <hr>
<h5>{% trans "Details" %}</h5> <h5>{% trans "Shipment Details" %}</h5>
<p>{{ shipment.description }}</p> <p class="lead">{{ shipment.description }}</p>
<div class="row"> <div class="row">
<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">{% trans "Weight" %}</small>
<strong>{{ shipment.weight }}</strong> <strong>{{ shipment.weight }}</strong>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<small class="text-muted d-block">{% trans "Delivery Date" %}</small> <small class="text-muted d-block">{% trans "Requested Delivery Date" %}</small>
<strong>{{ shipment.delivery_date }}</strong> <strong>{{ shipment.delivery_date }}</strong>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% if user == shipment.shipper and shipment.status == 'OPEN' %} <!-- Offers related to this shipment -->
<div class="card shadow-sm"> <div class="card shadow-sm mb-4">
<div class="card-header bg-white"> <div class="card-header bg-white">
<h5 class="mb-0">{% trans "Received Bids" %}</h5> <h5 class="mb-0">{% trans "Offer Status" %}</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 mb-0">
<thead> <thead>
<tr> <tr>
<th>{% trans "Truck Owner" %}</th>
<th>{% trans "Truck" %}</th> <th>{% trans "Truck" %}</th>
<th>{% trans "Amount" %}</th> <th>{% trans "Amount" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Action" %}</th> <th>{% trans "Action" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for bid in bids %} {% for bid in bids %}
<tr> <tr>
<td>{{ bid.truck_owner.username }}</td> <td>{{ bid.truck.display_truck_type }} ({{ bid.truck.plate_no }})</td>
<td>{{ bid.truck.display_truck_type }}</td>
<td>${{ bid.amount }}</td> <td>${{ bid.amount }}</td>
<td> <td>
<a href="{% url 'accept_bid' bid.id %}" class="btn btn-sm btn-success">{% trans "Accept" %}</a> <span class="badge {% if bid.status == 'PENDING' %}bg-info{% elif bid.status == 'ACCEPTED' %}bg-success{% else %}bg-secondary{% endif %}">
{{ bid.get_status_display }}
</span>
</td>
<td>
{% if bid.status == 'PENDING' and user == bid.truck_owner %}
<div class="btn-group btn-group-sm">
<a href="{% url 'accept_bid' bid.id %}" class="btn btn-success">{% trans "Accept" %}</a>
<a href="{% url 'reject_bid' bid.id %}" class="btn btn-outline-danger">{% trans "Reject" %}</a>
</div>
{% else %}
<span class="text-muted small">{% trans "No action available" %}</span>
{% endif %}
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4" class="text-center py-4 text-muted">{% trans "No bids received yet." %}</td> <td colspan="4" class="text-center py-4 text-muted">{% trans "No offers for this shipment." %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -65,37 +76,49 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %}
{% if shipment.status == 'IN_PROGRESS' %} {% if shipment.status == 'IN_PROGRESS' %}
<div class="alert alert-success d-flex align-items-center"> <div class="alert alert-success d-flex align-items-center p-4">
<i class="fa-solid fa-truck-moving fa-2x me-3"></i> <i class="fa-solid fa-truck-moving fa-3x me-4 text-success opacity-50"></i>
<div> <div>
<strong>{% trans "Shipment in progress!" %}</strong><br> <h5 class="alert-heading">{% trans "Shipment is IN PROGRESS" %}</h5>
{% trans "Assigned Truck:" %} {{ shipment.assigned_truck.display_truck_type }} ({{ shipment.assigned_truck.plate_no }}) <p class="mb-0">{% trans "Assigned Truck:" %} <strong>{{ shipment.assigned_truck.display_truck_type }} ({{ shipment.assigned_truck.plate_no }})</strong></p>
</div> </div>
</div> </div>
<div class="mt-4">
<a href="https://wa.me/{{ shipment.assigned_truck.owner.profile.phone_number }}" class="btn btn-success btn-lg">
<i class="fa-brands fa-whatsapp me-2"></i> {% trans "Contact Driver on WhatsApp" %}
</a>
</div>
{% endif %} {% endif %}
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="card shadow-sm"> <div class="card shadow-sm mb-4">
<div class="card-body"> <div class="card-body">
<h5>{% trans "Contact Information" %}</h5> <h5>{% trans "Stakeholders" %}</h5>
<p> <div class="mb-3">
<strong>{% trans "Shipper:" %}</strong> {{ shipment.shipper.username }}<br> <small class="text-muted d-block">{% trans "Shipper" %}</small>
{% if shipment.status == 'IN_PROGRESS' %} <strong>{{ shipment.shipper.username }}</strong>
<strong>{% trans "Phone:" %}</strong> {{ shipment.shipper.profile.phone_number }} </div>
{% endif %} {% if shipment.assigned_truck %}
</p> <div class="mb-3">
<small class="text-muted d-block">{% trans "Truck Owner" %}</small>
<strong>{{ shipment.assigned_truck.owner.username }}</strong>
</div>
{% endif %}
</div> </div>
</div> </div>
{% if shipment.status == 'IN_PROGRESS' %}
<div class="d-grid gap-2">
{% 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>
{% 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>
{% endif %}
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -4,58 +4,126 @@
{% block content %} {% block content %}
<div class="container py-5"> <div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h2>{% trans "Shipper Dashboard" %}</h2> <div>
<a href="{% url 'post_shipment' %}" class="btn btn-success"> <h2 class="mb-1">{% trans "Shipper Dashboard" %}</h2>
<i class="fa-solid fa-plus me-2"></i> {% trans "Post New Shipment" %} <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> </a>
</div> </div>
<div class="row"> <!-- Active Offers -->
<div class="col-md-12"> <div class="card shadow-sm border-0 mb-5">
<div class="card shadow-sm"> <div class="card-header bg-white py-3">
<div class="card-header bg-white"> <h5 class="mb-0 text-primary">{% trans "My Shipping Offers" %}</h5>
<h5 class="mb-0">{% trans "My Shipments" %}</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 align-middle mb-0">
<table class="table table-hover mb-0"> <thead class="table-light">
<thead class="table-light"> <tr>
<tr> <th>{% trans "Truck" %}</th>
<th>{% trans "Description" %}</th> <th>{% trans "Route" %}</th>
<th>{% trans "Route" %}</th> <th>{% trans "Amount" %}</th>
<th>{% trans "Delivery Date" %}</th> <th>{% trans "Date Sent" %}</th>
<th>{% trans "Status" %}</th> <th>{% trans "Status" %}</th>
<th>{% trans "Bids" %}</th> <th>{% trans "Action" %}</th>
<th>{% trans "Action" %}</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> {% for bid in bids %}
{% for shipment in shipments %} <tr>
<tr> <td>
<td>{{ shipment.description|truncatechars:30 }}</td> <div><strong>{{ bid.truck.display_truck_type }}</strong></div>
<td>{{ shipment.origin }} <i class="fa-solid fa-arrow-right mx-1"></i> {{ shipment.destination }}</td> <small class="text-muted">{{ bid.truck.plate_no }}</small>
<td>{{ shipment.delivery_date }}</td> </td>
<td> <td>
<span class="badge {% if shipment.status == 'OPEN' %}bg-primary{% elif shipment.status == 'IN_PROGRESS' %}bg-warning{% else %}bg-success{% endif %}"> <span>{{ bid.shipment.origin }}</span>
{{ shipment.get_status_display }} <i class="fa-solid fa-arrow-right mx-1 text-muted small"></i>
</span> <span>{{ bid.shipment.destination }}</span>
</td> </td>
<td>{{ shipment.bids.count }}</td> <td class="fw-bold">${{ bid.amount }}</td>
<td> <td>{{ bid.created_at|date:"d M Y" }}</td>
<a href="{% url 'shipment_detail' shipment.id %}" class="btn btn-sm btn-outline-primary">{% trans "View Details" %}</a> <td>
</td> {% if bid.status == 'PENDING' %}
</tr> <span class="badge bg-info">{% trans "Pending" %}</span>
{% empty %} {% elif bid.status == 'ACCEPTED' %}
<tr> <span class="badge bg-success">{% trans "Accepted" %}</span>
<td colspan="6" class="text-center py-4">{% trans "No shipments posted yet." %}</td> {% else %}
</tr> <span class="badge bg-danger">{% trans "Rejected" %}</span>
{% endfor %} {% endif %}
</tbody> </td>
</table> <td>
</div> <a href="{% url 'shipment_detail' bid.shipment.id %}" class="btn btn-sm btn-outline-secondary">{% trans "Details" %}</a>
</div> </td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-5 text-muted">
<i class="fa-solid fa-paper-plane fa-2x mb-3 opacity-25"></i>
<p>{% trans "You haven't sent any offers yet." %}</p>
<a href="{% url 'marketplace' %}" class="btn btn-sm btn-primary">{% trans "Find a Truck" %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Active Shipments -->
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0">{% trans "Active Shipments" %}</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>{% trans "Description" %}</th>
<th>{% trans "Route" %}</th>
<th>{% trans "Delivery Date" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Assigned Truck" %}</th>
<th>{% trans "Action" %}</th>
</tr>
</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.delivery_date }}</td>
<td>
<span class="badge {% if shipment.status == 'IN_PROGRESS' %}bg-warning{% elif shipment.status == 'COMPLETED' %}bg-success{% else %}bg-secondary{% endif %}">
{{ shipment.get_status_display }}
</span>
</td>
<td>
{% if shipment.assigned_truck %}
{{ shipment.assigned_truck.plate_no }}
{% else %}
<span class="text-muted italic">{% trans "None" %}</span>
{% endif %}
</td>
<td>
<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>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -7,121 +7,119 @@
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<div> <div>
<h2 class="mb-1">{% trans "Truck Owner Dashboard" %}</h2> <h2 class="mb-1">{% trans "Truck Owner Dashboard" %}</h2>
<p class="text-muted">{% trans "Manage your fleet and active bids." %}</p> <p class="text-muted">{% trans "Manage your fleet and incoming shipping offers." %}</p>
</div> </div>
<div> <div>
<a href="{% url 'marketplace' %}" class="btn btn-primary me-2"> <a href="{% url 'truck_register' %}" class="btn btn-success btn-lg">
<i class="fa-solid fa-search me-2"></i> {% trans "Find Shipments" %} <i class="fa-solid fa-plus me-2"></i> {% trans "Add New Truck" %}
</a>
<a href="{% url 'truck_register' %}" class="btn btn-success">
<i class="fa-solid fa-plus me-2"></i> {% trans "Register Truck" %}
</a> </a>
</div> </div>
</div> </div>
<!-- Approved Trucks Section --> <!-- Incoming Offers Section -->
<h4 class="mb-3">{% trans "My Approved Trucks" %}</h4> <div class="card shadow-sm border-0 mb-5">
<div class="row mb-5"> <div class="card-header bg-white py-3">
{% if trucks %} <h5 class="mb-0 text-primary">{% trans "Received Shipping Offers" %}</h5>
{% for truck in trucks %} </div>
<div class="col-md-4 mb-4"> <div class="card-body p-0">
<div class="card h-100 shadow-sm border-0"> <div class="table-responsive">
{% if truck.truck_picture %} <table class="table table-hover align-middle mb-0">
<img src="{{ truck.truck_picture.url }}" class="card-img-top" alt="{{ truck.display_truck_type }}" style="height: 200px; object-fit: cover;"> <thead class="table-light">
{% else %} <tr>
<div class="bg-light d-flex align-items-center justify-content-center" style="height: 200px;"> <th>{% trans "Route" %}</th>
<i class="fa-solid fa-truck fa-4x text-secondary"></i> <th>{% trans "Truck" %}</th>
</div> <th>{% trans "Shipper" %}</th>
{% endif %} <th>{% trans "Offer Amount" %}</th>
<div class="card-body"> <th>{% trans "Status" %}</th>
<h5 class="card-title">{{ truck.display_truck_type }}</h5> <th>{% trans "Action" %}</th>
<p class="card-text mb-1"><strong>{% trans "Plate No:" %}</strong> {{ truck.plate_no }}</p> </tr>
<p class="card-text mb-1"><strong>{% trans "Model:" %}</strong> {{ truck.display_model }} ({{ truck.year }})</p> </thead>
<p class="card-text mb-1"><strong>{% trans "Capacity:" %}</strong> {{ truck.display_load_capacity }}</p> <tbody>
{% if truck.registration_expiry_date %} {% for bid in bids %}
<p class="card-text mb-1"><strong>{% trans "Expiry Date:" %}</strong> {{ truck.registration_expiry_date }}</p> <tr>
{% endif %} <td>
<a href="{% url 'edit_truck' truck.id %}" class="btn btn-outline-primary btn-sm mt-3"> <div class="fw-bold">{{ bid.shipment.origin }} <i class="fa-solid fa-arrow-right mx-1 text-muted small"></i> {{ bid.shipment.destination }}</div>
<i class="fa-solid fa-edit me-1"></i> {% trans "Edit Details" %} <small class="text-muted">{{ bid.shipment.delivery_date }}</small>
</a> </td>
</div> <td>{{ bid.truck.plate_no }}</td>
<div class="card-footer bg-white border-0"> <td>{{ bid.shipment.shipper.username }}</td>
<span class="badge bg-success w-100 py-2">{% trans "Approved" %}</span> <td class="fw-bold text-success">${{ bid.amount }}</td>
</div> <td>
</div> {% if bid.status == 'PENDING' %}
<span class="badge bg-info">{% trans "Pending" %}</span>
{% elif bid.status == 'ACCEPTED' %}
<span class="badge bg-success">{% trans "Accepted" %}</span>
{% else %}
<span class="badge bg-secondary">{% trans "Rejected" %}</span>
{% endif %}
</td>
<td>
{% if bid.status == 'PENDING' %}
<div class="btn-group btn-group-sm">
<a href="{% url 'accept_bid' bid.id %}" class="btn btn-success">{% trans "Accept" %}</a>
<a href="{% url 'reject_bid' bid.id %}" class="btn btn-outline-danger">{% trans "Reject" %}</a>
</div>
{% else %}
<a href="{% url 'shipment_detail' bid.shipment.id %}" class="btn btn-sm btn-outline-primary">{% trans "View Details" %}</a>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-5 text-muted">
<i class="fa-solid fa-inbox fa-2x mb-3 opacity-25"></i>
<p>{% trans "No offers received yet." %}</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endfor %} </div>
{% else %}
<div class="col-12 text-center py-4">
<p class="text-muted">{% trans "No approved trucks yet." %}</p>
</div>
{% endif %}
</div> </div>
<!-- Pending Trucks Section --> <!-- My Fleet -->
{% if pending_trucks %} <h4 class="mb-3">{% trans "My Fleet" %}</h4>
<h4 class="mb-3">{% trans "Pending Approval" %}</h4> <div class="row">
<div class="row mb-5"> {% for truck in trucks %}
{% for truck in pending_trucks %}
<div class="col-md-4 mb-4"> <div class="col-md-4 mb-4">
<div class="card h-100 shadow-sm border-0 opacity-75"> <div class="card h-100 shadow-sm border-0">
{% if truck.truck_picture %}
<img src="{{ truck.truck_picture.url }}" class="card-img-top" alt="{{ truck.display_truck_type }}" style="height: 200px; object-fit: cover;">
{% else %}
<div class="bg-light d-flex align-items-center justify-content-center" style="height: 200px;">
<i class="fa-solid fa-truck fa-4x text-secondary opacity-25"></i>
</div>
{% endif %}
<div class="card-body"> <div class="card-body">
<h5 class="card-title text-muted">{{ truck.display_truck_type }}</h5> <div class="d-flex justify-content-between align-items-start mb-2">
<p class="card-text mb-1"><strong>{% trans "Plate No:" %}</strong> {{ truck.plate_no }}</p> <h5 class="card-title mb-0">{{ truck.display_truck_type }}</h5>
<p class="card-text">{% trans "Submitted on:" %} {{ truck.created_at|date }}</p> <span class="badge bg-success">{% trans "Approved" %}</span>
<a href="{% url 'edit_truck' truck.id %}" class="btn btn-outline-secondary btn-sm mt-3"> </div>
<i class="fa-solid fa-edit me-1"></i> {% trans "Edit Details" %} <p class="card-text mb-1"><small class="text-muted">{% trans "Plate No:" %}</small> <strong>{{ truck.plate_no }}</strong></p>
<p class="card-text mb-1"><small class="text-muted">{% trans "Capacity:" %}</small> {{ truck.display_load_capacity }}</p>
<a href="{% url 'edit_truck' truck.id %}" class="btn btn-outline-primary btn-sm mt-3 w-100">
<i class="fa-solid fa-edit me-1"></i> {% trans "Edit Truck" %}
</a> </a>
</div> </div>
<div class="card-footer bg-white border-0"> </div>
<span class="badge bg-warning text-dark w-100 py-2">{% trans "Waiting for Admin Review" %}</span> </div>
{% endfor %}
{% for truck in pending_trucks %}
<div class="col-md-4 mb-4">
<div class="card h-100 shadow-sm border-0 bg-light">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title mb-0 text-muted">{{ truck.display_truck_type }}</h5>
<span class="badge bg-warning text-dark">{% trans "Pending" %}</span>
</div>
<p class="card-text mb-1"><small class="text-muted">{% trans "Plate No:" %}</small> <strong>{{ truck.plate_no }}</strong></p>
<p class="card-text mt-3"><small class="text-muted italic">{% trans "Waiting for admin approval..." %}</small></p>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
<div class="row">
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-header bg-white">
<h5 class="mb-0">{% trans "My Active Bids" %}</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="bg-light">
<tr>
<th>{% trans "Shipment" %}</th>
<th>{% trans "Amount" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Date" %}</th>
</tr>
</thead>
<tbody>
{% for bid in bids %}
<tr>
<td>{{ bid.shipment.origin }} - {{ bid.shipment.destination }}</td>
<td>${{ bid.amount }}</td>
<td>
<span class="badge {% if bid.status == 'PENDING' %}bg-warning text-dark{% elif bid.status == 'ACCEPTED' %}bg-success{% else %}bg-danger{% endif %}">
{{ bid.get_status_display }}
</span>
</td>
<td>{{ bid.created_at|date }}</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-4">{% trans "No bids placed yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -17,6 +17,7 @@ urlpatterns = [
path("shipment/post/", views.post_shipment, name="post_shipment"), path("shipment/post/", views.post_shipment, name="post_shipment"),
path("marketplace/", views.marketplace, name="marketplace"), path("marketplace/", views.marketplace, name="marketplace"),
path("shipment/<int:shipment_id>/", views.shipment_detail, name="shipment_detail"), path("shipment/<int:shipment_id>/", views.shipment_detail, name="shipment_detail"),
path("shipment/<int:shipment_id>/bid/", views.place_bid, name="place_bid"), path("truck/<int:truck_id>/offer/", views.place_bid, name="place_bid"),
path("bid/<int:bid_id>/accept/", views.accept_bid, name="accept_bid"), path("bid/<int:bid_id>/accept/", views.accept_bid, name="accept_bid"),
] path("bid/<int:bid_id>/reject/", views.reject_bid, name="reject_bid"),
]

View File

@ -3,7 +3,7 @@ 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
from .forms import TruckForm, ShipmentForm, BidForm, UserRegistrationForm, OTPVerifyForm 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 _
from django.db.models import Q from django.db.models import Q
@ -152,15 +152,21 @@ def dashboard(request):
profile, created = Profile.objects.get_or_create(user=request.user) profile, created = Profile.objects.get_or_create(user=request.user)
if profile.role == 'SHIPPER': if profile.role == 'SHIPPER':
my_shipments = Shipment.objects.filter(shipper=request.user).order_by('-created_at') my_shipments = Shipment.objects.filter(shipper=request.user).order_by('-created_at')
return render(request, 'core/shipper_dashboard.html', {'shipments': my_shipments}) # In the new flow, Shippers place bids.
my_bids = Bid.objects.filter(shipment__shipper=request.user).order_by('-created_at')
return render(request, 'core/shipper_dashboard.html', {
'shipments': my_shipments,
'bids': my_bids
})
elif profile.role == 'TRUCK_OWNER': elif profile.role == 'TRUCK_OWNER':
approved_trucks = Truck.objects.filter(owner=request.user, is_approved=True) approved_trucks = Truck.objects.filter(owner=request.user, is_approved=True)
pending_trucks = Truck.objects.filter(owner=request.user, is_approved=False) pending_trucks = Truck.objects.filter(owner=request.user, is_approved=False)
my_bids = Bid.objects.filter(truck_owner=request.user).order_by('-created_at') # Truck owners receive bids in the new flow
my_received_bids = Bid.objects.filter(truck_owner=request.user).order_by('-created_at')
return render(request, 'core/truck_owner_dashboard.html', { return render(request, 'core/truck_owner_dashboard.html', {
'trucks': approved_trucks, 'trucks': approved_trucks,
'pending_trucks': pending_trucks, 'pending_trucks': pending_trucks,
'bids': my_bids 'bids': my_received_bids
}) })
elif profile.role == 'ADMIN' or request.user.is_superuser: elif profile.role == 'ADMIN' or request.user.is_superuser:
pending_trucks = Truck.objects.filter(is_approved=False).order_by('-created_at') pending_trucks = Truck.objects.filter(is_approved=False).order_by('-created_at')
@ -175,7 +181,6 @@ def dashboard(request):
} }
return render(request, 'core/admin_dashboard.html', context) return render(request, 'core/admin_dashboard.html', context)
else: else:
# Fallback for undefined roles
return redirect('/') return redirect('/')
@login_required @login_required
@ -188,7 +193,7 @@ def truck_register(request):
if form.is_valid(): if form.is_valid():
truck = form.save(commit=False) truck = form.save(commit=False)
truck.owner = request.user truck.owner = request.user
truck.is_approved = False # Ensure it stays false on new reg truck.is_approved = False
truck.save() truck.save()
messages.success(request, _("Truck registered successfully! It will be visible after admin approval.")) messages.success(request, _("Truck registered successfully! It will be visible after admin approval."))
return redirect('dashboard') return redirect('dashboard')
@ -207,7 +212,7 @@ def edit_truck(request, truck_id):
form = TruckForm(request.POST, request.FILES, instance=truck) form = TruckForm(request.POST, request.FILES, instance=truck)
if form.is_valid(): if form.is_valid():
truck = form.save(commit=False) truck = form.save(commit=False)
truck.is_approved = False # Reset approval status on update truck.is_approved = False
truck.save() truck.save()
messages.success(request, _("Truck data updated successfully! It will be reviewed by admin again.")) messages.success(request, _("Truck data updated successfully! It will be reviewed by admin again."))
return redirect('dashboard') return redirect('dashboard')
@ -227,10 +232,9 @@ def approve_truck(request, truck_id):
truck.is_approved = True truck.is_approved = True
truck.save() truck.save()
# 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"Your truck ({truck.plate_no}) has been approved! You can now place bids on shipments." msg = f"Your truck ({truck.plate_no}) has been approved! You can now receive offers for shipments."
send_whatsapp_message(owner_phone, msg) send_whatsapp_message(owner_phone, msg)
messages.success(request, _("Truck approved successfully!")) messages.success(request, _("Truck approved successfully!"))
@ -249,6 +253,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."""
if request.user.profile.role != 'SHIPPER': if request.user.profile.role != 'SHIPPER':
return redirect('dashboard') return redirect('dashboard')
@ -269,80 +274,103 @@ def post_shipment(request):
@login_required @login_required
def marketplace(request): def marketplace(request):
if request.user.profile.role != 'TRUCK_OWNER': """Shippers browse available trucks here."""
if request.user.profile.role != 'SHIPPER':
return redirect('dashboard') return redirect('dashboard')
shipments = Shipment.objects.filter(status='OPEN').order_by('-created_at') trucks = Truck.objects.filter(is_approved=True).order_by('-created_at')
return render(request, 'core/marketplace.html', {'shipments': shipments}) return render(request, 'core/marketplace.html', {'trucks': trucks})
@login_required @login_required
def place_bid(request, shipment_id): def place_bid(request, truck_id):
shipment = get_object_or_404(Shipment, id=shipment_id) """Shipper makes an offer to a specific truck."""
if request.user.profile.role != 'TRUCK_OWNER': truck = get_object_or_404(Truck, id=truck_id, is_approved=True)
return redirect('dashboard') if request.user.profile.role != 'SHIPPER':
# Optional: Only allow bidding if user has at least one approved truck
if not Truck.objects.filter(owner=request.user, is_approved=True).exists():
messages.warning(request, _("You must have at least one approved truck to place a bid."))
return redirect('dashboard') return redirect('dashboard')
if request.method == 'POST': if request.method == 'POST':
form = BidForm(request.POST, user=request.user) form = ShipperOfferForm(request.POST)
if form.is_valid(): if form.is_valid():
bid = form.save(commit=False) # Create Shipment
bid.truck_owner = request.user shipment = Shipment.objects.create(
bid.shipment = shipment shipper=request.user,
bid.save() description=form.cleaned_data['description'],
weight=form.cleaned_data['weight'],
origin=form.cleaned_data['origin'],
destination=form.cleaned_data['destination'],
delivery_date=form.cleaned_data['delivery_date'],
status='OPEN'
)
# Create Bid (Offer)
bid = Bid.objects.create(
shipment=shipment,
truck_owner=truck.owner,
truck=truck,
amount=form.cleaned_data['amount'],
comments=form.cleaned_data.get('comments', ''),
status='PENDING'
)
# Notify Shipper via WhatsApp # Notify Truck Owner via WhatsApp
shipper_phone = getattr(shipment.shipper.profile, 'full_phone_number', None) owner_phone = getattr(truck.owner.profile, 'full_phone_number', None)
if shipper_phone: if owner_phone:
msg = f"New bid placed on your shipment from {shipment.origin} to {shipment.destination}! Amount: {bid.amount}" msg = f"New offer received for your truck ({truck.plate_no})! Route: {shipment.origin} to {shipment.destination}. Amount: {bid.amount}"
send_whatsapp_message(shipper_phone, msg) send_whatsapp_message(owner_phone, msg)
messages.success(request, _("Bid placed successfully!")) messages.success(request, _("Offer sent successfully!"))
return redirect('marketplace') return redirect('dashboard')
else: else:
messages.error(request, _("Error placing bid. Please check the form.")) messages.error(request, _("Error sending offer. Please check the form."))
else: else:
form = BidForm(user=request.user) form = ShipperOfferForm()
return render(request, 'core/place_bid.html', {'form': form, 'shipment': shipment}) return render(request, 'core/place_bid.html', {'form': form, 'truck': truck})
@login_required @login_required
def shipment_detail(request, shipment_id): def shipment_detail(request, shipment_id):
shipment = get_object_or_404(Shipment, id=shipment_id) shipment = get_object_or_404(Shipment, id=shipment_id)
# Security: check if user is shipper or a truck owner who bid # Security: check if user is shipper, truck owner of a bid, or admin
if shipment.shipper != request.user and not Bid.objects.filter(shipment=shipment, truck_owner=request.user).exists(): is_involved = Bid.objects.filter(shipment=shipment, truck_owner=request.user).exists() or shipment.shipper == request.user
if request.user.profile.role != 'ADMIN' and not request.user.is_superuser: if not is_involved and not (request.user.profile.role == 'ADMIN' or request.user.is_superuser):
return redirect('dashboard') return redirect('dashboard')
bids = shipment.bids.all() bids = shipment.bids.all()
return render(request, 'core/shipment_detail.html', {'shipment': shipment, 'bids': bids}) return render(request, 'core/shipment_detail.html', {'shipment': shipment, 'bids': bids})
@login_required @login_required
def accept_bid(request, bid_id): def accept_bid(request, bid_id):
"""Truck owner accepts an offer from a shipper."""
bid = get_object_or_404(Bid, id=bid_id) bid = get_object_or_404(Bid, id=bid_id)
if bid.shipment.shipper != request.user: if bid.truck_owner != request.user:
messages.error(request, _("You are not authorized to accept this offer."))
return redirect('dashboard') return redirect('dashboard')
# Accept this bid # Accept this bid
bid.status = 'ACCEPTED' bid.status = 'ACCEPTED'
bid.save() bid.save()
# Reject others
bid.shipment.bids.exclude(id=bid_id).update(status='REJECTED')
# Update shipment # Update shipment
bid.shipment.status = 'IN_PROGRESS' bid.shipment.status = 'IN_PROGRESS'
bid.shipment.assigned_truck = bid.truck bid.shipment.assigned_truck = bid.truck
bid.shipment.save() bid.shipment.save()
# Notify Truck Owner via WhatsApp # Notify Shipper via WhatsApp
owner_phone = getattr(bid.truck_owner.profile, 'full_phone_number', None) shipper_phone = getattr(bid.shipment.shipper.profile, 'full_phone_number', None)
if owner_phone: if shipper_phone:
msg = f"Congratulations! Your bid for the shipment from {bid.shipment.origin} to {bid.shipment.destination} has been accepted." msg = f"Your offer for truck {bid.truck.plate_no} ({bid.shipment.origin} to {bid.shipment.destination}) has been accepted!"
send_whatsapp_message(owner_phone, msg) send_whatsapp_message(shipper_phone, msg)
messages.success(request, _("Bid accepted! Shipment is now in progress.")) messages.success(request, _("Offer accepted! Shipment is now in progress."))
return redirect('shipment_detail', shipment_id=bid.shipment.id) return redirect('dashboard')
@login_required
def reject_bid(request, bid_id):
"""Truck owner rejects an offer."""
bid = get_object_or_404(Bid, id=bid_id)
if bid.truck_owner != request.user:
return redirect('dashboard')
bid.status = 'REJECTED'
bid.save()
messages.info(request, _("Offer rejected."))
return redirect('dashboard')