Autosave: 20260126-073515
This commit is contained in:
parent
88187c1cc8
commit
1e9f216ade
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -8,7 +8,6 @@ from django.shortcuts import render
|
||||
from django.utils.html import format_html
|
||||
from django.contrib import messages
|
||||
from .whatsapp_utils import send_whatsapp_message_detailed
|
||||
from django.core.mail import send_html_email
|
||||
from django.conf import settings
|
||||
from .mail import send_html_email
|
||||
import logging
|
||||
|
||||
18
core/migrations/0018_alter_otpverification_purpose.py
Normal file
18
core/migrations/0018_alter_otpverification_purpose.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-26 06:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0017_driverrating'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='otpverification',
|
||||
name='purpose',
|
||||
field=models.CharField(choices=[('profile_update', 'Profile Update'), ('password_reset', 'Password Reset'), ('registration', 'Registration'), ('login', 'Login')], default='profile_update', max_length=20),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -213,6 +213,7 @@ class OTPVerification(models.Model):
|
||||
('profile_update', _('Profile Update')),
|
||||
('password_reset', _('Password Reset')),
|
||||
('registration', _('Registration')),
|
||||
('login', _('Login')),
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
code = models.CharField(max_length=6)
|
||||
@ -269,4 +270,4 @@ class DriverRating(models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Driver Rating')
|
||||
verbose_name_plural = _('Driver Ratings')
|
||||
verbose_name_plural = _('Driver Ratings')
|
||||
@ -180,7 +180,7 @@
|
||||
<span class="stat-title">{% trans "Total Revenue" %}</span>
|
||||
<div class="stat-icon icon-revenue">💰</div>
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.total_revenue|floatform:2 }} <span style="font-size: 1rem; color: #6b7280;">OMR</span></div>
|
||||
<div class="stat-value">{{ stats.total_revenue|floatformat:3 }} <span style="font-size: 1rem; color: #6b7280;">OMR</span></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
|
||||
@ -245,5 +245,38 @@
|
||||
|
||||
<!-- Bootstrap Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Chat Widget -->
|
||||
<button id="masar-chat-toggle" class="d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-chat-dots-fill fs-4"></i>
|
||||
</button>
|
||||
<div id="masar-chat-widget" class="d-none bg-white rounded-4 shadow overflow-hidden">
|
||||
<div class="bg-dark text-white p-3 d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-robot me-2 fs-5"></i>
|
||||
<h6 class="mb-0 fw-bold">MasarX AI</h6>
|
||||
</div>
|
||||
<button id="masar-chat-close" class="btn btn-sm btn-link text-white p-0">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="masar-chat-messages" class="flex-grow-1 p-3 bg-light overflow-auto">
|
||||
<div class="d-flex mb-3 justify-content-start">
|
||||
<div class="p-3 rounded-3 shadow-sm bg-light text-dark" style="max-width: 80%;">
|
||||
{% trans "Hello! How can I help you with your shipments today?" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 border-top bg-white">
|
||||
<form id="masar-chat-form" class="d-flex">
|
||||
<input type="text" id="masar-chat-input" class="form-control me-2" placeholder="{% trans "Type a message..." %}" autocomplete="off">
|
||||
<button type="submit" class="btn btn-masarx-primary px-3">
|
||||
<i class="bi bi-send-fill"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{% static "js/chat.js" %}?v={{ deployment_timestamp }}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -20,37 +20,132 @@
|
||||
<div class="tab-content" id="pills-tabContent">
|
||||
<!-- Available Shipments -->
|
||||
<div class="tab-pane fade show active" id="pills-available" role="tabpanel">
|
||||
{% if available_parcels %}
|
||||
<div class="row g-4">
|
||||
{% for parcel in available_parcels %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card border-0 shadow-sm h-100" style="border-radius: 15px;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ parcel.description|truncatechars:30 }}</h5>
|
||||
<p class="card-text mb-1 small"><strong>{% trans "Pickup" %}:</strong> {{ parcel.pickup_address }}</p>
|
||||
<p class="card-text mb-3 small"><strong>{% trans "Delivery" %}:</strong> {{ parcel.delivery_address }}</p>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-muted small"><strong>{% trans "Weight" %}:</strong> {{ parcel.weight }} kg</span>
|
||||
</div>
|
||||
|
||||
<!-- Bid/Price Highlight -->
|
||||
<div class="bg-light p-2 rounded text-center mb-3 border border-primary border-opacity-25">
|
||||
<small class="text-uppercase text-muted fw-bold" style="font-size: 0.7rem;">{% trans "Shipper's Offer (Bid)" %}</small>
|
||||
<div class="text-primary fw-bold fs-4">{{ parcel.price }} <span class="fs-6">OMR</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="mb-0 text-muted">{% trans "Browse Shipments" %}</h5>
|
||||
<div class="btn-group" role="group" aria-label="View toggle">
|
||||
<button type="button" class="btn btn-outline-primary active" id="btn-grid-view">
|
||||
<i class="bi bi-grid"></i> {% trans "Grid" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="btn-list-view">
|
||||
<i class="bi bi-list"></i> {% trans "List" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{% url 'accept_parcel' parcel.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-masarx-primary w-100">{% trans "Accept Shipment" %}</button>
|
||||
</form>
|
||||
{% if available_parcels %}
|
||||
|
||||
<!-- Grid View -->
|
||||
<div id="view-grid" class="row g-4">
|
||||
{% for parcel in available_parcels %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card border-0 shadow-sm h-100" style="border-radius: 15px;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ parcel.description|truncatechars:30 }}</h5>
|
||||
<p class="card-text mb-1 small"><strong>{% trans "Pickup" %}:</strong> {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}</p>
|
||||
<p class="card-text mb-3 small"><strong>{% trans "Delivery" %}:</strong> {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}</p>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-muted small"><strong>{% trans "Weight" %}:</strong> {{ parcel.weight }} kg</span>
|
||||
</div>
|
||||
|
||||
<!-- Bid/Price Highlight -->
|
||||
<div class="bg-light p-2 rounded text-center mb-3 border border-primary border-opacity-25">
|
||||
<small class="text-uppercase text-muted fw-bold" style="font-size: 0.7rem;">{% trans "Shipper's Offer (Bid)" %}</small>
|
||||
<div class="text-primary fw-bold fs-4">{{ parcel.price }} <span class="fs-6">OMR</span></div>
|
||||
</div>
|
||||
|
||||
<form action="{% url 'accept_parcel' parcel.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-masarx-primary w-100">{% trans "Accept Shipment" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- List View (Initially Hidden) -->
|
||||
<div id="view-list" class="d-none">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
{% for parcel in available_parcels %}
|
||||
<div class="card border-0 shadow-sm rounded-3">
|
||||
<div class="card-body p-3">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8 mb-3 mb-md-0">
|
||||
<h5 class="card-title mb-2">{{ parcel.description|truncatechars:80 }}</h5>
|
||||
<div class="d-flex flex-wrap gap-3 text-muted small">
|
||||
<span class="d-flex align-items-center gap-1">
|
||||
<i class="bi bi-geo-alt-fill text-primary"></i>
|
||||
<strong>{% trans "From" %}:</strong> {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}
|
||||
</span>
|
||||
<span class="d-flex align-items-center gap-1">
|
||||
<i class="bi bi-geo-alt-fill text-danger"></i>
|
||||
<strong>{% trans "To" %}:</strong> {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}
|
||||
</span>
|
||||
<span class="d-flex align-items-center gap-1">
|
||||
<i class="bi bi-box-seam"></i> {{ parcel.weight }} kg
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end">
|
||||
<div class="d-flex flex-column align-items-md-end gap-2">
|
||||
<div class="text-primary fw-bold fs-5">{{ parcel.price }} OMR</div>
|
||||
<form action="{% url 'accept_parcel' parcel.id %}" method="POST" class="w-100 w-md-auto">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-masarx-primary btn-sm w-100">{% trans "Accept" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if available_parcels.has_other_pages %}
|
||||
<nav aria-label="Page navigation" class="mt-5">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if available_parcels.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ available_parcels.previous_page_number }}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">«</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for i in available_parcels.paginator.page_range %}
|
||||
{% if available_parcels.number == i %}
|
||||
<li class="page-item active"><span class="page-link">{{ i }}</span></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if available_parcels.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ available_parcels.next_page_number }}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">»</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<p class="text-center py-5">{% trans "No shipments available at the moment." %}</p>
|
||||
<p class="text-center py-5">{% trans "No shipments available at the moment." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -67,7 +162,7 @@
|
||||
<span class="badge bg-primary">{{ parcel.get_status_display }}</span>
|
||||
</div>
|
||||
<h5 class="card-title">{{ parcel.description|truncatechars:30 }}</h5>
|
||||
<p class="card-text mb-1 small"><strong>{% trans "To" %}:</strong> {{ parcel.delivery_address }}</p>
|
||||
<p class="card-text mb-1 small"><strong>{% trans "To" %}:</strong> {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}</p>
|
||||
<p class="card-text mb-3 small"><strong>{% trans "Receiver" %}:</strong> {{ parcel.receiver_name }}</p>
|
||||
|
||||
<form action="{% url 'update_status' parcel.id %}" method="POST" class="d-flex gap-2">
|
||||
@ -113,8 +208,8 @@
|
||||
<tr>
|
||||
<td class="ps-4">{{ parcel.created_at|date:"Y-m-d" }}</td>
|
||||
<td><span class="badge bg-light text-dark">#{{ parcel.tracking_number }}</span></td>
|
||||
<td>{{ parcel.pickup_city.name|default:parcel.pickup_address }}</td>
|
||||
<td>{{ parcel.delivery_city.name|default:parcel.delivery_address }}</td>
|
||||
<td>{{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}</td>
|
||||
<td>{{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}</td>
|
||||
<td>{{ parcel.price }} OMR</td>
|
||||
<td>
|
||||
<span class="badge {% if parcel.status == 'delivered' %}bg-success{% else %}bg-danger{% endif %}">
|
||||
@ -136,4 +231,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const gridViewBtn = document.getElementById('btn-grid-view');
|
||||
const listViewBtn = document.getElementById('btn-list-view');
|
||||
const gridView = document.getElementById('view-grid');
|
||||
const listView = document.getElementById('view-list');
|
||||
|
||||
function setView(view) {
|
||||
if (view === 'list') {
|
||||
if (gridView) gridView.classList.add('d-none');
|
||||
if (listView) listView.classList.remove('d-none');
|
||||
if (listViewBtn) listViewBtn.classList.add('active');
|
||||
if (gridViewBtn) gridViewBtn.classList.remove('active');
|
||||
localStorage.setItem('driverDashboardView', 'list');
|
||||
} else {
|
||||
if (listView) listView.classList.add('d-none');
|
||||
if (gridView) gridView.classList.remove('d-none');
|
||||
if (gridViewBtn) gridViewBtn.classList.add('active');
|
||||
if (listViewBtn) listViewBtn.classList.remove('active');
|
||||
localStorage.setItem('driverDashboardView', 'grid');
|
||||
}
|
||||
}
|
||||
|
||||
// Check local storage or default to grid
|
||||
const savedView = localStorage.getItem('driverDashboardView');
|
||||
if (savedView) {
|
||||
setView(savedView);
|
||||
}
|
||||
|
||||
// Bind events
|
||||
if (gridViewBtn) gridViewBtn.addEventListener('click', () => setView('grid'));
|
||||
if (listViewBtn) listViewBtn.addEventListener('click', () => setView('list'));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -86,6 +86,91 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Top Performers Section -->
|
||||
<section class="py-5 bg-white border-top">
|
||||
<div class="container py-5">
|
||||
<div class="row g-5">
|
||||
<!-- Top Drivers -->
|
||||
<div class="col-lg-6">
|
||||
<h3 class="mb-4 text-center">
|
||||
<i class="bi bi-trophy text-warning me-2"></i>{% trans "Top Rated Drivers" %}
|
||||
</h3>
|
||||
<div class="list-group list-group-flush shadow-sm rounded-3 overflow-hidden">
|
||||
{% for driver in top_drivers %}
|
||||
<div class="list-group-item d-flex align-items-center p-3">
|
||||
<div class="me-3 position-relative">
|
||||
{% if driver.profile_picture %}
|
||||
<img src="{{ driver.profile_picture.url }}" class="rounded-circle" width="50" height="50" style="object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center text-muted fw-bold" style="width: 50px; height: 50px;">
|
||||
{{ driver.user.first_name|first|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning text-dark border border-white">
|
||||
{{ forloop.counter }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">{{ driver.user.first_name }} {{ driver.user.last_name|first }}.</h6>
|
||||
<small class="text-muted">{% trans "Driver" %}</small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="text-warning small mb-1">
|
||||
<i class="bi bi-star-fill"></i> {{ driver.avg_rating|floatformat:1 }}
|
||||
</div>
|
||||
<small class="text-muted">({{ driver.rating_count }} {% trans "reviews" %})</small>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="list-group-item p-4 text-center text-muted">
|
||||
{% trans "No ratings yet. Be the first!" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Shippers -->
|
||||
<div class="col-lg-6">
|
||||
<h3 class="mb-4 text-center">
|
||||
<i class="bi bi-box-seam text-primary me-2"></i>{% trans "Top Shippers" %}
|
||||
</h3>
|
||||
<div class="list-group list-group-flush shadow-sm rounded-3 overflow-hidden">
|
||||
{% for shipper in top_shippers %}
|
||||
<div class="list-group-item d-flex align-items-center p-3">
|
||||
<div class="me-3 position-relative">
|
||||
{% if shipper.profile_picture %}
|
||||
<img src="{{ shipper.profile_picture.url }}" class="rounded-circle" width="50" height="50" style="object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center text-muted fw-bold" style="width: 50px; height: 50px;">
|
||||
{{ shipper.user.first_name|first|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-primary text-white border border-white">
|
||||
{{ forloop.counter }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">{{ shipper.user.first_name }} {{ shipper.user.last_name|first }}.</h6>
|
||||
<small class="text-muted">{% trans "Shipper" %}</small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="fw-bold text-primary mb-1">
|
||||
{{ shipper.shipment_count }}
|
||||
</div>
|
||||
<small class="text-muted">{% trans "Shipments" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="list-group-item p-4 text-center text-muted">
|
||||
{% trans "No shipments yet. Start shipping now!" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Testimonials Section -->
|
||||
{% if testimonials %}
|
||||
<section class="py-5 bg-light">
|
||||
|
||||
@ -22,34 +22,81 @@
|
||||
<p class="text-muted small">{% trans "Please login to your account" %}</p>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.errors %}
|
||||
<div class="text-danger small mt-1">{{ field.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="form-check">
|
||||
<!-- Optional: Remember Me logic could go here later -->
|
||||
</div>
|
||||
<a href="{% url 'password_reset' %}" class="text-decoration-none text-muted small hover-orange">
|
||||
{% trans "Forgot Password?" %}
|
||||
</a>
|
||||
<!-- Login Method Tabs -->
|
||||
<ul class="nav nav-pills nav-fill mb-4 p-1 bg-light rounded-3" id="loginTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active fw-bold rounded-3" id="password-tab" data-bs-toggle="tab" data-bs-target="#password-login" type="button" role="tab" aria-controls="password-login" aria-selected="true">{% trans "Password" %}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-bold rounded-3" id="otp-tab" data-bs-toggle="tab" data-bs-target="#otp-login" type="button" role="tab" aria-controls="otp-login" aria-selected="false">{% trans "OTP Login" %}</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="loginTabContent">
|
||||
<!-- Password Login Tab -->
|
||||
<div class="tab-pane fade show active" id="password-login" role="tabpanel" aria-labelledby="password-tab">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.errors %}
|
||||
<div class="text-danger small mt-1">{{ field.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="form-check">
|
||||
<!-- Optional: Remember Me logic could go here later -->
|
||||
</div>
|
||||
<a href="{% url 'password_reset' %}" class="text-decoration-none text-muted small hover-orange">
|
||||
{% trans "Forgot Password?" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3">{% trans "Login" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3">{% trans "Login" %}</button>
|
||||
|
||||
<div class="text-center">
|
||||
<span class="text-muted small">{% trans "Don't have an account?" %}</span>
|
||||
<a href="{% url 'register' %}" class="text-masarx-orange text-decoration-none fw-bold ms-1">{% trans "Register" %}</a>
|
||||
<!-- OTP Login Tab -->
|
||||
<div class="tab-pane fade" id="otp-login" role="tabpanel" aria-labelledby="otp-tab">
|
||||
<div id="otp-step-1">
|
||||
<div class="mb-3">
|
||||
<label for="otp-identifier" class="form-label fw-medium">{% trans "Email or Phone Number" %}</label>
|
||||
<input type="text" class="form-control" id="otp-identifier" placeholder="{% trans 'e.g. user@example.com or 96812345678' %}">
|
||||
<div id="otp-identifier-error" class="text-danger small mt-1 d-none"></div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3" id="btn-send-otp">
|
||||
<span id="btn-send-otp-text">{% trans "Send OTP" %}</span>
|
||||
<span id="btn-send-otp-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="otp-step-2" class="d-none">
|
||||
<div class="alert alert-info py-2 small" id="otp-sent-msg"></div>
|
||||
<div class="mb-3">
|
||||
<label for="otp-code" class="form-label fw-medium">{% trans "Enter OTP Code" %}</label>
|
||||
<input type="text" class="form-control text-center letter-spacing-2" id="otp-code" placeholder="123456" maxlength="6">
|
||||
<div id="otp-code-error" class="text-danger small mt-1 d-none"></div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3" id="btn-verify-otp">
|
||||
<span id="btn-verify-otp-text">{% trans "Verify & Login" %}</span>
|
||||
<span id="btn-verify-otp-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="text-center">
|
||||
<button type="button" class="btn btn-link text-muted small text-decoration-none" id="btn-back-otp">{% trans "Back" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="text-center border-top pt-3 mt-2">
|
||||
<span class="text-muted small">{% trans "Don't have an account?" %}</span>
|
||||
<a href="{% url 'register' %}" class="text-masarx-orange text-decoration-none fw-bold ms-1">{% trans "Register" %}</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -58,6 +105,15 @@
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.nav-pills .nav-link {
|
||||
color: #6c757d;
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
.nav-pills .nav-link.active {
|
||||
background-color: var(--accent-orange);
|
||||
color: white;
|
||||
}
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
padding: 12px 15px;
|
||||
@ -85,5 +141,130 @@
|
||||
.hover-orange:hover {
|
||||
color: var(--accent-orange) !important;
|
||||
}
|
||||
.letter-spacing-2 {
|
||||
letter-spacing: 4px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const btnSendOtp = document.getElementById('btn-send-otp');
|
||||
const btnVerifyOtp = document.getElementById('btn-verify-otp');
|
||||
const btnBackOtp = document.getElementById('btn-back-otp');
|
||||
const otpStep1 = document.getElementById('otp-step-1');
|
||||
const otpStep2 = document.getElementById('otp-step-2');
|
||||
const inputIdentifier = document.getElementById('otp-identifier');
|
||||
const inputCode = document.getElementById('otp-code');
|
||||
const errorIdentifier = document.getElementById('otp-identifier-error');
|
||||
const errorCode = document.getElementById('otp-code-error');
|
||||
const msgSent = document.getElementById('otp-sent-msg');
|
||||
|
||||
let userId = null;
|
||||
|
||||
function showLoading(btnId, show) {
|
||||
const btn = document.getElementById(btnId);
|
||||
const textSpan = document.getElementById(btnId + '-text');
|
||||
const spinnerSpan = document.getElementById(btnId + '-spinner');
|
||||
|
||||
if (show) {
|
||||
btn.disabled = true;
|
||||
textSpan.classList.add('d-none');
|
||||
spinnerSpan.classList.remove('d-none');
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
textSpan.classList.remove('d-none');
|
||||
spinnerSpan.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function showError(element, message) {
|
||||
element.innerText = message;
|
||||
element.classList.remove('d-none');
|
||||
}
|
||||
|
||||
function clearErrors() {
|
||||
errorIdentifier.classList.add('d-none');
|
||||
errorCode.classList.add('d-none');
|
||||
}
|
||||
|
||||
btnSendOtp.addEventListener('click', function() {
|
||||
const identifier = inputIdentifier.value.trim();
|
||||
if (!identifier) {
|
||||
showError(errorIdentifier, "{% trans 'Please enter your email or phone number.' %}");
|
||||
return;
|
||||
}
|
||||
|
||||
clearErrors();
|
||||
showLoading('btn-send-otp', true);
|
||||
|
||||
fetch("{% url 'request_login_otp' %}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
body: 'identifier=' + encodeURIComponent(identifier)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showLoading('btn-send-otp', false);
|
||||
if (data.success) {
|
||||
userId = data.user_id;
|
||||
msgSent.innerText = data.message;
|
||||
otpStep1.classList.add('d-none');
|
||||
otpStep2.classList.remove('d-none');
|
||||
} else {
|
||||
showError(errorIdentifier, data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showLoading('btn-send-otp', false);
|
||||
showError(errorIdentifier, "{% trans 'An error occurred. Please try again.' %}");
|
||||
console.error('Error:', error);
|
||||
});
|
||||
});
|
||||
|
||||
btnVerifyOtp.addEventListener('click', function() {
|
||||
const code = inputCode.value.trim();
|
||||
if (!code) {
|
||||
showError(errorCode, "{% trans 'Please enter the code.' %}");
|
||||
return;
|
||||
}
|
||||
|
||||
clearErrors();
|
||||
showLoading('btn-verify-otp', true);
|
||||
|
||||
fetch("{% url 'verify_login_otp' %}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
body: 'user_id=' + encodeURIComponent(userId) + '&code=' + encodeURIComponent(code)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showLoading('btn-verify-otp', false);
|
||||
if (data.success) {
|
||||
window.location.href = data.redirect_url;
|
||||
} else {
|
||||
showError(errorCode, data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showLoading('btn-verify-otp', false);
|
||||
showError(errorCode, "{% trans 'An error occurred. Please try again.' %}");
|
||||
console.error('Error:', error);
|
||||
});
|
||||
});
|
||||
|
||||
btnBackOtp.addEventListener('click', function() {
|
||||
otpStep2.classList.add('d-none');
|
||||
otpStep1.classList.remove('d-none');
|
||||
inputCode.value = '';
|
||||
clearErrors();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -20,10 +20,26 @@
|
||||
|
||||
<div class="tab-content" id="pills-tabContent">
|
||||
|
||||
<!-- Active Shipments (Card View) -->
|
||||
<!-- Active Shipments -->
|
||||
<div class="tab-pane fade show active" id="pills-active" role="tabpanel">
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="mb-0 text-muted">{% trans "Current Shipments" %}</h5>
|
||||
<div class="btn-group" role="group" aria-label="View toggle">
|
||||
<button type="button" class="btn btn-outline-primary active" id="btn-grid-view">
|
||||
<i class="bi bi-grid"></i> {% trans "Grid" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="btn-list-view">
|
||||
<i class="bi bi-list"></i> {% trans "List" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if active_parcels %}
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Grid View -->
|
||||
<div id="view-grid" class="row g-4">
|
||||
{% for parcel in active_parcels %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card border-0 shadow-sm h-100" style="border-radius: 15px;">
|
||||
@ -35,8 +51,8 @@
|
||||
</span>
|
||||
</div>
|
||||
<h5 class="card-title">{{ parcel.description|truncatechars:30 }}</h5>
|
||||
<p class="card-text mb-1 small text-muted"><i class="fas fa-map-marker-alt"></i> <strong>{% trans "From" %}:</strong> {{ parcel.pickup_address }}</p>
|
||||
<p class="card-text mb-3 small text-muted"><i class="fas fa-flag-checkered"></i> <strong>{% trans "To" %}:</strong> {{ parcel.delivery_address }}</p>
|
||||
<p class="card-text mb-1 small text-muted"><i class="fas fa-map-marker-alt"></i> <strong>{% trans "From" %}:</strong> {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}</p>
|
||||
<p class="card-text mb-3 small text-muted"><i class="fas fa-flag-checkered"></i> <strong>{% trans "To" %}:</strong> {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}</p>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="text-primary fw-bold">{{ parcel.price }} OMR</span>
|
||||
@ -61,6 +77,100 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- List View (Initially Hidden) -->
|
||||
<div id="view-list" class="d-none">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
{% for parcel in active_parcels %}
|
||||
<div class="card border-0 shadow-sm rounded-3">
|
||||
<div class="card-body p-3">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-7 mb-3 mb-md-0">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<h5 class="card-title mb-0">{{ parcel.description|truncatechars:60 }}</h5>
|
||||
<span class="badge bg-light text-dark small">#{{ parcel.tracking_number }}</span>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-3 text-muted small">
|
||||
<span class="d-flex align-items-center gap-1">
|
||||
<i class="bi bi-geo-alt-fill text-primary"></i>
|
||||
<strong>{% trans "From" %}:</strong> {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}
|
||||
</span>
|
||||
<span class="d-flex align-items-center gap-1">
|
||||
<i class="bi bi-geo-alt-fill text-danger"></i>
|
||||
<strong>{% trans "To" %}:</strong> {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}
|
||||
</span>
|
||||
<span class="d-flex align-items-center gap-1">
|
||||
<i class="bi bi-truck"></i>
|
||||
{% if parcel.carrier %}{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}{% else %}{% trans "Waiting" %}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5 text-md-end">
|
||||
<div class="d-flex flex-column align-items-md-end gap-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="text-primary fw-bold fs-5">{{ parcel.price }} OMR</span>
|
||||
<span class="badge {% if parcel.status == 'delivered' %}bg-success{% elif parcel.status == 'cancelled' %}bg-danger{% else %}bg-warning{% endif %}">
|
||||
{{ parcel.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if parcel.payment_status == 'pending' and payments_enabled %}
|
||||
<a href="{% url 'initiate_payment' parcel.id %}" class="btn btn-sm btn-outline-primary w-100 w-md-auto">
|
||||
<i class="fas fa-credit-card me-1"></i> {% trans "Pay Now" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="badge {% if parcel.payment_status == 'paid' %}bg-success{% else %}bg-secondary{% endif %}">
|
||||
{{ parcel.get_payment_status_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if active_parcels.has_other_pages %}
|
||||
<nav aria-label="Page navigation" class="mt-5">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if active_parcels.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ active_parcels.previous_page_number }}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">«</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for i in active_parcels.paginator.page_range %}
|
||||
{% if active_parcels.number == i %}
|
||||
<li class="page-item active"><span class="page-link">{{ i }}</span></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if active_parcels.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ active_parcels.next_page_number }}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">»</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<p class="lead">{% trans "You have no active shipments." %}</p>
|
||||
@ -135,4 +245,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const gridViewBtn = document.getElementById('btn-grid-view');
|
||||
const listViewBtn = document.getElementById('btn-list-view');
|
||||
const gridView = document.getElementById('view-grid');
|
||||
const listView = document.getElementById('view-list');
|
||||
|
||||
function setView(view) {
|
||||
if (view === 'list') {
|
||||
if (gridView) gridView.classList.add('d-none');
|
||||
if (listView) listView.classList.remove('d-none');
|
||||
if (listViewBtn) listViewBtn.classList.add('active');
|
||||
if (gridViewBtn) gridViewBtn.classList.remove('active');
|
||||
localStorage.setItem('shipperDashboardView', 'list');
|
||||
} else {
|
||||
if (listView) listView.classList.add('d-none');
|
||||
if (gridView) gridView.classList.remove('d-none');
|
||||
if (gridViewBtn) gridViewBtn.classList.add('active');
|
||||
if (listViewBtn) listViewBtn.classList.remove('active');
|
||||
localStorage.setItem('shipperDashboardView', 'grid');
|
||||
}
|
||||
}
|
||||
|
||||
// Check local storage or default to grid
|
||||
const savedView = localStorage.getItem('shipperDashboardView');
|
||||
if (savedView) {
|
||||
setView(savedView);
|
||||
}
|
||||
|
||||
// Bind events
|
||||
if (gridViewBtn) gridViewBtn.addEventListener('click', () => setView('grid'));
|
||||
if (listViewBtn) listViewBtn.addEventListener('click', () => setView('list'));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -43,6 +43,7 @@ urlpatterns = [
|
||||
path('article/1/', views.article_detail, name='article_detail'),
|
||||
path('ajax/get-governates/', views.get_governates, name='get_governates'),
|
||||
path('ajax/get-cities/', views.get_cities, name='get_cities'),
|
||||
path('ajax/chatbot/', views.chatbot, name='chatbot'),
|
||||
path('privacy-policy/', views.privacy_policy, name='privacy_policy'),
|
||||
path('terms-conditions/', views.terms_conditions, name='terms_conditions'),
|
||||
path('contact/', views.contact, name='contact'),
|
||||
@ -50,4 +51,8 @@ urlpatterns = [
|
||||
path('profile/', views.profile_view, name='profile'),
|
||||
path('profile/edit/', views.edit_profile, name='edit_profile'),
|
||||
path('profile/verify-otp/', views.verify_otp_view, name='verify_otp'),
|
||||
|
||||
# OTP Login
|
||||
path('login/request-otp/', views.request_login_otp, name='request_login_otp'),
|
||||
path('login/verify-otp/', views.verify_login_otp, name='verify_login_otp'),
|
||||
]
|
||||
193
core/views.py
193
core/views.py
@ -13,6 +13,9 @@ from django.urls import reverse
|
||||
from .payment_utils import ThawaniPay
|
||||
from django.conf import settings
|
||||
from django.core.mail import send_mail
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.db.models import Avg, Count
|
||||
import random
|
||||
import string
|
||||
from .whatsapp_utils import (
|
||||
@ -23,6 +26,8 @@ from .whatsapp_utils import (
|
||||
send_whatsapp_message
|
||||
)
|
||||
from .mail import send_contact_message, send_html_email
|
||||
import json
|
||||
from ai.local_ai_api import LocalAIApi
|
||||
|
||||
def index(request):
|
||||
tracking_id = request.GET.get('tracking_id')
|
||||
@ -36,11 +41,24 @@ def index(request):
|
||||
|
||||
testimonials = Testimonial.objects.filter(is_active=True)
|
||||
|
||||
# Top 5 Drivers (by Average Rating)
|
||||
top_drivers = Profile.objects.filter(role='car_owner').annotate(
|
||||
avg_rating=Avg('user__received_ratings__rating'),
|
||||
rating_count=Count('user__received_ratings')
|
||||
).filter(rating_count__gt=0).order_by('-avg_rating')[:5]
|
||||
|
||||
# Top 5 Shippers (by Shipment Count)
|
||||
top_shippers = Profile.objects.filter(role='shipper').annotate(
|
||||
shipment_count=Count('user__sent_parcels')
|
||||
).order_by('-shipment_count')[:5]
|
||||
|
||||
return render(request, 'core/index.html', {
|
||||
'parcel': parcel,
|
||||
'error': error,
|
||||
'tracking_id': tracking_id,
|
||||
'testimonials': testimonials
|
||||
'testimonials': testimonials,
|
||||
'top_drivers': top_drivers,
|
||||
'top_shippers': top_shippers
|
||||
})
|
||||
|
||||
def register(request):
|
||||
@ -58,14 +76,16 @@ def register(request):
|
||||
|
||||
# Send OTP
|
||||
method = form.cleaned_data.get('verification_method', 'email')
|
||||
otp_msg = _("Your Masar Verification Code is %(code)s") % {'code': code}
|
||||
|
||||
if method == 'whatsapp':
|
||||
phone = user.profile.phone_number
|
||||
send_whatsapp_message(phone, f"Your verification code is: {code}")
|
||||
send_whatsapp_message(phone, otp_msg)
|
||||
messages.info(request, _("Verification code sent to WhatsApp."))
|
||||
else:
|
||||
send_html_email(
|
||||
subject=_('Verification Code'),
|
||||
message=f'Your verification code is: {code}',
|
||||
message=otp_msg,
|
||||
recipient_list=[user.email],
|
||||
title=_('Welcome to Masar!'),
|
||||
request=request
|
||||
@ -124,9 +144,20 @@ def dashboard(request):
|
||||
|
||||
if profile.role == 'shipper':
|
||||
all_parcels = Parcel.objects.filter(shipper=request.user).order_by('-created_at')
|
||||
active_parcels = all_parcels.exclude(status__in=['delivered', 'cancelled'])
|
||||
active_parcels_list = all_parcels.exclude(status__in=['delivered', 'cancelled'])
|
||||
history_parcels = all_parcels.filter(status__in=['delivered', 'cancelled'])
|
||||
|
||||
# Pagination for Active Shipments
|
||||
page = request.GET.get('page', 1)
|
||||
paginator = Paginator(active_parcels_list, 9) # Show 9 parcels per page
|
||||
|
||||
try:
|
||||
active_parcels = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
active_parcels = paginator.page(1)
|
||||
except EmptyPage:
|
||||
active_parcels = paginator.page(paginator.num_pages)
|
||||
|
||||
platform_profile = PlatformProfile.objects.first()
|
||||
payments_enabled = platform_profile.enable_payment if platform_profile else True
|
||||
|
||||
@ -142,9 +173,20 @@ def dashboard(request):
|
||||
payments_enabled = platform_profile.enable_payment if platform_profile else True
|
||||
|
||||
if payments_enabled:
|
||||
available_parcels = Parcel.objects.filter(status='pending', payment_status='paid').order_by('-created_at')
|
||||
available_parcels_list = Parcel.objects.filter(status='pending', payment_status='paid').order_by('-created_at')
|
||||
else:
|
||||
available_parcels = Parcel.objects.filter(status='pending').order_by('-created_at')
|
||||
available_parcels_list = Parcel.objects.filter(status='pending').order_by('-created_at')
|
||||
|
||||
# Pagination for Available Shipments
|
||||
page = request.GET.get('page', 1)
|
||||
paginator = Paginator(available_parcels_list, 9) # Show 9 parcels per page
|
||||
|
||||
try:
|
||||
available_parcels = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
available_parcels = paginator.page(1)
|
||||
except EmptyPage:
|
||||
available_parcels = paginator.page(paginator.num_pages)
|
||||
|
||||
# Active: Picked up or In Transit
|
||||
my_parcels = Parcel.objects.filter(carrier=request.user).exclude(status__in=['delivered', 'cancelled']).order_by('-created_at')
|
||||
@ -360,10 +402,12 @@ def edit_profile(request):
|
||||
|
||||
# 4. Send OTP
|
||||
method = data.get('otp_method', 'email')
|
||||
otp_msg = _("Your Masar Update Code is %(code)s") % {'code': code}
|
||||
|
||||
if method == 'whatsapp':
|
||||
# Use current phone if available, else new phone
|
||||
phone = request.user.profile.phone_number or data['phone_number']
|
||||
send_whatsapp_message(phone, f"Your verification code is: {code}")
|
||||
send_whatsapp_message(phone, otp_msg)
|
||||
messages.info(request, _("Verification code sent to WhatsApp."))
|
||||
else:
|
||||
# Default to email
|
||||
@ -371,7 +415,7 @@ def edit_profile(request):
|
||||
target_email = data['email']
|
||||
send_html_email(
|
||||
subject=_('Verification Code'),
|
||||
message=f'Your verification code is: {code}',
|
||||
message=otp_msg,
|
||||
recipient_list=[target_email],
|
||||
title=_('Profile Update Verification'),
|
||||
request=request
|
||||
@ -473,3 +517,136 @@ def rate_driver(request, parcel_id):
|
||||
'form': form,
|
||||
'parcel': parcel
|
||||
})
|
||||
|
||||
@require_POST
|
||||
def request_login_otp(request):
|
||||
identifier = request.POST.get('identifier')
|
||||
|
||||
if not identifier:
|
||||
return JsonResponse({'success': False, 'message': _('Please enter an email or phone number.')})
|
||||
|
||||
# Clean identifier
|
||||
identifier = identifier.strip()
|
||||
|
||||
user = None
|
||||
method = 'email'
|
||||
|
||||
# Try to find user by email
|
||||
user = User.objects.filter(email__iexact=identifier).first()
|
||||
|
||||
# If not found, try by phone number
|
||||
if not user:
|
||||
profile = Profile.objects.filter(phone_number=identifier).first()
|
||||
if profile:
|
||||
user = profile.user
|
||||
method = 'whatsapp'
|
||||
else:
|
||||
# Fallback: maybe they entered a phone without country code or with?
|
||||
# For now, simplistic search
|
||||
pass
|
||||
|
||||
if not user:
|
||||
# Don't reveal if user exists or not for security, but for UX on this project we can be a bit more helpful
|
||||
return JsonResponse({'success': False, 'message': _('User not found with this email or phone number.')})
|
||||
|
||||
if not user.is_active:
|
||||
return JsonResponse({'success': False, 'message': _('Account is inactive. Please verify registration first.')})
|
||||
|
||||
# Generate OTP
|
||||
code = ''.join(random.choices(string.digits, k=6))
|
||||
OTPVerification.objects.create(user=user, code=code, purpose='login')
|
||||
|
||||
# Send OTP
|
||||
otp_msg = _("Your Masar Login Code is %(code)s. Do not share this code.") % {'code': code}
|
||||
|
||||
try:
|
||||
if method == 'whatsapp':
|
||||
phone = user.profile.phone_number
|
||||
send_whatsapp_message(phone, otp_msg)
|
||||
message_sent = _("OTP sent to your WhatsApp.")
|
||||
else:
|
||||
send_html_email(
|
||||
subject=_('Login OTP'),
|
||||
message=otp_msg,
|
||||
recipient_list=[user.email],
|
||||
title=_('Login Verification'),
|
||||
request=request
|
||||
)
|
||||
message_sent = _("OTP sent to your email.")
|
||||
|
||||
return JsonResponse({'success': True, 'message': message_sent, 'user_id': user.id})
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'message': _('Failed to send OTP. Please try again.')})
|
||||
|
||||
@require_POST
|
||||
def verify_login_otp(request):
|
||||
user_id = request.POST.get('user_id')
|
||||
code = request.POST.get('code')
|
||||
|
||||
if not user_id or not code:
|
||||
return JsonResponse({'success': False, 'message': _('Invalid request.')})
|
||||
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
otp = OTPVerification.objects.filter(
|
||||
user=user,
|
||||
code=code,
|
||||
purpose='login',
|
||||
is_verified=False
|
||||
).latest('created_at')
|
||||
|
||||
if otp.is_valid():
|
||||
# Cleanup
|
||||
otp.is_verified = True
|
||||
otp.save()
|
||||
|
||||
# Login
|
||||
login(request, user)
|
||||
|
||||
return JsonResponse({'success': True, 'redirect_url': reverse('dashboard')})
|
||||
else:
|
||||
return JsonResponse({'success': False, 'message': _('Invalid or expired OTP.')})
|
||||
|
||||
except (User.DoesNotExist, OTPVerification.DoesNotExist):
|
||||
return JsonResponse({'success': False, 'message': _('Invalid OTP.')})
|
||||
|
||||
@require_POST
|
||||
def chatbot(request):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
user_message = data.get("message", "")
|
||||
language = data.get("language", "en")
|
||||
|
||||
if not user_message:
|
||||
return JsonResponse({"success": False, "error": "Empty message"})
|
||||
|
||||
system_prompt = (
|
||||
"You are MasarX AI, a helpful and professional assistant for the Masar logistics platform. "
|
||||
"The platform connects shippers with drivers for small parcel deliveries. "
|
||||
"Answer the user's questions about shipping, tracking, becoming a driver, or general support. "
|
||||
"If the user speaks Arabic, reply in Arabic. If English, reply in English. "
|
||||
"Keep responses concise and helpful."
|
||||
)
|
||||
|
||||
if language == "ar":
|
||||
system_prompt += " The user is currently browsing in Arabic."
|
||||
else:
|
||||
system_prompt += " The user is currently browsing in English."
|
||||
|
||||
response = LocalAIApi.create_response({
|
||||
"input": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_message},
|
||||
]
|
||||
})
|
||||
|
||||
if response.get("success"):
|
||||
text = LocalAIApi.extract_text(response)
|
||||
return JsonResponse({"success": True, "response": text})
|
||||
else:
|
||||
return JsonResponse({"success": False, "error": response.get("error", "AI Error")})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({"success": False, "error": "Invalid JSON"})
|
||||
except Exception as e:
|
||||
return JsonResponse({"success": False, "error": str(e)})
|
||||
|
||||
Binary file not shown.
@ -1315,4 +1315,18 @@ msgstr "شكراً لملاحظاتك!"
|
||||
#~ msgstr "لم ترسل أي شحنات بعد."
|
||||
|
||||
#~ msgid "Find Loads"
|
||||
#~ msgstr "البحث عن شحنات"
|
||||
#~ msgstr "البحث عن شحنات"
|
||||
msgid "Top Rated Drivers"
|
||||
msgstr "أفضل السائقين تقييماً"
|
||||
|
||||
msgid "Top Shippers"
|
||||
msgstr "أفضل الشاحنين"
|
||||
|
||||
msgid "No ratings yet. Be the first!"
|
||||
msgstr "لا توجد تقييمات بعد. كن الأول!"
|
||||
|
||||
msgid "No shipments yet. Start shipping now!"
|
||||
msgstr "لا توجد شحنات بعد. ابدأ الشحن الآن!"
|
||||
|
||||
msgid "Shipments"
|
||||
msgstr "شحنات"
|
||||
|
||||
@ -103,4 +103,62 @@ h1, h2, h3, h4, h5, h6 {
|
||||
.status-pending { background: #FFE8CC; color: #D9480F; }
|
||||
.status-picked_up { background: #E3FAFC; color: #0B7285; }
|
||||
.status-in_transit { background: #E7F5FF; color: #1864AB; }
|
||||
.status-delivered { background: #EBFBEE; color: #2B8A3E; }
|
||||
.status-delivered { background: #EBFBEE; color: #2B8A3E; }
|
||||
|
||||
/* Chat Widget */
|
||||
#masar-chat-widget {
|
||||
position: fixed;
|
||||
bottom: 90px;
|
||||
right: 20px;
|
||||
width: 350px;
|
||||
height: 500px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#masar-chat-toggle {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
background-color: var(--accent-orange);
|
||||
color: white;
|
||||
border: none;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
#masar-chat-toggle:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* RTL Support */
|
||||
[dir="rtl"] #masar-chat-widget {
|
||||
right: auto;
|
||||
left: 20px;
|
||||
}
|
||||
[dir="rtl"] #masar-chat-toggle {
|
||||
right: auto;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.typing-dots span {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #adb5bd;
|
||||
border-radius: 50%;
|
||||
margin: 0 2px;
|
||||
animation: typing 1s infinite;
|
||||
}
|
||||
.typing-dots span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-dots span:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes typing {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
118
static/js/chat.js
Normal file
118
static/js/chat.js
Normal file
@ -0,0 +1,118 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const chatWidget = document.getElementById('masar-chat-widget');
|
||||
const chatToggle = document.getElementById('masar-chat-toggle');
|
||||
const chatClose = document.getElementById('masar-chat-close');
|
||||
const chatForm = document.getElementById('masar-chat-form');
|
||||
const chatInput = document.getElementById('masar-chat-input');
|
||||
const chatMessages = document.getElementById('masar-chat-messages');
|
||||
|
||||
if (!chatWidget) return;
|
||||
|
||||
// Toggle Chat
|
||||
function toggleChat() {
|
||||
if (chatWidget.classList.contains('d-none')) {
|
||||
chatWidget.classList.remove('d-none');
|
||||
setTimeout(() => chatInput.focus(), 100);
|
||||
} else {
|
||||
chatWidget.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
chatToggle.addEventListener('click', toggleChat);
|
||||
chatClose.addEventListener('click', toggleChat);
|
||||
|
||||
// Send Message
|
||||
chatForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
// Add User Message
|
||||
addMessage(message, 'user');
|
||||
chatInput.value = '';
|
||||
|
||||
// Show Typing Indicator
|
||||
const typingId = addTypingIndicator();
|
||||
|
||||
// Send to Backend
|
||||
fetch('/ajax/chatbot/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
language: document.documentElement.lang || 'en'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
removeMessage(typingId);
|
||||
if (data.success) {
|
||||
addMessage(data.response, 'bot');
|
||||
} else {
|
||||
addMessage('Sorry, I encountered an error.', 'bot');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
removeMessage(typingId);
|
||||
addMessage('Sorry, connection error.', 'bot');
|
||||
console.error('Error:', error);
|
||||
});
|
||||
});
|
||||
|
||||
function addMessage(text, sender) {
|
||||
const div = document.createElement('div');
|
||||
div.className = `d-flex mb-3 ${sender === 'user' ? 'justify-content-end' : 'justify-content-start'}`;
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = `p-3 rounded-3 shadow-sm ${sender === 'user' ? 'bg-primary text-white' : 'bg-light text-dark'}`;
|
||||
bubble.style.maxWidth = '80%';
|
||||
bubble.style.wordWrap = 'break-word';
|
||||
// Convert newlines to <br> for basic formatting
|
||||
bubble.innerHTML = text.replace(/\n/g, '<br>');
|
||||
|
||||
div.appendChild(bubble);
|
||||
chatMessages.appendChild(div);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
return div.id;
|
||||
}
|
||||
|
||||
function addTypingIndicator() {
|
||||
const id = 'typing-' + Date.now();
|
||||
const div = document.createElement('div');
|
||||
div.id = id;
|
||||
div.className = 'd-flex mb-3 justify-content-start';
|
||||
div.innerHTML = ""
|
||||
+ "<div class=\"bg-light p-3 rounded-3 shadow-sm\">"
|
||||
+ " <div class=\"typing-dots\">"
|
||||
+ " <span></span><span></span><span></span>"
|
||||
+ " </div>"
|
||||
+ "</div>"
|
||||
;
|
||||
chatMessages.appendChild(div);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
return id;
|
||||
}
|
||||
|
||||
function removeMessage(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.remove();
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
});
|
||||
50
update_base.py
Normal file
50
update_base.py
Normal file
@ -0,0 +1,50 @@
|
||||
import os
|
||||
|
||||
with open("core/templates/base.html", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
widget_html = """
|
||||
<!-- Chat Widget -->
|
||||
<button id="masar-chat-toggle" class="d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-chat-dots-fill fs-4"></i>
|
||||
</button>
|
||||
|
||||
<div id="masar-chat-widget" class="d-none bg-white rounded-4 shadow overflow-hidden">
|
||||
<div class="bg-dark text-white p-3 d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-robot me-2 fs-5"></i>
|
||||
<h6 class="mb-0 fw-bold">MasarX AI</h6>
|
||||
</div>
|
||||
<button id="masar-chat-close" class="btn btn-sm btn-link text-white p-0">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="masar-chat-messages" class="flex-grow-1 p-3 bg-light overflow-auto">
|
||||
<div class="d-flex mb-3 justify-content-start">
|
||||
<div class="p-3 rounded-3 shadow-sm bg-light text-dark" style="max-width: 80%;">
|
||||
{% trans "Hello! How can I help you with your shipments today?" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3 border-top bg-white">
|
||||
<form id="masar-chat-form" class="d-flex">
|
||||
<input type="text" id="masar-chat-input" class="form-control me-2" placeholder="{% trans 'Type a message...' %}" autocomplete="off">
|
||||
<button type="submit" class="btn btn-masarx-primary px-3">
|
||||
<i class="bi bi-send-fill"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/chat.js' %}?v={{ deployment_timestamp }}"></script>
|
||||
"
|
||||
|
||||
if "masar-chat-toggle" not in content:
|
||||
new_content = content.replace("</body>", widget_html + "\n</body>")
|
||||
with open("core/templates/base.html", "w") as f:
|
||||
f.write(new_content)
|
||||
print("Updated base.html")
|
||||
else:
|
||||
print("Widget already exists")
|
||||
36
update_base_v2.py
Normal file
36
update_base_v2.py
Normal file
@ -0,0 +1,36 @@
|
||||
<!-- Chat Widget -->
|
||||
<button id="masar-chat-toggle" class="d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-chat-dots-fill fs-4"></i>
|
||||
</button>
|
||||
|
||||
<div id="masar-chat-widget" class="d-none bg-white rounded-4 shadow overflow-hidden">
|
||||
<div class="bg-dark text-white p-3 d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-robot me-2 fs-5"></i>
|
||||
<h6 class="mb-0 fw-bold">MasarX AI</h6>
|
||||
</div>
|
||||
<button id="masar-chat-close" class="btn btn-sm btn-link text-white p-0">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="masar-chat-messages" class="flex-grow-1 p-3 bg-light overflow-auto">
|
||||
<div class="d-flex mb-3 justify-content-start">
|
||||
<div class="p-3 rounded-3 shadow-sm bg-light text-dark" style="max-width: 80%;">
|
||||
{% trans "Hello! How can I help you with your shipments today?" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3 border-top bg-white">
|
||||
<form id="masar-chat-form" class="d-flex">
|
||||
<input type="text" id="masar-chat-input" class="form-control me-2" placeholder="{% trans 'Type a message...' %}" autocomplete="off">
|
||||
<button type="submit" class="btn btn-masarx-primary px-3">
|
||||
<i class="bi bi-send-fill"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/chat.js' %}?v={{ deployment_timestamp }}"></script>
|
||||
</body>
|
||||
Loading…
x
Reference in New Issue
Block a user